Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,71 @@ func Benchmark_reduce(b *testing.B) {

require.Equal(b, 5050, out.(int))
}

func Benchmark_min(b *testing.B) {
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}
env := map[string]any{"arr": arr}

program, err := expr.Compile(`min(arr)`, expr.Env(env))
require.NoError(b, err)

var out any
b.ResetTimer()
for n := 0; n < b.N; n++ {
out, _ = vm.Run(program, env)
}
b.StopTimer()

require.Equal(b, 1, out)
}

func Benchmark_max(b *testing.B) {
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}
env := map[string]any{"arr": arr}

program, err := expr.Compile(`max(arr)`, expr.Env(env))
require.NoError(b, err)

var out any
b.ResetTimer()
for n := 0; n < b.N; n++ {
out, _ = vm.Run(program, env)
}
b.StopTimer()

require.Equal(b, 100, out)
}

func Benchmark_mean(b *testing.B) {
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}
env := map[string]any{"arr": arr}

program, err := expr.Compile(`mean(arr)`, expr.Env(env))
require.NoError(b, err)

var out any
b.ResetTimer()
for n := 0; n < b.N; n++ {
out, _ = vm.Run(program, env)
}
b.StopTimer()

require.Equal(b, 50.5, out)
}

func Benchmark_median(b *testing.B) {
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}
env := map[string]any{"arr": arr}

program, err := expr.Compile(`median(arr)`, expr.Env(env))
require.NoError(b, err)

var out any
b.ResetTimer()
for n := 0; n < b.N; n++ {
out, _ = vm.Run(program, env)
}
b.StopTimer()

