Skip to content

Latest commit

 

History

History
226 lines (161 loc) · 5.62 KB

README.md

File metadata and controls

226 lines (161 loc) · 5.62 KB

evalfilter

This directory contains a standalone utility which can be used to experiment with the evalfilter package/library.

If you have this repository cloned locally you can install it via:

cd cmd/evalfilter
go install .

Otherwise you can fetch the source and install it via the standard golang approach:

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

Overview

The utility uses a number of subcommands for various purposes, you can see an overview by invoking it with no arguments:

$ evalfilter
Usage: evalfilter <flags> <subcommand> <subcommand args>


Subcommands:
	bytecode         Show the bytecode for a script.
	help             describe subcommands and their syntax
	lex              Show our lexer output.
	parse            Show our parser output.
	run              Run a script file, against a JSON object.

Bytecode Display

The bytecode sub-command allows you to see the instructions into which a script is compiled. This can be useful if you suspect there is a bug in the bytecode-generation, or if you're curious to see how a stack-based virtual machine might work.

If you're interested in the bytecode you should read the top-level BYTECODE.md file which contains more details.

Sample input:

// sample.in
if ( 1 + 2 * 3 == 7 ) { print( "OK\n" ); }
return true;

Sample usage:

$ evalfilter bytecode t.in
Bytecode:
0000	    OpConstant	   0	// push constant onto stack: "OK\n"
0003	    OpConstant	   1	// push constant onto stack: "print"
0006	        OpCall	   1	// call function with 1 arg(s)
0009	        OpTrue
0010	      OpReturn


Constant Pool:
0000 Type:STRING Value:"OK\n"
0001 Type:STRING Value:"print"

Here you'll notice that the generated bytecode is quite different from the input script. That is because the optimizer has worked its magic over a series of iterations.

Specifically the if condition was changed over a series of steps:

  1. if ( 1 + 2 * 3 == 7 ) { print( "OK\n" ); } return true;
  2. if ( 1 + 6 == 7 ) { print( "OK\n" ); } return true;
  3. if ( 7 == 7 ) { print( "OK\n" ); } return true;
  4. if ( true ) { print( "OK\n" ); } return true;
  5. print( "OK\n" ); return true;

If you add the -no-optimizer flag to the bytecode sub-command you can see the bytecode which was generated before the optimizer updated it:

$ evalfilter bytecode -no-optimizer sample.in
Bytecode:
0000	        OpPush	   1	// Push 1 to stack
0003	        OpPush	   2	// Push 2 to stack
0006	        OpPush	   3	// Push 3 to stack
0009	         OpMul
0010	         OpAdd
0011	        OpPush	   7	// Push 7 to stack
0014	       OpEqual
0015	 OpJumpIfFalse	  27
0018	    OpConstant	   0	// push constant onto stack: "OK\n"
0021	    OpConstant	   1	// push constant onto stack: "print"
0024	        OpCall	   1	// call function with 1 arg(s)
0027	        OpTrue
0028	      OpReturn


Constant Pool:
0000 Type:STRING Value:"OK\n"
0001 Type:STRING Value:"print"

Lexing Input

The lexer sub-command allows you to see how a given input-script would be lexed. Lexing is the process of splitting a source file into a series of tokens.

Most users won't care about this command, but it was helpful when updating the language to allow new operators.

Sample input:

// sample.in
if ( 1 + 2 * 3 == 7 ) { print( "OK\n" ); }
return true;

Sample usage:

$ evalfilter lex sample.in
{IF if}
{( (}
{INT 1}
{+ +}
{INT 2}
{* *}
{INT 3}
{== ==}
{INT 7}
{) )}
{{ {}
{IDENT print}
{( (}
{STRING OK
...

Parsing Input

The parse sub-command allows you to see how a given input-script would be parsed. Parsing is the process of turning the series of tokens produced by the lexer into an abstract-syntax-tree. (Once the AST exists our compiler generates our bytecode.)

Most users won't care about this command, but it was helpful when updating the language to allow new operators.

Sample input:

// sample.in
if ( 1 + 2 * 3 == 7 ) { print( "OK\n" ); }
return true;

Sample usage:

$ evalfilter parse sample.in
if((1 + (2 * 3)) == 7)
{
print("OK\n");

}
return true;

Running Scripts

The main reason for having the evalfilter command is to let users experiment with actually running scripts before they've embedded it into their own application(s).

The run-command allows you to run a script:

  • A script may be executed in a standalone fashion.
  • Or you may pass an object as input to the script, which better reflects how the library is designed to be used.
    • The object is passed in as a JSON file.

Sample input:

// sample.in
if ( 1 + 2 * 3 == 7 ) { print( "OK\n" ); }
return true;

Sample usage:

$ evalfilter run sample.in
OK
Script gave result type:BOOLEAN value:true - which is 'true'.

This example merely displayed the text OK, but you could also specify a more complex script and pass in an object to test it with:

Sample JSON:

$ cat sample.json
{"Forename": "Steve", "Surname": "Kemp", "Link": "https://steve.fi/" }

Now we'll use a more complex script:

// sample.in
print( "Person is ", Forename, " ", Surname, "\n" );

if ( Link ~= /^https/ ) { print( "Link uses SSL\n" ); }

return true;

And the output:

$ evalfilter run -json sample.json sample.in
Person is Steve Kemp
Link uses SSL
Script gave result type:BOOLEAN value:true - which is 'true'.

As with the bytecode sub-command you can disable the optimizer if you suspect you're seeing bogus output:

$ evalfilter run -json sample.json -no-optimizer sample.in

You can also see the opcodes being executed, as well as a view of the stack via the -debug flag:

$ evalfilter run -json sample.json -no-optimizer -debug sample.in