Skip to content

Commit 26af6c4

Browse files
authored
Merge branch 'master' into fix/issue-823-part2
2 parents 51c878b + 40bda0b commit 26af6c4

37 files changed

Lines changed: 23225 additions & 38 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
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', '1.24', '1.25' ]
14+
go-versions: [ '1.18', '1.22', '1.24', '1.25', '1.26' ]
1515
go-arch: [ '386' ]
1616
steps:
1717
- uses: actions/checkout@v3

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
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', '1.23', '1.24', '1.25' ]
14+
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25', '1.26' ]
1515
steps:
1616
- uses: actions/checkout@v3
1717
- name: Setup Go ${{ matrix.go-version }}

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
<h1><a href="https://expr-lang.org"><img src="https://expr-lang.org/img/logo.png" alt="Zx logo" height="48"align="right"></a> Expr</h1>
22

3-
> [!IMPORTANT]
4-
> The repository [github.com/antonmedv/expr](https://github.com/antonmedv/expr) moved to [github.com/**expr-lang**/expr](https://github.com/expr-lang/expr).
5-
63
[![test](https://github.com/expr-lang/expr/actions/workflows/test.yml/badge.svg)](https://github.com/expr-lang/expr/actions/workflows/test.yml)
74
[![Go Report Card](https://goreportcard.com/badge/github.com/expr-lang/expr)](https://goreportcard.com/report/github.com/expr-lang/expr)
85
[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/expr.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:expr)
@@ -173,6 +170,7 @@ func main() {
173170
* [WunderGraph Cosmo](https://github.com/wundergraph/cosmo) - GraphQL Federeration Router uses Expr to customize Middleware behaviour
174171
* [SOLO](https://solo.one) uses Expr interally to allow dynamic code execution with custom defined functions.
175172
* [Naoma.AI](https://www.naoma.ai) uses Expr as a part of its call scoring engine.
173+
* [GlassFlow.dev](https://github.com/glassflow/clickhouse-etl) uses Expr to do realtime data transformation in ETL pipelines
176174

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

ast/node.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ type StringNode struct {
104104
Value string // Value of the string.
105105
}
106106

107+
// BytesNode represents a byte slice.
108+
type BytesNode struct {
109+
base
110+
Value []byte // Value of the byte slice.
111+
}
112+
107113
// ConstantNode represents a constant.
108114
// Constants are predefined values like nil, true, false, array, map, etc.
109115
// The parser.Parse will never generate ConstantNode, it is only generated
@@ -181,6 +187,7 @@ type BuiltinNode struct {
181187
Arguments []Node // Arguments of the builtin function.
182188
Throws bool // If true then accessing a field or array index can throw an error. Used by optimizer.
183189
Map Node // Used by optimizer to fold filter() and map() builtins.
190+
Threshold *int // Used by optimizer for count() early termination.
184191
}
185192

186193
// PredicateNode represents a predicate.

ast/print.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ func (n *StringNode) String() string {
3333
return fmt.Sprintf("%q", n.Value)
3434
}
3535

36+
func (n *BytesNode) String() string {
37+
return fmt.Sprintf("b%q", n.Value)
38+
}
39+
3640
func (n *ConstantNode) String() string {
3741
if n.Value == nil {
3842
return "nil"

ast/visitor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func Walk(node *Node, v Visitor) {
1717
case *FloatNode:
1818
case *BoolNode:
1919
case *StringNode:
20+
case *BytesNode:
2021
case *ConstantNode:
2122
case *UnaryNode:
2223
Walk(&n.Node, v)

bench_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,71 @@ func Benchmark_reduce(b *testing.B) {
571571

572572
require.Equal(b, 5050, out.(int))
573573
}
574+
575+
func Benchmark_min(b *testing.B) {
576+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
577+
env := map[string]any{"arr": arr}
578+
579+
program, err := expr.Compile(`min(arr)`, expr.Env(env))
580+
require.NoError(b, err)
581+
582+
var out any
583+
b.ResetTimer()
584+
for n := 0; n < b.N; n++ {
585+
out, _ = vm.Run(program, env)
586+
}
587+
b.StopTimer()
588+
589+
require.Equal(b, 1, out)
590+
}
591+
592+
func Benchmark_max(b *testing.B) {
593+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
594+
env := map[string]any{"arr": arr}
595+
596+
program, err := expr.Compile(`max(arr)`, expr.Env(env))
597+
require.NoError(b, err)
598+
599+
var out any
600+
b.ResetTimer()
601+
for n := 0; n < b.N; n++ {
602+
out, _ = vm.Run(program, env)
603+
}
604+
b.StopTimer()
605+
606+
require.Equal(b, 100, out)
607+
}
608+
609+
func Benchmark_mean(b *testing.B) {
610+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
611+
env := map[string]any{"arr": arr}
612+
613+
program, err := expr.Compile(`mean(arr)`, expr.Env(env))
614+
require.NoError(b, err)
615+
616+
var out any
617+
b.ResetTimer()
618+
for n := 0; n < b.N; n++ {
619+
out, _ = vm.Run(program, env)
620+
}
621+
b.StopTimer()
622+
623+
require.Equal(b, 50.5, out)
624+
}
625+
626+
func Benchmark_median(b *testing.B) {
627+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
628+
env := map[string]any{"arr": arr}
629+
630+
program, err := expr.Compile(`median(arr)`, expr.Env(env))
631+
require.NoError(b, err)
632+
633+
var out any
634+
b.ResetTimer()
635+
for n := 0; n < b.N; n++ {
636+
out, _ = vm.Run(program, env)
637+
}
638+
b.StopTimer()
639+
640+
require.Equal(b, 50.5, out)
641+
}

builtin/builtin.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,13 +1000,17 @@ var Builtins = []*Function{
10001000

10011001
var desc bool
10021002
if len(args) == 2 {
1003-
switch args[1].(string) {
1003+
order, ok := args[1].(string)
1004+
if !ok {
1005+
return nil, 0, fmt.Errorf("sort order argument must be a string (got %T)", args[1])
1006+
}
1007+
switch order {
10041008
case "asc":
10051009
desc = false
10061010
case "desc":
10071011
desc = true
10081012
default:
1009-
return nil, 0, fmt.Errorf("invalid order %s, expected asc or desc", args[1])
1013+
return nil, 0, fmt.Errorf("invalid order %s, expected asc or desc", order)
10101014
}
10111015
}
10121016

builtin/builtin_test.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ import (
2121
func TestBuiltin(t *testing.T) {
2222
ArrayWithNil := []any{42}
2323
env := map[string]any{
24-
"ArrayOfString": []string{"foo", "bar", "baz"},
25-
"ArrayOfInt": []int{1, 2, 3},
26-
"ArrayOfAny": []any{1, "2", true},
27-
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
28-
"PtrArrayWithNil": &ArrayWithNil,
24+
"ArrayOfString": []string{"foo", "bar", "baz"},
25+
"ArrayOfInt": []int{1, 2, 3},
26+
"ArrayOfFloat": []float64{1.5, 2.5, 3.5},
27+
"ArrayOfInt32": []int32{1, 2, 3},
28+
"ArrayOfAny": []any{1, "2", true},
29+
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
30+
"PtrArrayWithNil": &ArrayWithNil,
31+
"EmptyIntArray": []int{},
32+
"EmptyFloatArray": []float64{},
33+
"NestedIntArrays": []any{[]int{1, 2}, []int{3, 4}},
34+
"NestedAnyArrays": []any{[]any{1, 2}, []any{3, 4}},
35+
"MixedNestedArray": []any{1, []int{2, 3}, []float64{4.0, 5.0}},
36+
"NestedInt32Array": []any{[]int32{1, 2}, []int32{3, 4}},
2937
}
3038

3139
var tests = []struct {
@@ -86,6 +94,22 @@ func TestBuiltin(t *testing.T) {
8694
{`min([1, 2, 3])`, 1},
8795
{`min([1.5, 2.5, 3.5])`, 1.5},
8896
{`min(-1, [1.5, 2.5, 3.5])`, -1},
97+
{`max(ArrayOfInt)`, 3},
98+
{`min(ArrayOfInt)`, 1},
99+
{`max(ArrayOfFloat)`, 3.5},
100+
{`min(ArrayOfFloat)`, 1.5},
101+
{`max(EmptyIntArray, 5)`, 5},
102+
{`min(EmptyFloatArray, 5)`, 5},
103+
{`max(NestedIntArrays)`, 4},
104+
{`min(NestedIntArrays)`, 1},
105+
{`max(NestedAnyArrays)`, 4},
106+
{`min(NestedAnyArrays)`, 1},
107+
{`max(MixedNestedArray)`, 5.0},
108+
{`min(MixedNestedArray)`, 1},
109+
{`max(ArrayOfInt32)`, int32(3)},
110+
{`min(ArrayOfInt32)`, int32(1)},
111+
{`max(NestedInt32Array)`, int32(4)},
112+
{`min(NestedInt32Array)`, int32(1)},
89113
{`sum(1..9)`, 45},
90114
{`sum([.5, 1.5, 2.5])`, 4.5},
91115
{`sum([])`, 0},
@@ -97,6 +121,13 @@ func TestBuiltin(t *testing.T) {
97121
{`mean(10, [1, 2, 3], 1..9)`, 4.6923076923076925},
98122
{`mean(-10, [1, 2, 3, 4])`, 0.0},
99123
{`mean(10.9, 1..9)`, 5.59},
124+
{`mean(ArrayOfInt)`, 2.0},
125+
{`mean(ArrayOfFloat)`, 2.5},
126+
{`mean(NestedIntArrays)`, 2.5},
127+
{`mean(NestedAnyArrays)`, 2.5},
128+
{`mean(MixedNestedArray)`, 3.0},
129+
{`mean(ArrayOfInt32)`, 2.0},
130+
{`mean(NestedInt32Array)`, 2.5},
100131
{`median(1..9)`, 5.0},
101132
{`median([.5, 1.5, 2.5])`, 1.5},
102133
{`median([])`, 0.0},
@@ -105,6 +136,13 @@ func TestBuiltin(t *testing.T) {
105136
{`median(10, [1, 2, 3], 1..9)`, 4.0},
106137
{`median(-10, [1, 2, 3, 4])`, 2.0},
107138
{`median(1..5, 4.9)`, 3.5},
139+
{`median(ArrayOfInt)`, 2.0},
140+
{`median(ArrayOfFloat)`, 2.5},
141+
{`median(NestedIntArrays)`, 2.5},
142+
{`median(NestedAnyArrays)`, 2.5},
143+
{`median(MixedNestedArray)`, 3.0},
144+
{`median(ArrayOfInt32)`, 2.0},
145+
{`median(NestedInt32Array)`, 2.5},
108146
{`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"},
109147
{`fromJSON("[1, 2, 3]")`, []any{1.0, 2.0, 3.0}},
110148
{`toBase64("hello")`, "aGVsbG8="},
@@ -150,6 +188,8 @@ func TestBuiltin(t *testing.T) {
150188
{`reduce(1..9, # + #acc)`, 45},
151189
{`reduce([.5, 1.5, 2.5], # + #acc, 0)`, 4.5},
152190
{`reduce([], 5, 0)`, 0},
191+
{`reduce(10..1, # + #acc, 100)`, 100},
192+
{`reduce([], # + #acc, 42)`, 42},
153193
{`concat(ArrayOfString, ArrayOfInt)`, []any{"foo", "bar", "baz", 1, 2, 3}},
154194
{`concat(PtrArrayWithNil, [nil])`, []any{42, nil}},
155195
{`flatten([["a", "b"], [1, 2]])`, []any{"a", "b", 1, 2}},
@@ -243,6 +283,7 @@ func TestBuiltin_errors(t *testing.T) {
243283
{`timezone(nil)`, "cannot use nil as argument (type string) to call timezone (1:10)"},
244284
{`flatten([1, 2], [3, 4])`, "invalid number of arguments (expected 1, got 2)"},
245285
{`flatten(1)`, "cannot flatten int"},
286+
{`fromJSON("5e2482")`, "cannot unmarshal number"},
246287
}
247288
for _, test := range errorTests {
248289
t.Run(test.input, func(t *testing.T) {
@@ -259,6 +300,15 @@ func TestBuiltin_errors(t *testing.T) {
259300
}
260301
}
261302

303+
func TestBuiltin_env_not_callable(t *testing.T) {
304+
code := `$env(''matches'i'?t:get().UTC())`
305+
env := map[string]any{"t": 1}
306+
307+
_, err := expr.Compile(code, expr.Env(env))
308+
require.Error(t, err)
309+
assert.Contains(t, err.Error(), "is not callable")
310+
}
311+
262312
func TestBuiltin_types(t *testing.T) {
263313
env := map[string]any{
264314
"num": 42,

0 commit comments

Comments
 (0)