Skip to content

Commit 3e8863a

Browse files
rrb3942antonmedv
authored andcommitted
Separate patching phases for patchers that require multiple passes. (expr-lang#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 1fe8676 commit 3e8863a

File tree

3 files changed

+126
-20
lines changed

3 files changed

+126
-20
lines changed

checker/checker.go

Lines changed: 60 additions & 20 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,26 +56,32 @@ func ParseCheck(input string, config *conf.Config) (*parser.Tree, error) {
2256
}
2357

2458
if len(config.Visitors) > 0 {
25-
// We need to perform types check, because some visitors may rely on
26-
// types information available in the tree.
27-
_, _ = Check(tree, config)
28-
29-
// TODO fubang 1000 count?
30-
for i := 0; i < 1000; i++ {
31-
more := false
32-
for _, v := range config.Visitors {
33-
ast.Walk(&tree.Node, v)
34-
35-
if v, ok := v.(interface {
36-
ShouldRepeat() bool
37-
}); ok {
38-
more = more || v.ShouldRepeat()
39-
}
40-
}
41-
if !more {
42-
break
43-
}
44-
}
59+
// // We need to perform types check, because some visitors may rely on
60+
// // types information available in the tree.
61+
// _, _ = Check(tree, config)
62+
63+
// // TODO fubang 1000 count?
64+
// for i := 0; i < 1000; i++ {
65+
// more := false
66+
// for _, v := range config.Visitors {
67+
// ast.Walk(&tree.Node, v)
68+
69+
// if v, ok := v.(interface {
70+
// ShouldRepeat() bool
71+
// }); ok {
72+
// more = more || v.ShouldRepeat()
73+
// }
74+
// }
75+
// if !more {
76+
// break
77+
// }
78+
// }
79+
80+
// Run all patchers that dont support being run repeatedly first
81+
runVisitors(tree, config, false)
82+
83+
// Run patchers that require multiple passes next (currently only Operator patching)
84+
runVisitors(tree, config, true)
4585
}
4686
_, err = Check(tree, config)
4787
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)