Skip to content

Commit ed9de34

Browse files
rrb3942antonmedv
andauthored
Separate patching phases for patchers that require multiple passes. (#659)
* Reset tracking of applied operator overloads before every walk of the ast tree. * Remove limit on operator overload resolution * Do separate patcher phase for those that require multiple passes (Currently only need for Operator overloading). This patcher phase is run after other patchers. * Add test to track how many times a patcher is run. Used to detect if patching phases is erroneously applying the patcher multiple times. --------- Co-authored-by: Anton Medvedev <[email protected]>
1 parent bc77b4b commit ed9de34

File tree

3 files changed

+105
-19
lines changed

3 files changed

+105
-19
lines changed

checker/checker.go

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,40 @@ import (
1313
"github.com/expr-lang/expr/parser"
1414
)
1515

16+
// Run visitors in a given config over the given tree
17+
// runRepeatable controls whether to filter for only vistors that require multiple passes or not
18+
func runVisitors(tree *parser.Tree, config *conf.Config, runRepeatable bool) {
19+
for {
20+
more := false
21+
for _, v := range config.Visitors {
22+
// We need to perform types check, because some visitors may rely on
23+
// types information available in the tree.
24+
_, _ = Check(tree, config)
25+
26+
r, repeatable := v.(interface {
27+
Reset()
28+
ShouldRepeat() bool
29+
})
30+
31+
if repeatable {
32+
if runRepeatable {
33+
r.Reset()
34+
ast.Walk(&tree.Node, v)
35+
more = more || r.ShouldRepeat()
36+
}
37+
} else {
38+
if !runRepeatable {
39+
ast.Walk(&tree.Node, v)
40+
}
41+
}
42+
}
43+
44+
if !more {
45+
break
46+
}
47+
}
48+
}
49+
1650
// ParseCheck parses input expression and checks its types. Also, it applies
1751
// all provided patchers. In case of error, it returns error with a tree.
1852
func ParseCheck(input string, config *conf.Config) (*parser.Tree, error) {
@@ -22,25 +56,11 @@ func ParseCheck(input string, config *conf.Config) (*parser.Tree, error) {
2256
}
2357

2458
if len(config.Visitors) > 0 {
25-
for i := 0; i < 1000; i++ {
26-
more := false
27-
for _, v := range config.Visitors {
28-
// We need to perform types check, because some visitors may rely on
29-
// types information available in the tree.
30-
_, _ = Check(tree, config)
31-
32-
ast.Walk(&tree.Node, v)
33-
34-
if v, ok := v.(interface {
35-
ShouldRepeat() bool
36-
}); ok {
37-
more = more || v.ShouldRepeat()
38-
}
39-
}
40-
if !more {
41-
break
42-
}
43-
}
59+
// Run all patchers that dont support being run repeatedly first
60+
runVisitors(tree, config, false)
61+
62+
// Run patchers that require multiple passes next (currently only Operator patching)
63+
runVisitors(tree, config, true)
4464
}
4565
_, err = Check(tree, config)
4666
if err != nil {

patcher/operator_override.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func (p *OperatorOverloading) Visit(node *ast.Node) {
4343
}
4444
}
4545

46+
// Tracking must be reset before every walk over the AST tree
47+
func (p *OperatorOverloading) Reset() {
48+
p.applied = false
49+
}
50+
4651
func (p *OperatorOverloading) ShouldRepeat() bool {
4752
return p.applied
4853
}

test/patch/patch_count_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package patch_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr/internal/testify/require"
7+
8+
"github.com/expr-lang/expr"
9+
"github.com/expr-lang/expr/ast"
10+
"github.com/expr-lang/expr/test/mock"
11+
)
12+
13+
// This patcher tracks how many nodes it patches which can
14+
// be used to verify if it was run too many times or not at all
15+
type countingPatcher struct {
16+
PatchCount int
17+
}
18+
19+
func (c *countingPatcher) Visit(node *ast.Node) {
20+
switch (*node).(type) {
21+
case *ast.IntegerNode:
22+
c.PatchCount++
23+
}
24+
}
25+
26+
// Test over a simple expression
27+
func TestPatch_Count(t *testing.T) {
28+
patcher := countingPatcher{}
29+
30+
_, err := expr.Compile(
31+
`5 + 5`,
32+
expr.Env(mock.Env{}),
33+
expr.Patch(&patcher),
34+
)
35+
require.NoError(t, err)
36+
37+
require.Equal(t, 2, patcher.PatchCount, "Patcher run an unexpected number of times during compile")
38+
}
39+
40+
// Test with operator overloading
41+
func TestPatchOperator_Count(t *testing.T) {
42+
patcher := countingPatcher{}
43+
44+
_, err := expr.Compile(
45+
`5 + 5`,
46+
expr.Env(mock.Env{}),
47+
expr.Patch(&patcher),
48+
expr.Operator("+", "_intAdd"),
49+
expr.Function(
50+
"_intAdd",
51+
func(params ...any) (any, error) {
52+
return params[0].(int) + params[1].(int), nil
53+
},
54+
new(func(int, int) int),
55+
),
56+
)
57+
58+
require.NoError(t, err)
59+
60+
require.Equal(t, 2, patcher.PatchCount, "Patcher run an unexpected number of times during compile")
61+
}

0 commit comments

Comments
 (0)