Skip to content

Commit 44ae912

Browse files
authored
Merge branch 'master' into refactor/env-type-assert
2 parents 4fd9673 + af32277 commit 44ae912

22 files changed

+1582
-133
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*\[generated\].go linguist-language=txt

.github/scripts/coverage.mjs

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
const expected = 90
44
const exclude = [
5-
'expr/test',
6-
'checker/mock',
7-
'vm/func_types',
8-
'vm/runtime/helpers',
9-
'internal/difflib',
10-
'internal/spew',
11-
'internal/testify',
5+
'expr/test', // We do not need to test the test package.
6+
'checker/mock', // Mocks only used for testing.
7+
'vm/func_types', // Generated files.
8+
'vm/runtime/helpers', // Generated files.
9+
'internal/difflib', // Test dependency. This is vendored dependency, and ideally we also have good tests for it.
10+
'internal/spew', // Test dependency.
11+
'internal/testify', // Test dependency.
12+
'patcher/value', // Contains a lot of repeating code. Ideally we should have a test for it.
13+
'pro', // Expr Pro is not a part of the main codebase.
1214
]
1315

1416
cd(path.resolve(__dirname, '..', '..'))

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
go-versions: [ '1.18', '1.22' ]
14+
go-versions: [ '1.18', '1.22', '1.24' ]
1515
go-arch: [ '386' ]
1616
steps:
1717
- uses: actions/checkout@v3

.github/workflows/diff.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ jobs:
1313
with:
1414
go-version: 1.18
1515
- name: Install benchstat
16-
run: go install golang.org/x/perf/cmd/benchstat@latest
16+
# NOTE: benchstat@latest requires go 1.23 since 2025-02-14 - this is the last go 1.18 ref
17+
# https://cs.opensource.google/go/x/perf/+/c95ad7d5b636f67d322a7e4832e83103d0fdd292
18+
run: go install golang.org/x/perf/cmd/benchstat@884df5810d2850d775c2cb4885a7ea339128a17d
1719

1820
- uses: actions/checkout@v3
1921
- name: Benchmark new code

.github/workflows/fuzz.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ jobs:
2121
fuzz-seconds: 600
2222
output-sarif: true
2323
- name: Upload Crash
24-
uses: actions/upload-artifact@v3
24+
uses: actions/upload-artifact@v4
2525
if: failure() && steps.build.outcome == 'success'
2626
with:
2727
name: artifacts
2828
path: ./out/artifacts
2929
- name: Upload Sarif
3030
if: always() && steps.build.outcome == 'success'
31-
uses: github/codeql-action/upload-sarif@v2
31+
uses: github/codeql-action/upload-sarif@v3
3232
with:
3333
# Path to SARIF file relative to the root of the repository
3434
sarif_file: cifuzz-sarif/results.sarif

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22' ]
14+
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ]
1515
steps:
1616
- uses: actions/checkout@v3
1717
- name: Setup Go ${{ matrix.go-version }}

