A bytecode-based virtual machine to implement scripting/filtering support in your golang project.

Overview

GoDoc Go Report Card license

eval-filter

The evalfilter package provides an embeddable evaluation-engine, which allows simple logic which might otherwise be hardwired into your golang application to be delegated to (user-written) script(s).

There is no shortage of embeddable languages which are available to the golang world, this library is intended to be something that is:

  • Simple to embed.
  • Simple to use, as there are only three methods you need to call:
  • Simple to understand.
  • As fast as it can be, without being too magical.

The scripting language is C-like, and is generally intended to allow you to filter objects, which means you might call the same script upon multiple objects, and the script will return either true or false as appropriate to denote whether some action might be taken by your application against that particular object.

It certainly is possible for you to handle arbitrary return-values from the script(s) you execute, and indeed the script itself could call back into your application to carry out tasks, via the addition of new primitives implemented and exported by your host application, which would make the return value almost irrelevant.

If you go down that route then this repository contains a general-purpose scripting-language, which can be used to execute user-supplied scripts.

My Google GMail message labeller uses the evalfilter in such a standalone manner, executing a script for each new/unread email by default. The script can then add labels to messages based upon their sender/recipients/subjects. etc. The notion of filtering there doesn't make sense, it just wants to execute flexible operations on messages.

However the ideal use-case, for which this was designed, is that your application receives objects of some kind, perhaps as a result of incoming webhook submissions, network events, or similar, and you wish to decide how to handle those objects in a flexible fashion.

Implementation

In terms of implementation the script to be executed is split into tokens by the lexer, then those tokens are parsed into an abstract-syntax-tree. Once the AST exists it is walked by the compiler and a series of bytecode instructions are generated.

Once the bytecode has been generated it can be executed multiple times, there is no state which needs to be maintained, which makes actually executing the script (i.e. running the bytecode) a fast process.

At execution-time the bytecode which was generated is interpreted by a simple virtual machine. The virtual machine is fairly naive implementation of a stack-based virtual machine, with some runtime support to provide the builtin-functions, as well as supporting the addition of host-specific functions.

The bytecode itself is documented briefly in BYTECODE.md, but it is not something you should need to understand to use the library, only if you're interested in debugging a misbehaving script.

Scripting Facilities

Types

The scripting-language this package presents supports the basic types you'd expect:

  • Arrays.
  • Floating-point numbers.
  • Hashes.
  • Integers.
  • Regular expressions.
  • Strings.
  • Time / Date values.
    • i.e. We can use reflection to handle time.Time values in any structure/map we're operating upon.

The types are supported both in the language itself, and in the reflection-layer which is used to allow the script access to fields in the Golang object/map you supply to it.

Built-In Functions

These are the built-in functions which are always available, though your users can write their own functions within the language (see functions).

