Skip to content

Commit 6121e25

Browse files
authored
Make expr.Eval faster (#775)
* Make expr.Eval faster * Add bench_call_test.go * Make parser config optional
1 parent 3a0a1da commit 6121e25

File tree

8 files changed

+130
-23
lines changed

8 files changed

+130
-23
lines changed

bench_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ func Benchmark_expr(b *testing.B) {
3131
require.True(b, out.(bool))
3232
}
3333

34+
func Benchmark_expr_eval(b *testing.B) {
35+
params := make(map[string]any)
36+
params["Origin"] = "MOW"
37+
params["Country"] = "RU"
38+
params["Adults"] = 1
39+
params["Value"] = 100
40+
41+
var out any
42+
var err error
43+
44+
b.ResetTimer()
45+
for n := 0; n < b.N; n++ {
46+
out, err = expr.Eval(`(Origin == "MOW" || Country == "RU") && (Value >= 100 || Adults == 1)`, params)
47+
}
48+
b.StopTimer()
49+
50+
require.NoError(b, err)
51+
require.True(b, out.(bool))
52+
}
53+
3454
func Benchmark_expr_reuseVm(b *testing.B) {
3555
params := make(map[string]any)
3656
params["Origin"] = "MOW"

builtin/builtin_test.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,15 @@ func TestBuiltin_errors(t *testing.T) {
246246
}
247247
for _, test := range errorTests {
248248
t.Run(test.input, func(t *testing.T) {
249-
_, err := expr.Eval(test.input, nil)
250-
assert.Error(t, err)
251-
assert.Contains(t, err.Error(), test.err)
249+
program, err := expr.Compile(test.input)
250+
if err != nil {
251+
assert.Error(t, err)
252+
assert.Contains(t, err.Error(), test.err)
253+
} else {
254+
_, err = expr.Run(program, nil)
255+
assert.Error(t, err)
256+
assert.Contains(t, err.Error(), test.err)
257+
}
252258
})
253259
}
254260
}

compiler/compiler.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -776,12 +776,16 @@ func (c *compiler) CallNode(node *ast.CallNode) {
776776
}
777777
c.compile(node.Callee)
778778

779-
isMethod, _, _ := checker.MethodIndex(c.config.Env, node.Callee)
780-
if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok {
781-
c.emit(OpCallTyped, index)
782-
return
783-
} else if checker.IsFastFunc(node.Callee.Type(), isMethod) {
784-
c.emit(OpCallFast, len(node.Arguments))
779+
if c.config != nil {
780+
isMethod, _, _ := checker.MethodIndex(c.config.Env, node.Callee)
781+
if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok {
782+
c.emit(OpCallTyped, index)
783+
return
784+
} else if checker.IsFastFunc(node.Callee.Type(), isMethod) {
785+
c.emit(OpCallFast, len(node.Arguments))
786+
} else {
787+
c.emit(OpCall, len(node.Arguments))
788+
}
785789
} else {
786790
c.emit(OpCall, len(node.Arguments))
787791
}

expr.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/expr-lang/expr/conf"
1414
"github.com/expr-lang/expr/file"
1515
"github.com/expr-lang/expr/optimizer"
16+
"github.com/expr-lang/expr/parser"
1617
"github.com/expr-lang/expr/patcher"
1718
"github.com/expr-lang/expr/vm"
1819
)
@@ -240,7 +241,12 @@ func Eval(input string, env any) (any, error) {
240241
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
241242
}
242243

243-
program, err := Compile(input)
244+
tree, err := parser.Parse(input)
245+
if err != nil {
246+
return nil, err
247+
}
248+
249+
program, err := compiler.Compile(tree, nil)
244250
if err != nil {
245251
return nil, err
246252
}

expr_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1684,7 +1684,7 @@ func TestEval_exposed_error(t *testing.T) {
16841684

16851685
fileError, ok := err.(*file.Error)
16861686
require.True(t, ok, "error should be of type *file.Error")
1687-
require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
1687+
require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
16881688
require.Equal(t, 2, fileError.Column)
16891689
require.Equal(t, 1, fileError.Line)
16901690
}

parser/lexer/lexer_test.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,13 @@ func TestLex_location(t *testing.T) {
311311
tokens, err := Lex(source)
312312
require.NoError(t, err)
313313
require.Equal(t, []Token{
314-
{Location: file.Location{From: 0, To: 1}, Kind: "Number", Value: "1"},
315-
{Location: file.Location{From: 1, To: 3}, Kind: "Operator", Value: ".."},
316-
{Location: file.Location{From: 3, To: 4}, Kind: "Number", Value: "2"},
317-
{Location: file.Location{From: 5, To: 6}, Kind: "Number", Value: "3"},
318-
{Location: file.Location{From: 6, To: 8}, Kind: "Operator", Value: ".."},
319-
{Location: file.Location{From: 8, To: 9}, Kind: "Number", Value: "4"},
320-
{Location: file.Location{From: 8, To: 9}, Kind: "EOF", Value: ""},
314+
{Location: file.Location{From: 0, To: 1}, Kind: Number, Value: "1"},
315+
{Location: file.Location{From: 1, To: 3}, Kind: Operator, Value: ".."},
316+
{Location: file.Location{From: 3, To: 4}, Kind: Number, Value: "2"},
317+
{Location: file.Location{From: 5, To: 6}, Kind: Number, Value: "3"},
318+
{Location: file.Location{From: 6, To: 8}, Kind: Operator, Value: ".."},
319+
{Location: file.Location{From: 8, To: 9}, Kind: Number, Value: "4"},
320+
{Location: file.Location{From: 8, To: 9}, Kind: EOF, Value: ""},
321321
}, tokens)
322322
}
323323

parser/parser.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ type parser struct {
5656

5757
func (p *parser) checkNodeLimit() error {
5858
p.nodeCount++
59+
if p.config == nil {
60+
if p.nodeCount > conf.DefaultMaxNodes {
61+
p.error("compilation failed: expression exceeds maximum allowed nodes")
62+
return nil
63+
}
64+
return nil
65+
}
5966
if p.config.MaxNodes > 0 && p.nodeCount > p.config.MaxNodes {
6067
p.error("compilation failed: expression exceeds maximum allowed nodes")
6168
return nil
@@ -91,9 +98,7 @@ type Tree struct {
9198
}
9299

93100
func Parse(input string) (*Tree, error) {
94-
return ParseWithConfig(input, &conf.Config{
95-
Disabled: map[string]bool{},
96-
})
101+
return ParseWithConfig(input, nil)
97102
}
98103

99104
func ParseWithConfig(input string, config *conf.Config) (*Tree, error) {
@@ -515,7 +520,10 @@ func (p *parser) toFloatNode(number float64) Node {
515520
func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) Node {
516521
var node Node
517522

518-
isOverridden := p.config.IsOverridden(token.Value)
523+
isOverridden := false
524+
if p.config != nil {
525+
isOverridden = p.config.IsOverridden(token.Value)
526+
}
519527
isOverridden = isOverridden && checkOverrides
520528

521529
if b, ok := predicates[token.Value]; ok && !isOverridden {
@@ -562,7 +570,7 @@ func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) N
562570
if node == nil {
563571
return nil
564572
}
565-
} else if _, ok := builtin.Index[token.Value]; ok && !p.config.Disabled[token.Value] && !isOverridden {
573+
} else if _, ok := builtin.Index[token.Value]; ok && (p.config == nil || !p.config.Disabled[token.Value]) && !isOverridden {
566574
node = p.createNode(&BuiltinNode{
567575
Name: token.Value,
568576
Arguments: p.parseArguments(arguments),

test/bench/bench_call_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package bench_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/internal/testify/require"
8+
"github.com/expr-lang/expr/vm"
9+
)
10+
11+
type Env struct {
12+
Fn func() bool
13+
}
14+
15+
func BenchmarkCall_callTyped(b *testing.B) {
16+
code := `Fn()`
17+
18+
p, err := expr.Compile(code, expr.Env(Env{}))
19+
require.NoError(b, err)
20+
require.Equal(b, p.Bytecode[1], vm.OpCallTyped)
21+
22+
env := Env{
23+
Fn: func() bool {
24+
return true
25+
},
26+
}
27+
28+
var out any
29+
30+
b.ResetTimer()
31+
for n := 0; n < b.N; n++ {
32+
program, _ := expr.Compile(code, expr.Env(env))
33+
out, err = vm.Run(program, env)
34+
}
35+
b.StopTimer()
36+
37+
require.NoError(b, err)
38+
require.True(b, out.(bool))
39+
}
40+
41+
func BenchmarkCall_eval(b *testing.B) {
42+
code := `Fn()`
43+
44+
p, err := expr.Compile(code)
45+
require.NoError(b, err)
46+
require.Equal(b, p.Bytecode[1], vm.OpCall)
47+
48+
env := Env{
49+
Fn: func() bool {
50+
return true
51+
},
52+
}
53+
54+
var out any
55+
b.ResetTimer()
56+
for n := 0; n < b.N; n++ {
57+
out, err = expr.Eval(code, env)
58+
}
59+
b.StopTimer()
60+
61+
require.NoError(b, err)
62+
require.True(b, out.(bool))
63+
}

0 commit comments

Comments
 (0)