ast/print.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,13 @@ func (n *UnaryNode) String() string {
5050
op = fmt.Sprintf("%s ", n.Operator)
5151
}
5252
wrap := false
53-
switch n.Node.(type) {
54-
case *BinaryNode, *ConditionalNode:
53+
switch b := n.Node.(type) {
54+
case *BinaryNode:
55+
if operator.Binary[b.Operator].Precedence <
56+
operator.Unary[n.Operator].Precedence {
57+
wrap = true
58+
}
59+
case *ConditionalNode:
5560
wrap = true
5661
}
5762
if wrap {
@@ -68,10 +73,21 @@ func (n *BinaryNode) String() string {
6873
var lhs, rhs string
6974
var lwrap, rwrap bool
7075

76+
if l, ok := n.Left.(*UnaryNode); ok {
77+
if operator.Unary[l.Operator].Precedence <
78+
operator.Binary[n.Operator].Precedence {
79+
lwrap = true
80+
}
81+
}
7182
if lb, ok := n.Left.(*BinaryNode); ok {
7283
if operator.Less(lb.Operator, n.Operator) {
7384
lwrap = true
7485
}
86+
if operator.Binary[lb.Operator].Precedence ==
87+
operator.Binary[n.Operator].Precedence &&
88+
operator.Binary[n.Operator].Associativity == operator.Right {
89+
lwrap = true
90+
}
7591
if lb.Operator == "??" {
7692
lwrap = true
7793
}
@@ -83,6 +99,11 @@ func (n *BinaryNode) String() string {
8399
if operator.Less(rb.Operator, n.Operator) {
84100
rwrap = true
85101
}
102+
if operator.Binary[rb.Operator].Precedence ==
103+
operator.Binary[n.Operator].Precedence &&
104+
operator.Binary[n.Operator].Associativity == operator.Left {
105+
rwrap = true
106+
}
86107
if operator.IsBoolean(rb.Operator) && n.Operator != rb.Operator {
87108
rwrap = true
88109
}

ast/print_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ func TestPrint(t *testing.T) {
7878
{`{("a" + "b"): 42}`, `{("a" + "b"): 42}`},
7979
{`(One == 1 ? true : false) && Two == 2`, `(One == 1 ? true : false) && Two == 2`},
8080
{`not (a == 1 ? b > 1 : b < 2)`, `not (a == 1 ? b > 1 : b < 2)`},
81+
{`(-(1+1)) ** 2`, `(-(1 + 1)) ** 2`},
82+
{`2 ** (-(1+1))`, `2 ** -(1 + 1)`},
83+
{`(2 ** 2) ** 3`, `(2 ** 2) ** 3`},
84+
{`(3 + 5) / (5 % 3)`, `(3 + 5) / (5 % 3)`},
85+
{`(-(1+1)) == 2`, `-(1 + 1) == 2`},
8186
}
8287

8388
for _, tt := range tests {

checker/checker.go

+39-19
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 {

conf/config.go

+29-17
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,43 @@ import (
1010
"github.com/expr-lang/expr/vm/runtime"
1111
)
1212

13+
const (
14+
// DefaultMemoryBudget represents an upper limit of memory usage
15+
DefaultMemoryBudget uint = 1e6
16+
17+
// DefaultMaxNodes represents an upper limit of AST nodes
18+
DefaultMaxNodes uint = 10000
19+
)
20+
1321
type FunctionsTable map[string]*builtin.Function
1422

1523
type Config struct {
16-
EnvObject any
17-
Env nature.Nature
18-
Expect reflect.Kind
19-
ExpectAny bool
20-
Optimize bool
21-
Strict bool
22-
Profile bool
23-
ConstFns map[string]reflect.Value
24-
Visitors []ast.Visitor
25-
Functions FunctionsTable
26-
Builtins FunctionsTable
27-
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+
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
2838
}
2939

3040
// CreateNew creates new config with default values.
3141
func CreateNew() *Config {
3242
c := &Config{
33-
Optimize: true,
34-
ConstFns: make(map[string]reflect.Value),
35-
Functions: make(map[string]*builtin.Function),
36-
Builtins: make(map[string]*builtin.Function),
37-
Disabled: make(map[string]bool),
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),
3850
}
3951
for _, f := range builtin.Builtins {
4052
c.Builtins[f.Name] = f

docs/language-definition.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Backticks strings are raw strings, they do not support escape sequences.
9797
<tr>
9898
<td><strong>Conditional</strong></td>
9999
<td>
100-
<code>?:</code> (ternary), <code>??</code> (nil coalescing)
100+
<code>?:</code> (ternary), <code>??</code> (nil coalescing), <code>if {} else {}</code> (multiline)
101101
</td>
102102
</tr>
103103
<tr>

expr_test.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func ExampleOperator() {
312312
// Output: true
313313
}
314314

315-
func ExampleOperator_Decimal() {
315+
func ExampleOperator_with_decimal() {
316316
type Decimal struct{ N float64 }
317317
code := `A + B - C`
318318

@@ -585,7 +585,7 @@ func ExampleWithContext() {
585585
// Output: 42
586586
}
587587

588-
func ExampleWithTimezone() {
588+
func ExampleTimezone() {
589589
program, err := expr.Compile(`now().Location().String()`, expr.Timezone("Asia/Kamchatka"))
590590
if err != nil {
591591
fmt.Printf("%v", err)
@@ -1291,6 +1291,21 @@ func TestExpr(t *testing.T) {
12911291
`1 < 2 < 3 == true`,
12921292
true,
12931293
},
1294+
{
1295+
`if 1 > 2 { 333 * 2 + 1 } else { 444 }`,
1296+
444,
1297+
},
1298+
{
1299+
`let a = 3;
1300+
let b = 2;
1301+
if a>b {let c = Add(a, b); c+1} else {Add(10, b)}
1302+
`,
1303+
6,
1304+
},
1305+
{
1306+
`if "a" < "b" {let x = "a"; x} else {"abc"}`,
1307+
"a",
1308+
},
12941309
}
12951310

12961311
for _, tt := range tests {

internal/difflib/difflib_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ group
102102
}
103103
}
104104

105-
func ExampleGetUnifiedDiffCode() {
105+
func ExampleGetUnifiedDiffString() {
106106
a := `one
107107
two
108108
three
@@ -135,7 +135,7 @@ four`
135135
// -fmt.Printf("%s,%T",a,b)
136136
}
137137

138-
func ExampleGetContextDiffCode() {
138+
func ExampleGetContextDiffString() {
139139
a := `one
140140
two
141141
three
@@ -172,7 +172,7 @@ four`
172172
// four
173173
}
174174

175-
func ExampleGetContextDiffString() {
175+
func ExampleGetContextDiffString_second() {
176176
a := `one
177177
two
178178
three

parser/lexer/lexer_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,42 @@ func TestLex(t *testing.T) {
239239
{Kind: EOF},
240240
},
241241
},
242+
{
243+
`if a>b {x1+x2} else {x2}`,
244+
[]Token{
245+
{Kind: Operator, Value: "if"},
246+
{Kind: Identifier, Value: "a"},
247+
{Kind: Operator, Value: ">"},
248+
{Kind: Identifier, Value: "b"},
249+
{Kind: Bracket, Value: "{"},
250+
{Kind: Identifier, Value: "x1"},
251+
{Kind: Operator, Value: "+"},
252+
{Kind: Identifier, Value: "x2"},
253+
{Kind: Bracket, Value: "}"},
254+
{Kind: Operator, Value: "else"},
255+
{Kind: Bracket, Value: "{"},
256+
{Kind: Identifier, Value: "x2"},
257+
{Kind: Bracket, Value: "}"},
258+
{Kind: EOF},
259+
},
260+
},
261+
{
262+
`a>b if {x1} else {x2}`,
263+
[]Token{
264+
{Kind: Identifier, Value: "a"},
265+
{Kind: Operator, Value: ">"},
266+
{Kind: Identifier, Value: "b"},
267+
{Kind: Operator, Value: "if"},
268+
{Kind: Bracket, Value: "{"},
269+
{Kind: Identifier, Value: "x1"},
270+
{Kind: Bracket, Value: "}"},
271+
{Kind: Operator, Value: "else"},
272+
{Kind: Bracket, Value: "{"},
273+
{Kind: Identifier, Value: "x2"},
274+
{Kind: Bracket, Value: "}"},
275+
{Kind: EOF},
276+
},
277+
},
242278
}
243279

244280
for _, test := range tests {

parser/lexer/state.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ loop:
129129
switch l.word() {
130130
case "not":
131131
return not
132-
case "in", "or", "and", "matches", "contains", "startsWith", "endsWith":
133-
l.emit(Operator)
134-
case "let":
132+
case "in", "or", "and", "matches", "contains", "startsWith", "endsWith", "let", "if", "else":
135133
l.emit(Operator)
136134
default:
137135
l.emit(Identifier)

0 commit comments

Comments
 (0)