diff --git a/bench_test.go b/bench_test.go index de159bed9..c9f2b7182 100644 --- a/bench_test.go +++ b/bench_test.go @@ -31,6 +31,26 @@ func Benchmark_expr(b *testing.B) { require.True(b, out.(bool)) } +func Benchmark_expr_eval(b *testing.B) { + params := make(map[string]any) + params["Origin"] = "MOW" + params["Country"] = "RU" + params["Adults"] = 1 + params["Value"] = 100 + + var out any + var err error + + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = expr.Eval(`(Origin == "MOW" || Country == "RU") && (Value >= 100 || Adults == 1)`, params) + } + b.StopTimer() + + require.NoError(b, err) + require.True(b, out.(bool)) +} + func Benchmark_expr_reuseVm(b *testing.B) { params := make(map[string]any) params["Origin"] = "MOW" diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 71f775a49..6ca1e8fdd 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -246,9 +246,15 @@ func TestBuiltin_errors(t *testing.T) { } for _, test := range errorTests { t.Run(test.input, func(t *testing.T) { - _, err := expr.Eval(test.input, nil) - assert.Error(t, err) - assert.Contains(t, err.Error(), test.err) + program, err := expr.Compile(test.input) + if err != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.err) + } else { + _, err = expr.Run(program, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), test.err) + } }) } } diff --git a/compiler/compiler.go b/compiler/compiler.go index 90e54e54b..595355d28 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -776,12 +776,16 @@ func (c *compiler) CallNode(node *ast.CallNode) { } c.compile(node.Callee) - isMethod, _, _ := checker.MethodIndex(c.config.Env, node.Callee) - if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok { - c.emit(OpCallTyped, index) - return - } else if checker.IsFastFunc(node.Callee.Type(), isMethod) { - c.emit(OpCallFast, len(node.Arguments)) + if c.config != nil { + isMethod, _, _ := checker.MethodIndex(c.config.Env, node.Callee) + if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok { + c.emit(OpCallTyped, index) + return + } else if checker.IsFastFunc(node.Callee.Type(), isMethod) { + c.emit(OpCallFast, len(node.Arguments)) + } else { + c.emit(OpCall, len(node.Arguments)) + } } else { c.emit(OpCall, len(node.Arguments)) } diff --git a/expr.go b/expr.go index 5e60791e1..33b7cf354 100644 --- a/expr.go +++ b/expr.go @@ -13,6 +13,7 @@ import ( "github.com/expr-lang/expr/conf" "github.com/expr-lang/expr/file" "github.com/expr-lang/expr/optimizer" + "github.com/expr-lang/expr/parser" "github.com/expr-lang/expr/patcher" "github.com/expr-lang/expr/vm" ) @@ -240,7 +241,12 @@ func Eval(input string, env any) (any, error) { return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env") } - program, err := Compile(input) + tree, err := parser.Parse(input) + if err != nil { + return nil, err + } + + program, err := compiler.Compile(tree, nil) if err != nil { return nil, err } diff --git a/expr_test.go b/expr_test.go index 4ebe6d4de..4d3736a97 100644 --- a/expr_test.go +++ b/expr_test.go @@ -1684,7 +1684,7 @@ func TestEval_exposed_error(t *testing.T) { fileError, ok := err.(*file.Error) require.True(t, ok, "error should be of type *file.Error") - require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error()) + require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error()) require.Equal(t, 2, fileError.Column) require.Equal(t, 1, fileError.Line) } diff --git a/parser/lexer/lexer_test.go b/parser/lexer/lexer_test.go index f1cb3f28f..db02d2acf 100644 --- a/parser/lexer/lexer_test.go +++ b/parser/lexer/lexer_test.go @@ -311,13 +311,13 @@ func TestLex_location(t *testing.T) { tokens, err := Lex(source) require.NoError(t, err) require.Equal(t, []Token{ - {Location: file.Location{From: 0, To: 1}, Kind: "Number", Value: "1"}, - {Location: file.Location{From: 1, To: 3}, Kind: "Operator", Value: ".."}, - {Location: file.Location{From: 3, To: 4}, Kind: "Number", Value: "2"}, - {Location: file.Location{From: 5, To: 6}, Kind: "Number", Value: "3"}, - {Location: file.Location{From: 6, To: 8}, Kind: "Operator", Value: ".."}, - {Location: file.Location{From: 8, To: 9}, Kind: "Number", Value: "4"}, - {Location: file.Location{From: 8, To: 9}, Kind: "EOF", Value: ""}, + {Location: file.Location{From: 0, To: 1}, Kind: Number, Value: "1"}, + {Location: file.Location{From: 1, To: 3}, Kind: Operator, Value: ".."}, + {Location: file.Location{From: 3, To: 4}, Kind: Number, Value: "2"}, + {Location: file.Location{From: 5, To: 6}, Kind: Number, Value: "3"}, + {Location: file.Location{From: 6, To: 8}, Kind: Operator, Value: ".."}, + {Location: file.Location{From: 8, To: 9}, Kind: Number, Value: "4"}, + {Location: file.Location{From: 8, To: 9}, Kind: EOF, Value: ""}, }, tokens) } diff --git a/parser/parser.go b/parser/parser.go index 91c0fa8f8..0a463fed5 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -56,6 +56,13 @@ type parser struct { func (p *parser) checkNodeLimit() error { p.nodeCount++ + if p.config == nil { + if p.nodeCount > conf.DefaultMaxNodes { + p.error("compilation failed: expression exceeds maximum allowed nodes") + return nil + } + return nil + } if p.config.MaxNodes > 0 && p.nodeCount > p.config.MaxNodes { p.error("compilation failed: expression exceeds maximum allowed nodes") return nil @@ -91,9 +98,7 @@ type Tree struct { } func Parse(input string) (*Tree, error) { - return ParseWithConfig(input, &conf.Config{ - Disabled: map[string]bool{}, - }) + return ParseWithConfig(input, nil) } func ParseWithConfig(input string, config *conf.Config) (*Tree, error) { @@ -515,7 +520,10 @@ func (p *parser) toFloatNode(number float64) Node { func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) Node { var node Node - isOverridden := p.config.IsOverridden(token.Value) + isOverridden := false + if p.config != nil { + isOverridden = p.config.IsOverridden(token.Value) + } isOverridden = isOverridden && checkOverrides if b, ok := predicates[token.Value]; ok && !isOverridden { @@ -562,7 +570,7 @@ func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) N if node == nil { return nil } - } else if _, ok := builtin.Index[token.Value]; ok && !p.config.Disabled[token.Value] && !isOverridden { + } else if _, ok := builtin.Index[token.Value]; ok && (p.config == nil || !p.config.Disabled[token.Value]) && !isOverridden { node = p.createNode(&BuiltinNode{ Name: token.Value, Arguments: p.parseArguments(arguments), diff --git a/test/bench/bench_call_test.go b/test/bench/bench_call_test.go new file mode 100644 index 000000000..9c9c13226 --- /dev/null +++ b/test/bench/bench_call_test.go @@ -0,0 +1,63 @@ +package bench_test + +import ( + "testing" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/internal/testify/require" + "github.com/expr-lang/expr/vm" +) + +type Env struct { + Fn func() bool +} + +func BenchmarkCall_callTyped(b *testing.B) { + code := `Fn()` + + p, err := expr.Compile(code, expr.Env(Env{})) + require.NoError(b, err) + require.Equal(b, p.Bytecode[1], vm.OpCallTyped) + + env := Env{ + Fn: func() bool { + return true + }, + } + + var out any + + b.ResetTimer() + for n := 0; n < b.N; n++ { + program, _ := expr.Compile(code, expr.Env(env)) + out, err = vm.Run(program, env) + } + b.StopTimer() + + require.NoError(b, err) + require.True(b, out.(bool)) +} + +func BenchmarkCall_eval(b *testing.B) { + code := `Fn()` + + p, err := expr.Compile(code) + require.NoError(b, err) + require.Equal(b, p.Bytecode[1], vm.OpCall) + + env := Env{ + Fn: func() bool { + return true + }, + } + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = expr.Eval(code, env) + } + b.StopTimer() + + require.NoError(b, err) + require.True(b, out.(bool)) +}