Make expr.Eval faster (#775)

* Make expr.Eval faster

* Add bench_call_test.go

* Make parser config optional
This commit is contained in:
Anton Medvedev
2025-03-24 14:48:41 +01:00
committed by GitHub
parent 85fa77e98b
commit 452d355706
8 changed files with 130 additions and 23 deletions
+20
View File
@@ -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"
+7 -1
View File
@@ -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)
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)
}
})
}
}
+4
View File
@@ -776,6 +776,7 @@ func (c *compiler) CallNode(node *ast.CallNode) {
}
c.compile(node.Callee)
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)
@@ -785,6 +786,9 @@ func (c *compiler) CallNode(node *ast.CallNode) {
} else {
c.emit(OpCall, len(node.Arguments))
}
} else {
c.emit(OpCall, len(node.Arguments))
}
}
func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
+7 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+7 -7
View File
@@ -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)
}
+13 -5
View File
@@ -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),
+63
View File
@@ -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))
}