From 3a78c21a6a3e75d61ff75e23787b748505035c17 Mon Sep 17 00:00:00 2001 From: lvliangxiong Date: Thu, 25 Sep 2025 06:59:27 +0000 Subject: [PATCH 1/2] feat: add disable short-circuiting option to compiler --- compiler/compiler.go | 16 ++++++++++++++ conf/config.go | 2 ++ expr.go | 7 ++++++ expr_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++ vm/opcodes.go | 2 ++ vm/vm.go | 10 +++++++++ 6 files changed, 89 insertions(+) diff --git a/compiler/compiler.go b/compiler/compiler.go index c7469030c..0872f94a2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -446,6 +446,14 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) { c.emit(OpNot) case "or", "||": + if c.config.DisableSC { + c.compile(node.Left) + c.derefInNeeded(node.Left) + c.compile(node.Right) + c.derefInNeeded(node.Right) + c.emit(OpOr) + break + } c.compile(node.Left) c.derefInNeeded(node.Left) end := c.emit(OpJumpIfTrue, placeholder) @@ -455,6 +463,14 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) { c.patchJump(end) case "and", "&&": + if c.config.DisableSC { + c.compile(node.Left) + c.derefInNeeded(node.Left) + c.compile(node.Right) + c.derefInNeeded(node.Right) + c.emit(OpAnd) + break + } c.compile(node.Left) c.derefInNeeded(node.Left) end := c.emit(OpJumpIfFalse, placeholder) diff --git a/conf/config.go b/conf/config.go index 8799d3d04..b721d1ba7 100644 --- a/conf/config.go +++ b/conf/config.go @@ -35,6 +35,7 @@ type Config struct { Builtins FunctionsTable Disabled map[string]bool // disabled builtins NtCache nature.Cache + DisableSC bool } // CreateNew creates new config with default values. @@ -46,6 +47,7 @@ func CreateNew() *Config { Functions: make(map[string]*builtin.Function), Builtins: make(map[string]*builtin.Function), Disabled: make(map[string]bool), + DisableSC: false, } for _, f := range builtin.Builtins { c.Builtins[f.Name] = f diff --git a/expr.go b/expr.go index e8f4eb64b..31651c0d5 100644 --- a/expr.go +++ b/expr.go @@ -126,6 +126,13 @@ func Optimize(b bool) Option { } } +// DisableShortCircuit turns short circuit off. +func DisableShortCircuit() Option { + return func(c *conf.Config) { + c.DisableSC = true + } +} + // Patch adds visitor to list of visitors what will be applied before compiling AST to bytecode. func Patch(visitor ast.Visitor) Option { return func(c *conf.Config) { diff --git a/expr_test.go b/expr_test.go index 01ccdeeb9..a8be9ef3c 100644 --- a/expr_test.go +++ b/expr_test.go @@ -2883,3 +2883,55 @@ func TestIssue807(t *testing.T) { t.Fatalf("expected 'in' operator to return false for unexported field") } } + +func ExampleDisableShortCircuit() { + OR := func(a, b bool) bool { + return a || b + } + + env := map[string]any{ + "foo": func() bool { + fmt.Println("foo") + return false + }, + "bar": func() bool { + fmt.Println("bar") + return false + }, + "OR": OR, + } + + program, _ := expr.Compile("true || foo() or bar()", expr.Env(env), expr.Operator("or", "OR"), expr.Operator("||", "OR")) + got, _ := expr.Run(program, env) + fmt.Println(got) + + // Output: + // foo + // bar + // true +} + +func TestDisableShortCircuit(t *testing.T) { + count := 0 + exprStr := "foo() or bar()" + env := map[string]any{ + "foo": func() bool { + count++ + return true + }, + "bar": func() bool { + count++ + return true + }, + } + + program, _ := expr.Compile(exprStr, expr.DisableShortCircuit()) + got, _ := expr.Run(program, env) + assert.Equal(t, 2, count) + assert.True(t, got.(bool)) + + program, _ = expr.Compile(exprStr) + got, _ = expr.Run(program, env) + assert.Equal(t, 3, count) + assert.True(t, got.(bool)) +} diff --git a/vm/opcodes.go b/vm/opcodes.go index 84d751d6b..5fca0fa29 100644 --- a/vm/opcodes.go +++ b/vm/opcodes.go @@ -84,5 +84,7 @@ const ( OpProfileStart OpProfileEnd OpBegin + OpAnd + OpOr OpEnd // This opcode must be at the end of this list. ) diff --git a/vm/vm.go b/vm/vm.go index ed61d2f90..85abeb7c1 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -560,6 +560,16 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { Len: array.Len(), }) + case OpAnd: + a := vm.pop() + b := vm.pop() + vm.push(a.(bool) && b.(bool)) + + case OpOr: + a := vm.pop() + b := vm.pop() + vm.push(a.(bool) || b.(bool)) + case OpEnd: vm.Scopes = vm.Scopes[:len(vm.Scopes)-1] From fd9c3f0982a328347392f5a5e1935b24f670c8ff Mon Sep 17 00:00:00 2001 From: lvliangxiong Date: Sat, 27 Sep 2025 10:26:16 +0800 Subject: [PATCH 2/2] fix: nil config && unit test --- compiler/compiler.go | 4 ++-- vm/program.go | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 0872f94a2..f8902f357 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -446,7 +446,7 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) { c.emit(OpNot) case "or", "||": - if c.config.DisableSC { + if c.config != nil && c.config.DisableSC { c.compile(node.Left) c.derefInNeeded(node.Left) c.compile(node.Right) @@ -463,7 +463,7 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) { c.patchJump(end) case "and", "&&": - if c.config.DisableSC { + if c.config != nil && c.config.DisableSC { c.compile(node.Left) c.derefInNeeded(node.Left) c.compile(node.Right) diff --git a/vm/program.go b/vm/program.go index 15ce26f5b..31d1b88eb 100644 --- a/vm/program.go +++ b/vm/program.go @@ -372,6 +372,12 @@ func (program *Program) DisassembleWriter(w io.Writer) { case OpBegin: code("OpBegin") + case OpAnd: + code("OpAnd") + + case OpOr: + code("OpOr") + case OpEnd: code("OpEnd")