Skip to content

Commit eeb1b8b

Browse files
authored
Add expr.MaxNodes() option (#799)
* Add expr.MaxNodes() option * Fix error message * Add more tests and comments
1 parent 5fbfe72 commit eeb1b8b

File tree

4 files changed

+86
-46
lines changed

4 files changed

+86
-46
lines changed

conf/config.go

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,41 @@ import (
1010
"github.com/expr-lang/expr/vm/runtime"
1111
)
1212

13-
const (
14-
// DefaultMemoryBudget represents an upper limit of memory usage
13+
var (
14+
// DefaultMemoryBudget represents default maximum allowed memory usage by the vm.VM.
1515
DefaultMemoryBudget uint = 1e6
1616

17-
// DefaultMaxNodes represents an upper limit of AST nodes
18-
DefaultMaxNodes uint = 10000
17+
// DefaultMaxNodes represents default maximum allowed AST nodes by the compiler.
18+
DefaultMaxNodes uint = 1e4
1919
)
2020

2121
type FunctionsTable map[string]*builtin.Function
2222

2323
type Config struct {
24-
EnvObject any
25-
Env nature.Nature
26-
Expect reflect.Kind
27-
ExpectAny bool
28-
Optimize bool
29-
Strict bool
30-
Profile bool
31-
MaxNodes uint
32-
MemoryBudget uint
33-
ConstFns map[string]reflect.Value
34-
Visitors []ast.Visitor
35-
Functions FunctionsTable
36-
Builtins FunctionsTable
37-
Disabled map[string]bool // disabled builtins
24+
EnvObject any
25+
Env nature.Nature
26+
Expect reflect.Kind
27+
ExpectAny bool
28+
Optimize bool
29+
Strict bool
30+
Profile bool
31+
MaxNodes uint
32+
ConstFns map[string]reflect.Value
33+
Visitors []ast.Visitor
34+
Functions FunctionsTable
35+
Builtins FunctionsTable
36+
Disabled map[string]bool // disabled builtins
3837
}
3938

4039
// CreateNew creates new config with default values.
4140
func CreateNew() *Config {
4241
c := &Config{
43-
Optimize: true,
44-
MaxNodes: DefaultMaxNodes,
45-
MemoryBudget: DefaultMemoryBudget,
46-
ConstFns: make(map[string]reflect.Value),
47-
Functions: make(map[string]*builtin.Function),
48-
Builtins: make(map[string]*builtin.Function),
49-
Disabled: make(map[string]bool),
42+
Optimize: true,
43+
MaxNodes: DefaultMaxNodes,
44+
ConstFns: make(map[string]reflect.Value),
45+
Functions: make(map[string]*builtin.Function),
46+
Builtins: make(map[string]*builtin.Function),
47+
Disabled: make(map[string]bool),
5048
}
5149
for _, f := range builtin.Builtins {
5250
c.Builtins[f.Name] = f

expr.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ func Timezone(name string) Option {
195195
})
196196
}
197197

198+
// MaxNodes sets the maximum number of nodes allowed in the expression.
199+
// By default, the maximum number of nodes is conf.DefaultMaxNodes.
200+
// If MaxNodes is set to 0, the node budget check is disabled.
201+
func MaxNodes(n uint) Option {
202+
return func(c *conf.Config) {
203+
c.MaxNodes = n
204+
}
205+
}
206+
198207
// Compile parses and compiles given input expression to bytecode program.
199208
func Compile(input string, ops ...Option) (*vm.Program, error) {
200209
config := conf.CreateNew()

expr_test.go

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/expr-lang/expr/conf"
1314
"github.com/expr-lang/expr/internal/testify/assert"
1415
"github.com/expr-lang/expr/internal/testify/require"
1516
"github.com/expr-lang/expr/types"
17+
"github.com/expr-lang/expr/vm"
1618

1719
"github.com/expr-lang/expr"
1820
"github.com/expr-lang/expr/ast"
@@ -2225,26 +2227,6 @@ func TestEval_slices_out_of_bound(t *testing.T) {
22252227
}
22262228
}
22272229

2228-
func TestMemoryBudget(t *testing.T) {
2229-
tests := []struct {
2230-
code string
2231-
}{
2232-
{`map(1..100, {map(1..100, {map(1..100, {0})})})`},
2233-
{`len(1..10000000)`},
2234-
}
2235-
2236-
for _, tt := range tests {
2237-
t.Run(tt.code, func(t *testing.T) {
2238-
program, err := expr.Compile(tt.code)
2239-
require.NoError(t, err, "compile error")
2240-
2241-
_, err = expr.Run(program, nil)
2242-
assert.Error(t, err, "run error")
2243-
assert.Contains(t, err.Error(), "memory budget exceeded")
2244-
})
2245-
}
2246-
}
2247-
22482230
func TestExpr_custom_tests(t *testing.T) {
22492231
f, err := os.Open("custom_tests.json")
22502232
if os.IsNotExist(err) {
@@ -2731,3 +2713,55 @@ func TestIssue785_get_nil(t *testing.T) {
27312713
})
27322714
}
27332715
}
2716+
2717+
func TestMaxNodes(t *testing.T) {
2718+
maxNodes := uint(100)
2719+
2720+
code := ""
2721+
for i := 0; i < int(maxNodes); i++ {
2722+
code += "1; "
2723+
}
2724+
2725+
_, err := expr.Compile(code, expr.MaxNodes(maxNodes))
2726+
require.Error(t, err)
2727+
assert.Contains(t, err.Error(), "exceeds maximum allowed nodes")
2728+
2729+
_, err = expr.Compile(code, expr.MaxNodes(maxNodes+1))
2730+
require.NoError(t, err)
2731+
}
2732+
2733+
func TestMaxNodesDisabled(t *testing.T) {
2734+
code := ""
2735+
for i := 0; i < 2*int(conf.DefaultMaxNodes); i++ {
2736+
code += "1; "
2737+
}
2738+
2739+
_, err := expr.Compile(code, expr.MaxNodes(0))
2740+
require.NoError(t, err)
2741+
}
2742+
2743+
func TestMemoryBudget(t *testing.T) {
2744+
tests := []struct {
2745+
code string
2746+
max int
2747+
}{
2748+
{`map(1..100, {map(1..100, {map(1..100, {0})})})`, -1},
2749+
{`len(1..10000000)`, -1},
2750+
{`1..100`, 100},
2751+
}
2752+
2753+
for _, tt := range tests {
2754+
t.Run(tt.code, func(t *testing.T) {
2755+
program, err := expr.Compile(tt.code)
2756+
require.NoError(t, err, "compile error")
2757+
2758+
vm := vm.VM{}
2759+
if tt.max > 0 {
2760+
vm.MemoryBudget = uint(tt.max)
2761+
}
2762+
_, err = vm.Run(program, nil)
2763+
require.Error(t, err, "run error")
2764+
assert.Contains(t, err.Error(), "memory budget exceeded")
2765+
})
2766+
}
2767+
}

vm/vm.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
7575
if len(vm.Variables) < program.variables {
7676
vm.Variables = make([]any, program.variables)
7777
}
78-
7978
if vm.MemoryBudget == 0 {
8079
vm.MemoryBudget = conf.DefaultMemoryBudget
8180
}

0 commit comments

Comments
 (0)