mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Make expr.Eval faster (#775)
* Make expr.Eval faster * Add bench_call_test.go * Make parser config optional
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user