require.Equal(b, 50.5, out)
}
48 changes: 43 additions & 5 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import (
func TestBuiltin(t *testing.T) {
ArrayWithNil := []any{42}
env := map[string]any{
"ArrayOfString": []string{"foo", "bar", "baz"},
"ArrayOfInt": []int{1, 2, 3},
"ArrayOfAny": []any{1, "2", true},
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
"PtrArrayWithNil": &ArrayWithNil,
"ArrayOfString": []string{"foo", "bar", "baz"},
"ArrayOfInt": []int{1, 2, 3},
"ArrayOfFloat": []float64{1.5, 2.5, 3.5},
"ArrayOfInt32": []int32{1, 2, 3},
"ArrayOfAny": []any{1, "2", true},
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
"PtrArrayWithNil": &ArrayWithNil,
"EmptyIntArray": []int{},
"EmptyFloatArray": []float64{},
"NestedIntArrays": []any{[]int{1, 2}, []int{3, 4}},
"NestedAnyArrays": []any{[]any{1, 2}, []any{3, 4}},
"MixedNestedArray": []any{1, []int{2, 3}, []float64{4.0, 5.0}},
"NestedInt32Array": []any{[]int32{1, 2}, []int32{3, 4}},
}

var tests = []struct {
Expand Down Expand Up @@ -86,6 +94,22 @@ func TestBuiltin(t *testing.T) {
{`min([1, 2, 3])`, 1},
{`min([1.5, 2.5, 3.5])`, 1.5},
{`min(-1, [1.5, 2.5, 3.5])`, -1},
{`max(ArrayOfInt)`, 3},
{`min(ArrayOfInt)`, 1},
{`max(ArrayOfFloat)`, 3.5},
{`min(ArrayOfFloat)`, 1.5},
{`max(EmptyIntArray, 5)`, 5},
{`min(EmptyFloatArray, 5)`, 5},
{`max(NestedIntArrays)`, 4},
{`min(NestedIntArrays)`, 1},
{`max(NestedAnyArrays)`, 4},
{`min(NestedAnyArrays)`, 1},
{`max(MixedNestedArray)`, 5.0},
{`min(MixedNestedArray)`, 1},
{`max(ArrayOfInt32)`, int32(3)},
{`min(ArrayOfInt32)`, int32(1)},
{`max(NestedInt32Array)`, int32(4)},
{`min(NestedInt32Array)`, int32(1)},
{`sum(1..9)`, 45},
{`sum([.5, 1.5, 2.5])`, 4.5},
{`sum([])`, 0},
Expand All @@ -97,6 +121,13 @@ func TestBuiltin(t *testing.T) {
{`mean(10, [1, 2, 3], 1..9)`, 4.6923076923076925},
{`mean(-10, [1, 2, 3, 4])`, 0.0},
{`mean(10.9, 1..9)`, 5.59},
{`mean(ArrayOfInt)`, 2.0},
{`mean(ArrayOfFloat)`, 2.5},
{`mean(NestedIntArrays)`, 2.5},
{`mean(NestedAnyArrays)`, 2.5},
{`mean(MixedNestedArray)`, 3.0},
{`mean(ArrayOfInt32)`, 2.0},
{`mean(NestedInt32Array)`, 2.5},
{`median(1..9)`, 5.0},
{`median([.5, 1.5, 2.5])`, 1.5},
{`median([])`, 0.0},
Expand All @@ -105,6 +136,13 @@ func TestBuiltin(t *testing.T) {
{`median(10, [1, 2, 3], 1..9)`, 4.0},
{`median(-10, [1, 2, 3, 4])`, 2.0},
{`median(1..5, 4.9)`, 3.5},
{`median(ArrayOfInt)`, 2.0},
{`median(ArrayOfFloat)`, 2.5},
{`median(NestedIntArrays)`, 2.5},
{`median(NestedAnyArrays)`, 2.5},
{`median(MixedNestedArray)`, 3.0},
{`median(ArrayOfInt32)`, 2.0},
{`median(NestedInt32Array)`, 2.5},
{`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"},
{`fromJSON("[1, 2, 3]")`, []any{1.0, 2.0, 3.0}},
{`toBase64("hello")`, "aGVsbG8="},
Expand Down
181 changes: 180 additions & 1 deletion builtin/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,75 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e
}
var val any
for _, arg := range args {
// Fast paths for common typed slices - avoid reflection and allocations
switch arr := arg.(type) {
case []int:
if len(arr) == 0 {
continue
}
m := arr[0]
for i := 1; i < len(arr); i++ {
if fn(m, arr[i]) {
m = arr[i]
}
}
if val == nil || fn(val, m) {
val = m
}
continue
case []float64:
if len(arr) == 0 {
continue
}
m := arr[0]
for i := 1; i < len(arr); i++ {
if fn(m, arr[i]) {
m = arr[i]
}
}
if val == nil || fn(val, m) {
val = m
}
continue
case []any:
// Fast path for []any with simple numeric types
for _, elem := range arr {
switch e := elem.(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64:
if val == nil || fn(val, e) {
val = e
}
case []int, []float64, []any:
// Nested array - recurse
nested, err := minMax(name, fn, depth+1, e)
if err != nil {
return nil, err
}
if nested != nil && (val == nil || fn(val, nested)) {
val = nested
}
default:
// Could be another slice type, use reflection
rv := reflect.ValueOf(e)
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
nested, err := minMax(name, fn, depth+1, e)
if err != nil {
return nil, err
}
if nested != nil && (val == nil || fn(val, nested)) {
val = nested
}
} else {
return nil, fmt.Errorf("invalid argument for %s (type %T)", name, e)
}
}
}
continue
}

// Slow path: use reflection for other types
rv := reflect.ValueOf(arg)
switch rv.Kind() {
case reflect.Array, reflect.Slice:
Expand All @@ -278,7 +347,6 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e
default:
return nil, fmt.Errorf("invalid argument for %s (type %T)", name, elemVal)
}

}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
Expand All @@ -305,6 +373,67 @@ func mean(depth int, args ...any) (int, float64, error) {
var count int

for _, arg := range args {
// Fast paths for common typed slices - avoid reflection and allocations
switch arr := arg.(type) {
case []int:
for _, v := range arr {
total += float64(v)
}
count += len(arr)
continue
case []float64:
for _, v := range arr {
total += v
}
count += len(arr)
continue
case []any:
// Fast path for []any - single pass without recursive calls for flat arrays
for _, elem := range arr {
switch e := elem.(type) {
case int:
total += float64(e)
count++
case float64:
total += e
count++
case []int, []float64, []any:
// Nested array - recurse
nestedCount, nestedSum, err := mean(depth+1, e)
if err != nil {
return 0, 0, err
}
total += nestedSum
count += nestedCount
default:
// Other numeric types or slices - use reflection
rv := reflect.ValueOf(e)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
total += float64(rv.Int())
count++
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
total += float64(rv.Uint())
count++
case reflect.Float32, reflect.Float64:
total += rv.Float()
count++
case reflect.Slice, reflect.Array:
nestedCount, nestedSum, err := mean(depth+1, e)
if err != nil {
return 0, 0, err
}
total += nestedSum
count += nestedCount
default:
return 0, 0, fmt.Errorf("invalid argument for mean (type %T)", e)
}
}
}
continue
}

// Slow path: use reflection for other types
rv := reflect.ValueOf(arg)
switch rv.Kind() {
case reflect.Array, reflect.Slice:
Expand Down Expand Up @@ -340,6 +469,56 @@ func median(depth int, args ...any) ([]float64, error) {
var values []float64

for _, arg := range args {
// Fast paths for common typed slices - avoid reflection and allocations
switch arr := arg.(type) {
case []int:
for _, v := range arr {
values = append(values, float64(v))
}
continue
case []float64:
values = append(values, arr...)
continue
case []any:
// Fast path for []any - single pass without recursive calls for flat arrays
for _, elem := range arr {
switch e := elem.(type) {
case int:
values = append(values, float64(e))
case float64:
values = append(values, e)
case []int, []float64, []any:
// Nested array - recurse
elems, err := median(depth+1, e)
if err != nil {
return nil, err
}
values = append(values, elems...)
default:
// Other numeric types or slices - use reflection
rv := reflect.ValueOf(e)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
values = append(values, float64(rv.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
values = append(values, float64(rv.Uint()))
case reflect.Float32, reflect.Float64:
values = append(values, rv.Float())
case reflect.Slice, reflect.Array:
elems, err := median(depth+1, e)
if err != nil {
return nil, err
}
values = append(values, elems...)
default:
return nil, fmt.Errorf("invalid argument for median (type %T)", e)
}
}
}
continue
}

// Slow path: use reflection for other types
rv := reflect.ValueOf(arg)
switch rv.Kind() {
case reflect.Array, reflect.Slice:
Expand Down
Loading