You can also easily add new primitives to the engine, by defining a function in your golang application and exporting it to the scripting-environment. For example the print function to generate output from your script is just a simple function implemented in Golang and exported to the environment. (This is true of all the built-in functions, which are registered by default.)

  • float(value)
    • Tries to convert the value to a floating-point number, returns Null on failure.
    • e.g. float("3.13").
  • getenv(value)
    • Return the value of the named environmental variable, or "" if not found.
  • int(value)
    • Tries to convert the value to an integer, returns Null on failure.
    • e.g. int("3").
  • keys
    • Returns the available keys in the specified hash, in sorted order.
  • len(field | value)
    • Returns the length of the given value, or the contents of the given field.
    • For arrays it returns the number of elements, as you'd expect.
  • lower(field | value)
    • Return the lower-case version of the given input.
  • print(field|value [, fieldN|valueN] )
    • Print the given values.
  • printf("Format string ..", arg1, arg2 .. argN);
    • Print the given values, with the specified golang format string
      • For example printf("%s %d %t\n", "Steve", 9 / 3 , ! false );
  • reverse(["Surname", "Forename"]);
    • Sorts the given array in reverse.
    • Add true as the second argument to ignore case.
  • sort(["Surname", "Forename"]);
    • Sorts the given array.
    • Add true as the second argument to ignore case.
  • split("string", "value");
    • Splits a string into an array, by the given substring..
  • sprintf("Format string ..", arg1, arg2 .. argN);
    • Format the given values, using the specified golang format string.
  • string( )
    • Converts a value to a string. e.g. "string(3/3.4)".
  • trim(field | string)
    • Returns the given string, or the contents of the given field, with leading/trailing whitespace removed.
  • type(field | value)
    • Returns the type of the given field, as a string.
      • For example string, integer, float, array, boolean, or null.
  • upper(field | value)
    • Return the upper-case version of the given input.
  • hour(field|value), minute(field:value), seconds(field:value
    • Allow converting a time to HH:MM:SS.
  • day(field|value), month(field:value), year(field:value
    • Allow converting a time to DD/MM/YYYY.
  • weekday(field|value)
    • Allow converting a time to "Saturday", "Sunday", etc.
  • now() & time() both return the current time.

Conditionals

As you'd expect the facilities are pretty normal/expected:

  • Perform comparisons of strings and numbers:
    • equality:
      • "if ( Message == "test" ) { return true; }"
    • inequality:
      • "if ( Count != 3 ) { return true; }"
    • size (<, <=, >, >=):
      • "if ( Count >= 10 ) { return false; }"
      • "if ( Hour >= 8 && Hour <= 17 ) { return false; }"
    • String matching against a regular expression:
      • "if ( Content ~= /needle/ )"
      • "if ( Content ~= /needle/i )"
        • With case insensitivity
    • Does not match a regular expression:
      • "if ( Content !~ /some text we don't want/ )"
    • Test if an array contains a value:
      • "return ( Name in [ "Alice", "Bob", "Chris" ] );"
  • Ternary expressions are also supported - but nesting them is a syntax error!
    • "a = Title ? Title : Subject;"
    • "return( result == 3 ? "Three" : "Four!" );"

Loops

Our script implements a golang-style loop, using either for or while as the keyword:

count = 0;
while ( count < 10 ) {
     print( "Count: ", count, "\n" );
     count++;
}

You could use either statement to iterate over an array contents, but that would be a little inefficient:

items = [ "Some", "Content", "Here" ];
i = 0;
for ( i < len(items) ) {
   print( items[i], "\n" );
   i++
}

A more efficient and readable approach is to iterate over arrays, and the characters inside a string, via foreach. You can receive both the index and the item at each step of the iteration like so:

foreach index, item in [ "My", "name", "is", "Steve" ] {
    printf( "%d: %s\n", index, item );
}

If you don't supply an index you'll receive just the item being iterated over instead, as you would expect (i.e. we don't default to returning the index, but the value in this case):

len = 0;
foreach char in "狐犬" {
    len++;
}
return( len == 2 );

The same kind of iteration works over hashes too (the single-argument version of the foreach loop iterates over values, rather than keys. Hash keys are available via keys so that seems like a more useful thing to return):

foreach key,value in { "Name": "Steve", "Location": "Finland" } {
  printf("Key %s has value %s\n", key, value );
}

The final helper is the ability to create arrays of integers via the .. primitive:

sum = 0;
foreach item in 1..10 {
    sum += item;
}
print( "Sum is ", sum, "\n" );

Here you note that len++ and sum += item; work as you'd expect. There is support for +=, -=, *=, and /=. The ++ and -- postfix operators are both available (for integers and floating-point numbers).

Functions

You can declare functions, for example:

function sum( input ) {
   local result;
   result = 0;
   foreach item in input {
     result = result + item;
   }
   return result;
}

printf("Sum is %d\n", sum(1..10));
return false;

See _examples/scripts/scope.in for another brief example, and discussion of scopes.

Case / Switch

We support the use of switch and case to simplify the handling of some control-flow. An example would look like this:

switch( Subject ) {
  case /^Re:/i {
     printf("Reply\n");
  }
  case /^fwd:/i {
     printf("Forwarded message\n");
  }
  case "DEAR" + "  " + WINNER" {
     printf("SPAM\n");
  }
  case "YOU HAVE WON" {
     printf("SPAM\n");
  }
  default {
     printf("New message!\n");
  }
}

Note that the case expression supports the following, as demonstrated in our switch example:

  • Expression matches.
  • Literal matches.
  • Regular expression matches.

To avoid fall-through-related bugs we've explicitly designed the case-statements to take blocks as arguments, rather than statements.

NOTE: Only the first matching case-statement will execute. In the following example only one message will be output:

count = 1;

switch( count ) {
  case 1 {
     printf("Match!\n");
  }
  case 1 {
     printf("This is not a match - the previous case statement won!\n");
  }
}

Use Cases

The motivation for this project came from a problem encountered while working:

  • I wanted to implement a simple "on-call notifier".
    • When messages were posted to Slack channels I wanted to sometimes trigger a phone-call to the on-call engineer.
    • Of course not all Slack-messages were worth waking up an engineer for..

The expectation was that non-developers might want to change the matching of messages to update the messages which were deemed worthy of waking up the on-call engineer. They shouldn't need to worry about rebuilding the on-call application, nor should they need to understand Go. So the logic was moved into a script and this evaluation engine was born.

Each time a Slack message was received it would be placed into a simple structure:

type Message struct {
    Author  string
    Channel string
    Message string
    Sent    time.Time
}

Then a simple script could then be executed against that object to decide whether to initiate a phone-call:

//
// You can see that comments are prefixed with "//".
//
// In my application a phone-call would be trigged if this
// script hit `return true;`.  If the return value was `false`
// then nothing would happen.
//

//
// If this is within office hours we'll assume somebody is around to
// handle the issue, so there is no need to raise a call.
//
if ( hour(Sent) >= 9 || hour(Sent) <= 17 ) {

    // 09AM - 5PM == Working day.  No need to notify anybody.

    // Unless it is a weekend, of course!
    if ( weekday(Sent) != "Saturday" && weekday(Sent) != "Sunday" ) {
       return false;
    }
}

//
// A service crashed with a panic?
//
// If so raise the engineer.
//
if ( Message ~=  /panic/i ) { return true; }


//
// At this point we decide the message is not important, so
// we ignore it.
//
// In a real-life implementation we'd probably work the other
// way round.  Default to triggering the call unless we knew
// it was a low-priority/irrelevant message.
//
return false;

You'll notice that we test fields such as Sent and Message here which come from the object we were given. That works due to the magic of reflection. Similarly we called a number of built-in functions related to time/date. These functions understand the golang time.Time type, from which the Sent value was read via reflection.

(All time.Time values are converted to seconds-past the Unix Epoch, but you can retrieve all the appropriate fields via hour(), minute(), day(), year(), weekday(), etc, as you would expect. Using them literally will return the Epoch value.)

Security

The user-supplied script is parsed and turned into a set of bytecode-instructions which are then executed. The bytecode instruction set is pretty minimal, and specifically has zero access to:

  • Your filesystem.
    • i.e. Reading files is not possible, neither is writing them.
  • The network.
    • i.e. Making outgoing network requests is not possible.

Of course you can export functions from your host-application to the scripting environment, to allow such things. If you do add primitives that have the possibility to cause security problems then the onus is definitely on you to make sure such accesses are either heavily audited or restricted appropriately.

Denial of Service

When it comes to security problems the most obvious issue we might suffer from is denial-of-service attacks; it is certainly possible for this library to be given faulty programs, for example invalid syntax, or references to undefined functions. Failures such as those would be detected at parse/run time, as appropriate.

In short running user-supplied scripts should be safe, but there is one obvious exception, the following program is valid:

print( "Hello, I'm wasting your time\n") ;

while( 1 ) {
  // Do nothing ..
}

print( "I'm never reached!\n" );

This program will never terminate! If you're handling untrusted user-scripts, you'll want to ensure that you explicitly setup a timeout period.

The following will do what you expect:

// Create the evaluator on the (malicious) script
eval := evalfilter.New(`while( 1 ) { } `)

// Setup a timeout period of five seconds
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
eval.SetContext(ctx)

// Now prepare as usual
err = eval.Prepare()
if ( err != nil ) { // handle error }

// Now execute as usual
ret, err = eval.Execute( object )
if ( err != nil ) { // handle error }

The program will be terminated with an error after five seconds, which means that your host application will continue to run rather than being blocked forever!

Sample Usage

To give you a quick feel for how things look you could consult the following simple examples:

  • example_test.go.
    • This filters a list of people by their age.
  • example_function_test.go.
    • This exports a function from the golang-host application to the scripting environment.
      • This is a demonstration of how you'd provide extra functionality when embedding the engine in your own application.
    • The new function is then used to filter a list of people.
  • example_user_defined_function_test.go
    • Writing a function within the scripting-environment, and then calling it.
  • _examples/embedded/variable/
    • Shows how to pass a variable back and forth between your host application and the scripting environment

Additional Examples

Additional examples of using the library to embed scripting support into simple host applications are available beneath the _examples/embedded directory.

There are also sample scripts contained beneath _examples/scripts which you can examine.

The standalone driver located beneath cmd/evalfilter allows you to examine bytecode, tokens, and run the example scripts, as documented later in this README file.

Finally if you want to compile this library to webassembly, and use it in a web-page that is also possible! See wasm/ for details.

Standalone Use

If you wish to experiment with script-syntax, after looking at the example scripts you can install the standalone driver:

go get github.com/skx/evalfilter/v2/cmd/evalfilter

This driver, contained within the repository at cmd/evalfilter has a number of sub-commands to allow you to experiment with the scripting environment:

  • Output a dissassembly of the bytecode instructions the compiler generated when preparing your script.
  • Run a script.
    • Optionally with a JSON object as input.
  • View the lexer and parser outputs.

Help is available by running evalfilter help, and the sub-commands are documented thoroughly, along with sample output.

TAB-completion is supported if you're running bash, execute the following to enable it:

$ source <(evalfilter bash-completion)

Benchmarking

The scripting language should be fast enough for most purposes; it will certainly cope well with running simple scripts for every incoming HTTP-request, for example. If you wish to test the speed there are some local benchmarks available.

You can run the benchmarks as follows:

go test -test.bench=evalfilter_ -benchtime=10s -run=^t
goos: linux
goarch: amd64
pkg: github.com/skx/evalfilter/v2
Benchmark_evalfilter_complex_map-4   	 4426123	      2721 ns/op
Benchmark_evalfilter_complex_obj-4   	 7657472	      1561 ns/op
Benchmark_evalfilter_simple-4        	15309301	       818 ns/op
Benchmark_evalfilter_trivial-4       	100000000	       105 ns/op
PASS
ok  	github.com/skx/evalfilter/v2	52.258s

The examples there are not particularly representative, but they will give you an idea of the general speed. In the real world the speed of the evaluation engine is unlikely to be a significant bottleneck.

One interesting thing that shows up clearly is that working with a struct is significantly faster than working with a map. I can only assume that the reflection overhead is shorter there, but I don't know why.

Fuzz Testing

Fuzz-testing is basically magic - you run your program with random input, which stress-tests it and frequently exposes corner-cases you've not considered.

This project has been fuzz-tested repeatedly, and FUZZING.md contains notes on how you can carry out testing of your own.

API Stability

The API will remain as-is for given major release number, so far we've had we've had two major releases:

  • 1.x.x
    • The initial implementation which parsed script into an AST then walked it.
  • 2.x.x
    • The updated design which parses the given script into an AST, then generates bytecode to execute when the script is actually run.

The second release was implemented to perform a significant speedup for the case where the same script might be reused multiple times.

See Also

This repository was put together after experimenting with a scripting language, and writing a BASIC interpreter along with a FORTH interpreter.

I've also played around with a couple of compilers which might be interesting to refer to:

Github Setup

This repository is configured to run tests upon every commit, and when pull-requests are created/updated. The testing is carried out via .github/run-tests.sh which is used by the github-action-tester action.

Steve

Comments
  • a method to serialize object.Object into a JSON compatible struct

    a method to serialize object.Object into a JSON compatible struct

    With eval.Execute returning the actual object.Object, it would be very useful to have a way to convert the object.Object into a struct that can be serialized as JSON.

    A simple eval script can then be used to create JSON views by plucking the data from the object, and returning a different view of the data, like in this sample script:

    if (name == "Dan") {
      return {
        "first_name": name,
       "city": address.city,
      };
    }
    

    Even though my actual object contains significantly more data, in this particular case, it'd only be a JSON object that returns a first name and a city.

    enhancement language sponsored 
    opened by dsikes 11
  • catching an error within a script

    catching an error within a script

    So, I was using the latest version of Eval Filter and I was attempting to process some JSON data. The data should be consistent in terms of the fields that it contains, but in some cases, they are not. So, this simple script,

    counter = 0;
    if foo.bar == true {
        counter++;
    }
    

    results in the following panic if foo or foo.bar is not present

    panic: reflect: call of reflect.Value.Interface on zero Value
    

    Is there a way we could support a try/catch? This would allow the system to raise an error, and then the caller can choose what to do with the error by handling it or ignoring it depending on their use case.

    bug sponsored 
    opened by dsikes 6
  • 130 user defined functions

    130 user defined functions

    Once complete this pull-request will allow users to define their own functions, via the new function keyword:

         function foo() {
             return true;
         }
    
    
         if ( foo() ) {
             printf("All is OK\n" );
         }
         return 1;
    

    This will close #130.

    opened by skx 4
  • Examine similar projects

    Examine similar projects

    Here's a small list of similar/related projects I could take inspiration from:

    • https://github.com/benhoyt/littlelang/
      • Small language, contains a sub-implementation of itself.
    • https://github.com/antonmedv/expr
      • Bytecode-based evaluation engine.
      • Wins all the benchmarks!
        • https://github.com/antonmedv/golang-expression-evaluation-comparison
    • https://github.com/haifenghuang/magpie
      • Simple language.
    • Golem-Language is another bytecode-based virtual machine
      • https://github.com/mjarmy/golem-lang
      • Notably has support for switch-statements.
    • https://github.com/d5/tengo/
      • Another small scripting language.
    • https://github.com/Knetic/govaluate
      • Another expression engine.
    opened by skx 4
  • Benchmarking ..

    Benchmarking ..

    There is an expression engine comparison repository which allows benchmarking different projects.

    I added this to benchmark evalfilter:

    
    package main
    
    import (
    	"testing"
    
    	"github.com/skx/evalfilter"
    )
    
    func Benchmark_evalfilter(b *testing.B) {
    	params := createParams()
    
    	eval := evalfilter.New(example)
    
    	var ret bool
    	var err error
    	b.ResetTimer()
    	for n := 0; n < b.N; n++ {
    		ret, err = eval.Run(params)
    	}
    	b.StopTimer()
    
    	if err != nil {
    		b.Fatal(err)
    	}
    	if !ret {
    		b.Fail()
    	}
    }
    

    The results were terrible! This project wasn't the slowest, but damn near!

    $ go test -bench=. -benchtime=5s
    Benchmark_bexpr-4              	 8790668	       686 ns/op
    Benchmark_celgo-4              	18292764	       333 ns/op
    Benchmark_celgo_startswith-4   	13063237	       467 ns/op
    Benchmark_evalfilter-4         	 1872409	      3209 ns/op
    Benchmark_expr-4               	32405442	       197 ns/op
    Benchmark_expr_startswith-4    	16776326	       348 ns/op
    Benchmark_goja-4               	14103356	       359 ns/op
    Benchmark_govaluate-4          	16351401	       375 ns/op
    Benchmark_gval-4               	  653475	      9432 ns/op
    Benchmark_otto-4               	 6175132	       985 ns/op
    Benchmark_starlark-4           	 1000000	      5960 ns/op
    

    I don't expect significant improvements without a major overhaul, but noting here for minor tweaks & documentation-purposes.

    documentation enhancement 
    opened by skx 4
  • Second attempt at nested fields

    Second attempt at nested fields

    This is a work in-progress which moves towards a simpler solution for nested field access.

    The realization that we need to only handle the case of maps, and allow parsing them to our internal hash-objects.

    If we have hash-objects parsed correctly then we can add the "." operator as a synonym, so these two lines of code are identical:

       foo["bar"]
       foo.bar
    

    The initial commit makes that change, and followups will handle the parsing.

    This replaces #161, and will close #159.

    opened by skx 3
  • 159 nested access

    159 nested access

    This pull-request features a major rewrite of our reflection code, with the intention that we can access nested fields.

    For example given the following input.json:

    {
        "name": {
            "forename": "John",
            "surname": {
                "value": "Smith"
            }
        },
        "age":30
    }
    

    And the following script:

    
    print( "The age is ", age, "\n" );
    print( "The FORENAME is ", name.forename, "\n" );
    print( "The SURENAME is ", name.surname.value, "\n" );
    
    return false;
    

    We can get the expected output:

    go build . ; ./evalfilter run -json ../../_examples/scripts/on-call.json  ../../_examples/scripts/on-call.script 
    The age is 30
    The FORENAME is John
    The SURENAME is Smith
    Script gave result type:BOOLEAN value:false - which is 'false'.
    

    This has only been lightly tested so far, but the existing test-cases continue to work so that's a reassuring sign.

    The only caveat here is that we internally use JSON encoding and then decoding. This causes two problems:

    • Slower than reflection.
    • Means private fields (i.e. lower-case member names) are ignored.

    Whether those are problems will remain to be seen.

    Closes #159.

    opened by skx 3
  • Support hashes ..

    Support hashes ..

    Hashes are an obvious data-structure to add, and the changes won't be too hard to add:

    • Hashes can be keyed by "string" or "number".
    • Hashes can be declared literally.
    • Hashes might be returned by some built-ins.

    Changes to be made, in brief:

    • [x] The parser needs to recognize a hash literal, at least.
    • [x] We need a Hash-object.
    • [x] The run-time needs to be updated to handle hash-objects. e.g. foreach.
    • [x] The run-time built-ins such as len, need update.
    • [x] We need a new "keys" built-in - or defer to foreach.
    • [x] The VM needs an OpHash to initialize a hash, at least.

    Test-case coverage should be high. We're in good shape there generally.

    opened by skx 3
  • We need to handle scoped variables

    We need to handle scoped variables

    This will be more important if we allow users to define functions, but we still have an issue with the way we declare loop-variables when iterating over arrays/strings.

    Consider this code:

    
    name = "Steve";
    print( "name starts as :", name, "\n");
    print( "index starts as:", index, "\n");
    
    foreach index,name in [ "Bob", "Chris" ] {
      printf( "%d -> %s\n", index, name );
    }
    
    //
    // BUG #1:
    //   name here is "Chris", not "Steve".
    //
    // BUG #2 / Leak:
    //   index here is 1
    //
    print( "Name is now: ", name, "\n" );
    print( "index is now :", index, "\n");
    
    return 0;
    

    Leaking the index variable is annoying, but probably safe.

    Changing the contents of the name variable is definitely an outright bug.

    bug language vm 
    opened by skx 3
  • We should support a `range` operation.

    We should support a `range` operation.

    Given a structure:

     type Message struct {
           Labels []string
     }
    

    We can run a script which iterates over the items:

       print("\tThe message has ", len(Labels), " labels.\n");
       i = 0;
       while( i < len(Labels) ) {
         print("\tLabel ", i+1 , " is " , Labels[i], "\n");
         i = i + 1;
       }
    

    But that's gross.

    We should support a range operator:

     foreach index, entry  Labels  {    
            print("Label ", i, " is ", entry );
     }
    

    Syntax is somewhat open, but the important thing is that we get access to the index, the entry, and users don't notice and complain about the lack of i++ support ;)

    opened by skx 3
  • 77 arrays

    77 arrays

    When this pull-request is complete we'll have support for arrays, which will close #77:

    • Support the use of reflection to read array-values from objects/maps
    • Support the use of arrays inside user-scripts.

    Current state is that we can lex/parse some array-specific items, but using them will crash as the AST is not compiled. Nor is the VM ready for them. Nor do the built-in functions support them. (e.g. len needs updating.)

    opened by skx 3
Releases(v2.1.19)
  • v2.1.19(May 25, 2022)

    This release adds two new primitives to the core:

    • join
      • This allows converting an array to a string, using a specified deliminator.
    • replace
      • This allows replacing the contents of a string with a regular-expression based search and a literal replacement value.

    Both these primitives have full test-coverage, and seem useful. Beyond the addition of these primitives minor changes were made to resolve warnings/failures detected by an updated release of the golangci-lint tool.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.18(Mar 31, 2022)

    This release mostly features only internal cleanups:

    • We've changed to using the fuzz-testing which is available in the golang 1.18 release, rather than using the external go-fuzz utility.
      • This was implemented in #178.
    • The linter we test our pull-requests with was updated to use golangci-lint, rather than the previous selection of tools.
      • This was implemented in #179

    There were two new featuresadded:

    • The implementation of a JSON interface to our objects, for those who embed our library
      • Reported in #173, and implemented in #174.
    • Support for chained/nested if and else if statements.
      • Reported in #171, and implemented in #172.
    Source code(tar.gz)
    Source code(zip)
  • v2.1.17(May 15, 2021)

    v2.1.17

    This release fixes a panic when reflection was used against a JSON object which contained a null value for a key. This was reported in #165, and resolved in #170.

    After resolving this panic I was reminded we have the setup for running fuzz-testing so I ran the fuzzer for several hours and resolved the few parser issues it caught. (These new issues were all related to the recently added support for hash-access via the . operator.)

    Although fuzzing is always somewhat random (by design!) I've been running it for several hours without any additional problems reported which is a reassuring thing:

    2021/05/15 12:20:19 workers: 1, corpus: 1885 (1m49s ago), crashers: 0, restarts: 1/9999, execs: 60544462 (4680/sec), cover: 1898, uptime: 3h35m
    2021/05/15 12:20:22 workers: 1, corpus: 1885 (1m52s ago), crashers: 0, restarts: 1/10000, execs: 60560974 (4680/sec), cover: 1898, uptime: 3h35m
    2021/05/15 12:20:25 workers: 1, corpus: 1885 (1m55s ago), crashers: 0, restarts: 1/9999, execs: 60577329 (4681/sec), cover: 1898, uptime: 3h35m
    
    Source code(tar.gz)
    Source code(zip)
  • v2.1.16(May 14, 2021)

    v2.1.16

    This release concentrates on stability and error-recovery:

    • The same instance of the evalfilter object can now be used across goroutines
      • Reported in #166, implemented in #169.
    • Any code, be it internal to the evaluator, or external in your built-in functions, which calls panic will now have that reported.
      • This was reported in #167, and resolved in #168.

    These changes, combined, should improve our robustness. This release also features a new built-in function panic which can be used to test the recovery, or terminate your scripts with a specific error-value.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.15(May 9, 2021)

    v2.1.15

    This release contains a small number of changes, as well as the usual internal cleanups and reorganizations.

    The following new primitives were added, and documented:

    • between
    • max
    • min

    We can now access nested hash-values, which is particularly useful when dealing with JSON objects. This can be achieved either via foo.bar.baz, or via the long-form which was previously supported (foo["bar"]["baz"]).

    Finally it is now possible to use the underscore (_) character in identifiers, which is necessary if you're working upon fields with names containing that character.

    Our test-coverage remains high (100% for the newly added features), and our reflection support was unified to ensure that the different kinds of input (struct vs. object) are both processed in the same manner. Some of our longer tests were broken down into smaller functions, to remove complexity but there were no real functional changes.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.14(Sep 30, 2020)

    v2.1.14

    This release features several new features and improvements, as well as the usual collection of bugfixes, and improvements to our internal codebase.

    Once more testing has been a big focus, we now have ~99% test-coverage of our virtual-machine, and significantly improved coverage of other areas in our package.

    New features include:

    • Support for hash-objects, in addition to the existing datatypes we support.
      • Hashes are defined as you would expect:
        • a = { "Name": "Steve", "Alive": true }
      • Hashes can be indexed, and iterated over
        • printf("%s\n", a["Name"]);
        • foreach key, value in a { printf("Key %s Value %s\n", key, value ); }
      • Example script available at _examples/scripts/hashes.script
    • Regular expressions were promoted to first-class objects.
      • Which means when dumping bytecode they are identified as regular-expressions rather than strings.
      • There are no actual changes to functionality though.
    • It is no longer a fatal error if a script doesn't end with a return statement.
      • The caller will receive &object.Void{} in that case.
      • If you're running as a filter you can decide what you want that to mean.
    • Errors from the parser will now contain more accurate line/column numbers.
      • Several new parser-errors will be identified more cleanly.
        • Only the initial error will be reported by default, because later errors are often bogus.
    • We gained support for case/switch statements.
      • These are similar to C-like switch expressions, but we use blocks to avoid any potential bugs with fall-through behaviour.
      • Example script available at _examples/scripts/switch.script
    Source code(tar.gz)
    Source code(zip)
  • v2.1.13(Sep 1, 2020)

    v2.1.13

    The major new feature in this release is the ability to create functions inside the scripts that the engine executes. For example it is now possible to write:

       // Sum the values in the provided array
       function sum(array) {
          local total;
          total = 0;
    
          foreach val in array {
             total += val;
          }
    
          return( total );
       }
    
       printf("Sum of 1-100 is %d\n", sum(1..100));
       return sum(1..100) == 5050;
    

    In this example you can see several new things demonstrated:

    • The definition of a new function, complete with a named argument, via the function .. block.
      • Note that functions don't need to be defined before they're used.
    • The use of the local keyword. This binds the variable to the local scope.
      • Without that you'd have a new global variable named total.
    • The use of the new mutator operation +=.

    The addition of functions required many small changes throughout the codebase, and as a result of that our test-coverage was improved significantly to give confidence nothing was broken in the process. (There is still room for additional test-coverage, and that will be worked upon going forward.)

    As minor features mutator-operations were added (+=, -=, *= and /=), as demonstrated above, and the keyword for was added as a synonym for while.

    A bugfix was made to the way that evalfilter-arrays are exported to Golang values, bounds-checking was added to the runtime virtual machine to guard against panics when executing untrusted bytecode. (Executing user-provided scripts is safe, as a result of the sanity-checks and error-detection in the lexer, parser, and evaluator. It is never expected that you'll process untrusted bytecode - but adding sanity-checks is still a useful thing to do.)

    Finally the new built-in function getenv was added, allowing scripts to read the contents of environmental variables.

    As a consequence of supporting user-defined functions the output of the bytecode subcommand in the standalone evalfilter binary was altered - The standalone executable also gained support for integrated TAB-completion (for bash).

    Source code(tar.gz)
    Source code(zip)
  • v2.1.12(Feb 29, 2020)

    v2.1.12

    This release makes some minor changes and improvements to our library, but nothing hugely significant:

    • We support the use of a context.Context to implement timeouts when executing scripts.
      • Reported in #127, implemented in #128.
    • The handling of numbers as conditionals was improved; negative numbers are not "true".
      • Reported in #125, implemented in #126.
    • Handling of variable-scoping was improved for our foreach iteration support.
      • Reported in #123, implemented in #124.

    Finally we've included a simple webassembly demo, which involves compiling our library to WASM and executing it in a browser. Find it beneath _examples/wasm.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.11(Feb 1, 2020)

    v2.1.11

    This release contains a number of changes to the scripting-language available to consumers of the library, largely as a result of my use of the evalfilter-library in a simple application for scripting the manipulation of Google Gmail labels.

    The language as a whole benefits from increasing usage, as it points out shortcomings that might otherwise not be apparent. In my case this largely revolved around the pain of iterating over arrays manually with a for-loop.

    A brief summary of changes in this release:

    • Implement the ability to iterate over arrays via the new foreach primitive.
      • Reported in #103, implemented in #105.
    • Allow creating arrays of integers via 1..10
      • Reported in #107, implemented in #108
    • Allow external functions added to the scripting environment to return "void".
      • This means that no return value will be available to the caller.
      • This fixes #86 and #106 - and was implemented in #109.
    • Implemented some new built-in functions:
      • sort() - reported in #104, implemented in #110.
      • reverse() - reported in #104, implemented in #110.
      • split() - reported in #111, implemented in #122.
    • Add support for the postfix ++ and -- operations.
      • Reported in #114, implemented in #115.
    • Allow iterating over the characters in a string, just like array iteration.
      • Reported in #113, implemented in #117.
    • Fixed bug: The optimizer could cause infinite loops when scripts didn't finish with a return statement
      • Reported in #119, fixed in #120.
    • Fixed bug: String-indexing was broken for multibyte characters
      • Reported in #118, fixed in #121
    • Fixed bug: Our parser was updated to avoid crashing on bogus input.
      • These were discovered as a result of fuzz-testing
        • Sample input which caused crashes included 0+foreach+0(), and !foreach%0().
    Source code(tar.gz)
    Source code(zip)
  • v2.1.10(Jan 6, 2020)

    v2.1.10

    Once again this release features on minor improvements to the code structure, layout, and internal organization.

    There have been new features in the release though, most notably:

    • The introduction of the ternary statement #99:
      • e.g. "field = Subject ? Subject : Title"
    • The reorganisation of the examples, beneath _examples/ handled in #102.
      • This now contains a couple of example scripts, as well as the examples showing how the evaluation-engine / scripting language can be embedded in your host application.
    • The regular expression support was improved in #98
      • It is now possible to escape characters via \, and only valid flags are accepted
      • (We use /i for ignoring-case, and /m for multi-line matches.)
    • Our opcode implementation was improved a little, via #100
    • We gained two new built-in functions sprintf and printf, via #101.
    Source code(tar.gz)
    Source code(zip)
  • v2.1.9(Jan 1, 2020)

    v2.1.9

    This release concentrated upon making more internal cleanups:

    • Shuffling the implementation of our code into different files/places
    • Improving test-coverage, rewording comments.

    The documentation has been overhauled:

    The library itself gained a new top-level method Execute, which allows the actual return value to be returned from your user-script - rather than just the pass/fail binary result which the Run method implements.

    The optimizer was improved to remove more code, in the event that conditionals were provably false, and significant cleanup was made in the implementation of the optimizer to avoid repetition.

    These cleanups and tweaks were largely as a result of looking at "the competition", because it seems crazy to avoid looking at similar and related projects. I tracked some brief notes about other solutions in #90.

    Finally we've added the built-in functions now() and time() which are useful when working with time-based fields.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.8(Dec 28, 2019)

    v2.1.8

    This release adds a new -debug flag to the evalfilter run .. subcommand, to allow tracing bytecode operations as they happen - including a display of the stack-state as every instruction is executing. This was reported in #86, and implemented in #87.

    We also gained the ability to work with time.Time values, in the struct/map objects we're executed against, as reported in #88 and implemented in #89

    Finally we made a couple of removals of dead/historic code which is not called/visible to users.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.7(Dec 27, 2019)

    v2.1.7

    This release adds the new in keyword, which allows you to test whether an array contains a specified value.

    You could previously have done this like so:

    staff = [ "Alice", "Bob", "Chris", "David", "Edward", "Fiona", "Georgette" ];
    i = 0;
    while( i < len(staff) ) {
        if ( Name == staff[i] ) {
            print("Message from staff-member " , Name, " ignoring\n" );
            return true;
        }
        i = i + 1
    }
    return false;
    

    But it is much simpler to use the new method:

    staff = [ "Alice", "Bob", "Chris", "David", "Edward", "Fiona", "Georgette" ];
    
    return( Name in Staff );
    

    This was reported in #84, and implemented in #85.

    I ran fuzz-testing against the compiler, and evaluator, for a couple of days which resulted in a bunch of small commits to resolve issues which had been identified. These are visible in #79, perhaps the most significant was that I'd failed to prohibit/detect division by zero.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.6(Dec 21, 2019)

    v2.1.6

    Shortly after the previous release I noticed a problem with the optimizer, generating broken code in some situations:

    • If a jump operation pointed to an opcode which had been optimized away
      • i.e. Replaced by an OpNop opcode
    • Then the target of the jump was not updated correctly.
      • Instead it defaulted to 0x0000.
      • Causing an infinite loop

    This was reported and fixed via #82.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.5(Dec 21, 2019)

    v2.1.5

    This release brings several new fixes to the codebase, as well as new features:

    • The built-in functions have moved to the environment package #71
      • This is a more natural place for them to live.
    • The optimization code has been simplified #72
      • And improved to avoid making mistakes, #75
    • Arrays are now supported as a native type #77
      • Including in the examination of user-supplied maps/interfaces/structures
    • The language now has support for while-loops as well as if-statements #80

    The use of arrays and while-loops is demonstrated in the updated "on-call example":

    Source code(tar.gz)
    Source code(zip)
  • v2.1.4(Nov 19, 2019)

    v2.1.4

    This release improves the speed of the virtual machine which implements the bytecode which the user-script is compiled into. It does this by introducing a new step in the operation of our interpreter:

    • Parse the user-supplied script, and generate bytecode instructions.
    • Optimize the generated bytecode.
    • Run the bytecode.
      • Optionally the same instance may be Run() multiple times against new objects/maps.

    The optimization was described in this blog post and may be examined via the cmd/evalfilter binary.

    The optimizer is enabled by default so you can see the result via the bytecode subcommand:

      $ evalfilter bytecode  input.txt
    

    If you wish you can disable the optimizer:

      $ evalfilter bytecode  -no-optimizer input.txt
    

    Or watch the distinct steps being executed:

      $ evalfilter bytecode  -show-optimizer input.txt
    
    Source code(tar.gz)
    Source code(zip)
  • v2.1.3(Nov 8, 2019)

    v2.1.3

    This release improves our internal codebase, fixing several long-standing linter warnings which have now been resolves. (For example renaming package-constants from being CamelCase.)

    We've updated the bytecode generation to optimize the case where an if expression had no else clause, as noted in #58, added regular expressions as a first-class object (almost!) in #56. Other than that the public-facing go-doc documentation was enhanced in #55 and removed references to outdated features/code in our README.md file.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.2(Nov 5, 2019)

  • v2.1.1(Nov 5, 2019)

    v2.1.1

    This release updated our module to explicitly give ourselves a /v2 version.

    NOTE: This wasn't sufficient to allow pinning to the v2x.x release by users, so v2.1.2 was made shortly afterward to update our usage of internal paths.

    Source code(tar.gz)
    Source code(zip)
  • v2.1.0(Nov 5, 2019)

    v2.1.0

    This release focuses upon improving our correctness, and making our engine faster.

    • We eat the cost of running reflection only once each time we run in #44
      • By parsing all structure/object/map fields the first time one is accessed.
    • We added go vet to our testing-pipeline in #49.
      • Which lead to some minor fixes.
    • Profiling also lead to the use of a static cache for all regular-expression objects in #51
      • Along with the removal of some dead-code.
    • We added test 100% coverage of our built-in functions in #52
    • We documented our bytecode, and we improved our go-doc documention.
    Source code(tar.gz)
    Source code(zip)
  • v2.0.0(Nov 3, 2019)

    v2.0.0

    This release features a breaking API change, which has forced us to bump the major-version.

    This release features a major rework or our internals:

    • Previously we parsed the source given to us into an AST.
    • We then walked the AST, evaluating as we went.

    Now we do something different:

    • We parse the source given to us into an AST.
    • We then walk the AST, to generate a series of bytecode operations.
    • Finally when the script is run we execute the bytecode operations in a simple virtual machine.

    This new approach has some additional overhead when we're performing our setup, but if the script is reused even one time then that overhead pays off; because running the virtual machine is super-fast compared to walking the AST tree.

    All the tests and examples have been updated, but to update you just need to call the Prepare() method, after New() and before Run().

    Source code(tar.gz)
    Source code(zip)
  • v1.5.2(Nov 2, 2019)

    v1.5.2

    This release is aimed at making our benchmark results a little faster, as noted in #32, by short-circuiting if we can the || and && operators.

    In short we can skip evaluating b in the following case if a is false:

    a && b
    

    Similarly if a is true we can skip b:

    a || b
    
    Source code(tar.gz)
    Source code(zip)
  • v1.5.1(Nov 1, 2019)

  • v1.5.0(Nov 1, 2019)

    v1.5.0

    This release was issued in error, before merging a pull-request rather than after. Rather than reuse the release ID, or delete it, I'll leave it in-place.

    Source code(tar.gz)
    Source code(zip)
  • v1.4.0(Oct 13, 2019)

    v1.4.0

    This release updates our utility command, cmd/evalfilter such that it can now dump the AST generated by parsing a program. This is useful when looking for syntax-errors, or investigating problems identified by fuzz-testing.

    Otherwise I've added three new mathematical operators:

    • % - Modulus
    • - Square root
    • ** - Power

    These work as you'd expect if ( √9 == 3 ) { print("Mathematics is fun\n"); }

    The final update in this release is the addition of a GetVariable method which mirrors the SetVariable already present. Using this new function you can run a script and retrieve any variables which the script has created/updated. This can be useful for passing state in both directions between a host-application and a user-written script.

    Source code(tar.gz)
    Source code(zip)
  • v1.3.0(Sep 29, 2019)

    v1.3.0

    This release is a minor update to v1.2.0, making minor changes to the documentation and adding three new built-in functions which scripts may call:

    • lower(field|string)
      • Return a lower-case version of the given string, or object/structure value.
    • type(field|object)
      • Return the type of a given field from the given object-structure, or the given object.
    • upper(field|string)
      • Return a lower-case version of the given string, or object/structure value.

    These are useful for user-scripts.

    Source code(tar.gz)
    Source code(zip)
  • v1.2.0(Sep 27, 2019)

    v1.2.0

    This release adds the new built-in function match which allows a regular expression to be tested against a string, or field.

    You can make a regular expression case-insensitive via the (?i) prefix:

    if ( match( Name, "(?i)^steve$" ) ) {
        print( "Steve is present\n");
    } else {
       print( "Steve is not present.\n");
    }
    

    Note that the string/field passed to this function will be split by any present newline-characters, and each piece matched separately. This allows "^" and "$" to work in a natural fashion.

    Source code(tar.gz)
    Source code(zip)
  • v1.1.0(Sep 25, 2019)

    v1.1.0

    This is a minor update which improves the use of functions in conditional-tests. This means that the following works as you would expect:

    if ( Function() ) { print("OK\n"); }
    

    Where Function() is a user-implemented function, added by a host application. Specifically the test passes if the function returns a non-empty string, a non-zero number, or a boolean true-result.

    There have been several fixes to the parser to avoid infinite loops when parsing malformed input:

    • Unterminated string-literals are handled correctly.
    • return statements without a trailing semi-colon are reported correctly.

    These issues were identified via the use of fuzz-testing, and using a fuzz-tester against the code is now documented in FUZZING.md.

    Source code(tar.gz)
    Source code(zip)
  • release-1.0.0(Sep 23, 2019)

    release-1.0.0

    This is the first stable release, having recently had most of the internals replaced and overhauled I'm happy to commit to keepign the user-facing API stable:

    • This mostly means the evalfilter package.
    • But also includes the object-package which is used in the implementation of user-added golang functions.

    The code is stable under load, and has been running in production reacting to thousands of incoming Slack messages, so while there might be issues the system as a whole does what I need it to do:

    • Dynamically choose which events are worthy of special-action.
    • Via the use of simple human-readable & editable scripts, rather than golang code.
    Source code(tar.gz)
    Source code(zip)
Owner
Steve Kemp
Steve Kemp
Expr – a tiny stack-based virtual machine written in Go

Expr – a tiny stack-based virtual machine written in Go The executor is designed to interpret a simple expression language and it's useful in delegati

Anthony Regeda 26 Nov 11, 2022
A simple virtual machine - compiler & interpreter - written in golang

go.vm Installation Build without Go Modules (Go before 1.11) Build with Go Modules (Go 1.11 or higher) Usage Opcodes Notes The compiler The interprete

Steve Kemp 262 Dec 17, 2022
Forth virtual machine in Go

forego - A Forth implementation in Go ===================================== Why? ---- For ego. This is me learning the language. Both of them. So

Vadim Vygonets 26 Sep 9, 2022
A customisable virtual machine written in Go

== About GoLightly == GoLightly is a lightweight virtual machine library implemented in Go, designed for flexibility and reuse. Traditionally popular

Eleanor McHugh 217 Nov 16, 2022
Weave Ignite is an open source Virtual Machine (VM) manager with a container UX and built-in GitOps management.

Weave Ignite is an open source Virtual Machine (VM) manager with a container UX and built-in GitOps management.

Temur Yunusov 0 Nov 16, 2021
Golem is a general purpose, interpreted scripting language.

The Golem Programming Language Golem is a general purpose, interpreted scripting language, that brings together ideas from many other languages, inclu

Mike Jarmy 1 Sep 28, 2022
Scripting language for Go.

Minima Minima is an experimental interpreter written in Go (the language is called the same). We needed a way to massage our JSON data with a scriptin

Janos Dobronszki 38 Feb 11, 2022
Create virtual machines and run Linux-based operating systems in Go using Apple Virtualization.framework.

vz - Go binding with Apple Virtualization.framework vz provides the power of the Apple Virtualization.framework in Go.

Kei Kamikawa 332 Jan 9, 2023
This is a Virtual Operating System made by using GOLANG and FYNE.

Virtual-Operating-System This is a Virtual Operating System made by using GOLANG and FYNE. Hello! All In this project I have made a virtual Operating

SURBHI SINHA 1 Nov 1, 2021
Virtual Operating System Using Golang

Virtual Operating System Virtual Operating System Using Golang And Fyne Installation 1.Install Go 2.Install Gcc 3.Install Fyne Using This Command:- g

Ranjit Kumar Sahoo 2 Jun 5, 2022
OperatingSys-GO - A Virtual Operating System made by using GOLANG and FYNE

Operating-System This is a Virtual Operating System made by using GOLANG and FYN

null 0 Jan 2, 2022
Lima: Linux virtual machines (on macOS, in most cases)

Linux virtual machines, on macOS (aka "Linux-on-Mac", "macOS subsystem for Linux", "containerd for Mac", unofficially)

Linux Machines 10.3k Jan 1, 2023
A dialect of Lisp extended to support concurrent programming, written in Go.

LispEx A dialect of Lisp extended to support concurrent programming. Overview LispEx is another Lisp Interpreter implemented with Go. The syntax, sema

null 181 Nov 22, 2022
A shell parser, formatter, and interpreter with bash support; includes shfmt

sh A shell parser, formatter, and interpreter. Supports POSIX Shell, Bash, and mksh. Requires Go 1.14 or later. Quick start To parse shell scripts, in

Daniel Martí 5.4k Jan 8, 2023
Monkey programming language project from 'Writing An Interpreter In Go'and 'Writing A Compiler In Go' Books

Monkey Monkey programming language ?? project from "Writing An Interpreter In Go

Amr Hesham 1 Dec 16, 2021
Process manager for Procfile-based applications

Hivemind Hivemind is a process manager for Procfile-based applications. At the moment, it supports Linux, FreeBSD, and macOS. Procfile is a simple for

Sergey Alexandrovich 840 Dec 25, 2022
Process manager for Procfile-based applications and tmux

Overmind Overmind is a process manager for Procfile-based applications and tmux. With Overmind, you can easily run several processes from your Procfil

Sergey Alexandrovich 2.1k Jan 4, 2023
Manage Procfile-based applications

Foreman Manage Procfile-based applications Installation $ gem install foreman Ruby users should take care not to install foreman in their project's G

David Dollar 5.8k Dec 30, 2022
An interpreter written in go for a brainfuck-based language called €*

eurostar-go-interpreter This is an interpreter written in go for a brainfuck-bas

null 9 Sep 14, 2022