Skip to content

Commit e8714b2

Browse files
authored
Merge branch 'master' into fix_print_with_precedence
2 parents 3375714 + 156e73d commit e8714b2

40 files changed

+1729
-226
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 }}

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,14 @@ func main() {
162162
* [Visually.io](https://visually.io) employs Expr as a business rule engine for its personalization targeting algorithm.
163163
* [Akvorado](https://github.com/akvorado/akvorado) utilizes Expr to classify exporters and interfaces in network flows.
164164
* [keda.sh](https://keda.sh) uses Expr to allow customization of its Kubernetes-based event-driven autoscaling.
165-
* [Span Digital](https://spandigital.com/) uses Expr in it's Knowledge Management products.
165+
* [Span Digital](https://spandigital.com/) uses Expr in its Knowledge Management products.
166166
* [Xiaohongshu](https://www.xiaohongshu.com/) combining yaml with Expr for dynamically policies delivery.
167167
* [Melrōse](https://melrōse.org) uses Expr to implement its music programming language.
168+
* [Tork](https://www.tork.run/) integrates Expr into its workflow execution.
169+
* [Critical Moments](https://criticalmoments.io) uses Expr for its mobile realtime conditional targeting system.
170+
* [WoodpeckerCI](https://woodpecker-ci.org) uses Expr for [filtering workflows/steps](https://woodpecker-ci.org/docs/usage/workflow-syntax#evaluate).
171+
* [FastSchema](https://github.com/fastschema/fastschema) - A BaaS leveraging Expr for its customizable and dynamic Access Control system.
172+
* [WunderGraph Cosmo](https://github.com/wundergraph/cosmo) - GraphQL Federeration Router uses Expr to customize Middleware behaviour
168173

169174
[Add your company too](https://github.com/expr-lang/expr/edit/master/README.md)
170175

builtin/builtin.go

+82
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,57 @@ var Builtins = []*Function{
830830
}
831831
},
832832
},
833+
834+
{
835+
Name: "uniq",
836+
Func: func(args ...any) (any, error) {
837+
if len(args) != 1 {
838+
return nil, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
839+
}
840+
841+
v := reflect.ValueOf(deref.Deref(args[0]))
842+
if v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
843+
return nil, fmt.Errorf("cannot uniq %s", v.Kind())
844+
}
845+
846+
size := v.Len()
847+
ret := []any{}
848+
849+
eq := func(i int) bool {
850+
for _, r := range ret {
851+
if runtime.Equal(v.Index(i).Interface(), r) {
852+
return true
853+
}
854+
}
855+
856+
return false
857+
}
858+
859+
for i := 0; i < size; i += 1 {
860+
if eq(i) {
861+
continue
862+
}
863+
864+
ret = append(ret, v.Index(i).Interface())
865+
}
866+
867+
return ret, nil
868+
},
869+
870+
Validate: func(args []reflect.Type) (reflect.Type, error) {
871+
if len(args) != 1 {
872+
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
873+
}
874+
875+
switch kind(args[0]) {
876+
case reflect.Interface, reflect.Slice, reflect.Array:
877+
return arrayType, nil
878+
default:
879+
return anyType, fmt.Errorf("cannot uniq %s", args[0])
880+
}
881+
},
882+
},
883+
833884
{
834885
Name: "concat",
835886
Safe: func(args ...any) (any, uint, error) {
@@ -873,6 +924,37 @@ var Builtins = []*Function{
873924
return arrayType, nil
874925
},
875926
},
927+
{
928+
Name: "flatten",
929+
Safe: func(args ...any) (any, uint, error) {
930+
var size uint
931+
if len(args) != 1 {
932+
return nil, 0, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
933+
}
934+
v := reflect.ValueOf(deref.Deref(args[0]))
935+
if v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
936+
return nil, size, fmt.Errorf("cannot flatten %s", v.Kind())
937+
}
938+
ret := flatten(v)
939+
size = uint(len(ret))
940+
return ret, size, nil
941+
},
942+
Validate: func(args []reflect.Type) (reflect.Type, error) {
943+
if len(args) != 1 {
944+
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
945+
}
946+
947+
for _, arg := range args {
948+
switch kind(deref.Type(arg)) {
949+
case reflect.Interface, reflect.Slice, reflect.Array:
950+
default:
951+
return anyType, fmt.Errorf("cannot flatten %s", arg)
952+
}
953+
}
954+
955+
return arrayType, nil
956+
},
957+
},
876958
{
877959
Name: "sort",
878960
Safe: func(args ...any) (any, uint, error) {

builtin/builtin_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ func TestBuiltin(t *testing.T) {
152152
{`reduce([], 5, 0)`, 0},
153153
{`concat(ArrayOfString, ArrayOfInt)`, []any{"foo", "bar", "baz", 1, 2, 3}},
154154
{`concat(PtrArrayWithNil, [nil])`, []any{42, nil}},
155+
{`flatten([["a", "b"], [1, 2]])`, []any{"a", "b", 1, 2}},
156+
{`flatten([["a", "b"], [1, 2, [3, 4]]])`, []any{"a", "b", 1, 2, 3, 4}},
157+
{`flatten([["a", "b"], [1, 2, [3, [[[["c", "d"], "e"]]], 4]]])`, []any{"a", "b", 1, 2, 3, "c", "d", "e", 4}},
158+
{`uniq([1, 15, "a", 2, 3, 5, 2, "a", 2, "b"])`, []any{1, 15, "a", 2, 3, 5, "b"}},
159+
{`uniq([[1, 2], "a", 2, 3, [1, 2], [1, 3]])`, []any{[]any{1, 2}, "a", 2, 3, []any{1, 3}}},
155160
}
156161

157162
for _, test := range tests {
@@ -236,6 +241,8 @@ func TestBuiltin_errors(t *testing.T) {
236241
{`now(nil)`, "invalid number of arguments (expected 0, got 1)"},
237242
{`date(nil)`, "interface {} is nil, not string (1:1)"},
238243
{`timezone(nil)`, "cannot use nil as argument (type string) to call timezone (1:10)"},
244+
{`flatten([1, 2], [3, 4])`, "invalid number of arguments (expected 1, got 2)"},
245+
{`flatten(1)`, "cannot flatten int"},
239246
}
240247
for _, test := range errorTests {
241248
t.Run(test.input, func(t *testing.T) {

builtin/lib.go

+14
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,17 @@ func median(args ...any) ([]float64, error) {
359359
}
360360
return values, nil
361361
}
362+
363+
func flatten(arg reflect.Value) []any {
364+
ret := []any{}
365+
for i := 0; i < arg.Len(); i++ {
366+
v := deref.Value(arg.Index(i))
367+
if v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
368+
x := flatten(v)
369+
ret = append(ret, x...)
370+
} else {
371+
ret = append(ret, v.Interface())
372+
}
373+
}
374+
return ret
375+
}

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 {

checker/checker_test.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -1072,14 +1072,16 @@ func TestCheck_builtin_without_call(t *testing.T) {
10721072

10731073
func TestCheck_types(t *testing.T) {
10741074
env := types.Map{
1075-
"foo": types.StrictMap{
1075+
"foo": types.Map{
10761076
"bar": types.Map{
1077-
"baz": types.String,
1077+
"baz": types.String,
1078+
types.Extra: types.String,
10781079
},
10791080
},
1080-
"arr": types.Array(types.StrictMap{
1081+
"arr": types.Array(types.Map{
10811082
"value": types.String,
10821083
}),
1084+
types.Extra: types.Any,
10831085
}
10841086

10851087
noerr := "no error"
@@ -1088,9 +1090,11 @@ func TestCheck_types(t *testing.T) {
10881090
err string
10891091
}{
10901092
{`unknown`, noerr},
1093+
{`[unknown + 42, another_unknown + "foo"]`, noerr},
10911094
{`foo.bar.baz > 0`, `invalid operation: > (mismatched types string and int)`},
10921095
{`foo.unknown.baz`, `unknown field unknown (1:5)`},
10931096
{`foo.bar.unknown`, noerr},
1097+
{`foo.bar.unknown + 42`, `invalid operation: + (mismatched types string and int)`},
10941098
{`[foo] | map(.unknown)`, `unknown field unknown`},
10951099
{`[foo] | map(.bar) | filter(.baz)`, `predicate should return boolean (got string)`},
10961100
{`arr | filter(.value > 0)`, `invalid operation: > (mismatched types string and int)`},
@@ -1102,7 +1106,8 @@ func TestCheck_types(t *testing.T) {
11021106
tree, err := parser.Parse(test.code)
11031107
require.NoError(t, err)
11041108

1105-
_, err = checker.Check(tree, conf.New(env))
1109+
config := conf.New(env)
1110+
_, err = checker.Check(tree, config)
11061111
if test.err == noerr {
11071112
require.NoError(t, err)
11081113
} else {

checker/nature/nature.go

+17-11
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ var (
1212
)
1313

1414
type Nature struct {
15-
Type reflect.Type // Type of the value. If nil, then value is unknown.
16-
Func *builtin.Function // Used to pass function type from callee to CallNode.
17-
ArrayOf *Nature // Elem nature of array type (usually Type is []any, but ArrayOf can be any nature).
18-
PredicateOut *Nature // Out nature of predicate.
19-
Fields map[string]Nature // Fields of map type.
20-
Strict bool // If map is types.StrictMap.
21-
Nil bool // If value is nil.
22-
Method bool // If value retrieved from method. Usually used to determine amount of in arguments.
23-
MethodIndex int // Index of method in type.
24-
FieldIndex []int // Index of field in type.
15+
Type reflect.Type // Type of the value. If nil, then value is unknown.
16+
Func *builtin.Function // Used to pass function type from callee to CallNode.
17+
ArrayOf *Nature // Elem nature of array type (usually Type is []any, but ArrayOf can be any nature).
18+
PredicateOut *Nature // Out nature of predicate.
19+
Fields map[string]Nature // Fields of map type.
20+
DefaultMapValue *Nature // Default value of map type.
21+
Strict bool // If map is types.StrictMap.
22+
Nil bool // If value is nil.
23+
Method bool // If value retrieved from method. Usually used to determine amount of in arguments.
24+
MethodIndex int // Index of method in type.
25+
FieldIndex []int // Index of field in type.
2526
}
2627

2728
func (n Nature) String() string {
@@ -54,7 +55,12 @@ func (n Nature) Key() Nature {
5455

5556
func (n Nature) Elem() Nature {
5657
switch n.Kind() {
57-
case reflect.Map, reflect.Ptr:
58+
case reflect.Ptr:
59+
return Nature{Type: n.Type.Elem()}
60+
case reflect.Map:
61+
if n.DefaultMapValue != nil {
62+
return *n.DefaultMapValue
63+
}
5864
return Nature{Type: n.Type.Elem()}
5965
case reflect.Array, reflect.Slice:
6066
if n.ArrayOf != nil {

0 commit comments

Comments
 (0)