Skip to content

Commit d3aea46

Browse files
authored
Merge branch 'expr-lang:master' into master
2 parents ef455a9 + d63c3b5 commit d3aea46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2471
-382
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

ast/find.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package ast
2+
3+
func Find(node Node, fn func(node Node) bool) Node {
4+
v := &finder{fn: fn}
5+
Walk(&node, v)
6+
return v.node
7+
}
8+
9+
type finder struct {
10+
node Node
11+
fn func(node Node) bool
12+
}
13+
14+
func (f *finder) Visit(node *Node) {
15+
if f.fn(*node) {
16+
f.node = *node
17+
}
18+
}

ast/find_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ast_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr/internal/testify/require"
7+
8+
"github.com/expr-lang/expr/ast"
9+
)
10+
11+
func TestFind(t *testing.T) {
12+
left := &ast.IdentifierNode{
13+
Value: "a",
14+
}
15+
var root ast.Node = &ast.BinaryNode{
16+
Operator: "+",
17+
Left: left,
18+
Right: &ast.IdentifierNode{
19+
Value: "b",
20+
},
21+
}
22+
23+
x := ast.Find(root, func(node ast.Node) bool {
24+
if n, ok := node.(*ast.IdentifierNode); ok {
25+
return n.Value == "a"
26+
}
27+
return false
28+
})
29+
30+
require.Equal(t, left, x)
31+
}

ast/node.go

+7
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ type VariableDeclaratorNode struct {
216216
Expr Node // Expression of the variable. Like "foo + 1" in "let foo = 1; foo + 1".
217217
}
218218

219+
// SequenceNode represents a sequence of nodes separated by semicolons.
220+
// All nodes are executed, only the last node will be returned.
221+
type SequenceNode struct {
222+
base
223+
Nodes []Node
224+
}
225+
219226
// ArrayNode represents an array.
220227
type ArrayNode struct {
221228
base

ast/print.go

+31-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
}
@@ -177,6 +198,14 @@ func (n *VariableDeclaratorNode) String() string {
177198
return fmt.Sprintf("let %s = %s; %s", n.Name, n.Value.String(), n.Expr.String())
178199
}
179200

201+
func (n *SequenceNode) String() string {
202+
nodes := make([]string, len(n.Nodes))
203+
for i, node := range n.Nodes {
204+
nodes[i] = node.String()
205+
}
206+
return strings.Join(nodes, "; ")
207+
}
208+
180209
func (n *ConditionalNode) String() string {
181210
var cond, exp1, exp2 string
182211
if _, ok := n.Cond.(*ConditionalNode); ok {

ast/print_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ 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`},
86+
{`if true { 1 } else { 2 }`, `true ? 1 : 2`},
8187
}
8288

8389
for _, tt := range tests {

ast/visitor.go

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func Walk(node *Node, v Visitor) {
5151
case *VariableDeclaratorNode:
5252
Walk(&n.Value, v)
5353
Walk(&n.Expr, v)
54+
case *SequenceNode:
55+
for i := range n.Nodes {
56+
Walk(&n.Nodes[i], v)
57+
}
5458
case *ConditionalNode:
5559
Walk(&n.Cond, v)
5660
Walk(&n.Exp1, v)

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import (
55
"math"
66
"reflect"
77
"strconv"
8+
"unicode/utf8"
89

910
"github.com/expr-lang/expr/internal/deref"
1011
)
1112

1213
func Len(x any) any {
1314
v := reflect.ValueOf(x)
1415
switch v.Kind() {
15-
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
16+
case reflect.Array, reflect.Slice, reflect.Map:
1617
return v.Len()
18+
case reflect.String:
19+
return utf8.RuneCountInString(v.String())
1720
default:
1821
panic(fmt.Sprintf("invalid argument for len (type %T)", x))
1922
}
@@ -359,3 +362,17 @@ func median(args ...any) ([]float64, error) {
359362
}
360363
return values, nil
361364
}
365+
366+
func flatten(arg reflect.Value) []any {
367+
ret := []any{}
368+
for i := 0; i < arg.Len(); i++ {
369+
v := deref.Value(arg.Index(i))
370+
if v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
371+
x := flatten(v)
372+
ret = append(ret, x...)
373+
} else {
374+
ret = append(ret, v.Interface())
375+
}
376+
}
377+
return ret
378+
}

0 commit comments

Comments
 (0)