Skip to content

Commit

Permalink
Add concat() builtin (expr-lang#565)
Browse files Browse the repository at this point in the history
* Add concat() builtin

* Use pkg deref in builtin

* Use pkg deref in checker

* Use pkg deref in types_table.go

* Use pkg deref in docgen.go

* Use pkg deref in vm
  • Loading branch information
antonmedv authored Feb 16, 2024
1 parent 36f9adb commit b9f96a0
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 149 deletions.
54 changes: 49 additions & 5 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/expr-lang/expr/internal/deref"
"github.com/expr-lang/expr/vm/runtime"
)

Expand Down Expand Up @@ -440,7 +441,7 @@ var Builtins = []*Function{
sum := int64(0)
i := 0
for ; i < v.Len(); i++ {
it := deref(v.Index(i))
it := deref.Value(v.Index(i))
if it.CanInt() {
sum += it.Int()
} else if it.CanFloat() {
Expand All @@ -453,7 +454,7 @@ var Builtins = []*Function{
float:
fSum := float64(sum)
for ; i < v.Len(); i++ {
it := deref(v.Index(i))
it := deref.Value(v.Index(i))
if it.CanInt() {
fSum += float64(it.Int())
} else if it.CanFloat() {
Expand Down Expand Up @@ -492,7 +493,7 @@ var Builtins = []*Function{
sum := float64(0)
i := 0
for ; i < v.Len(); i++ {
it := deref(v.Index(i))
it := deref.Value(v.Index(i))
if it.CanInt() {
sum += float64(it.Int())
} else if it.CanFloat() {
Expand Down Expand Up @@ -530,7 +531,7 @@ var Builtins = []*Function{
}
s := make([]float64, v.Len())
for i := 0; i < v.Len(); i++ {
it := deref(v.Index(i))
it := deref.Value(v.Index(i))
if it.CanInt() {
s[i] = float64(it.Int())
} else if it.CanFloat() {
Expand Down Expand Up @@ -850,7 +851,7 @@ var Builtins = []*Function{
}
out := reflect.MakeMap(mapType)
for i := 0; i < v.Len(); i++ {
pair := deref(v.Index(i))
pair := deref.Value(v.Index(i))
if pair.Kind() != reflect.Array && pair.Kind() != reflect.Slice {
return nil, fmt.Errorf("invalid pair %v", pair)
}
Expand Down Expand Up @@ -908,6 +909,49 @@ var Builtins = []*Function{
}
},
},
{
Name: "concat",
Safe: func(args ...any) (any, uint, error) {
if len(args) == 0 {
return nil, 0, fmt.Errorf("invalid number of arguments (expected at least 1, got 0)")
}

var size uint
var arr []any

for _, arg := range args {
v := reflect.ValueOf(deref.Deref(arg))

if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil, 0, fmt.Errorf("cannot concat %s", v.Kind())
}

size += uint(v.Len())

for i := 0; i < v.Len(); i++ {
item := v.Index(i)
arr = append(arr, item.Interface())
}
}

return arr, size, nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) == 0 {
return anyType, fmt.Errorf("invalid number of arguments (expected at least 1, got 0)")
}

for _, arg := range args {
switch kind(deref.Type(arg)) {
case reflect.Interface, reflect.Slice, reflect.Array:
default:
return anyType, fmt.Errorf("cannot concat %s", arg)
}
}

return arrayType, nil
},
},
{
Name: "sort",
Safe: func(args ...any) (any, uint, error) {
Expand Down
12 changes: 8 additions & 4 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ 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"}},
"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,
}

var tests = []struct {
Expand Down Expand Up @@ -130,6 +132,8 @@ func TestBuiltin(t *testing.T) {
{`reduce(1..9, # + #acc)`, 45},
{`reduce([.5, 1.5, 2.5], # + #acc, 0)`, 4.5},
{`reduce([], 5, 0)`, 0},
{`concat(ArrayOfString, ArrayOfInt)`, []any{"foo", "bar", "baz", 1, 2, 3}},
{`concat(PtrArrayWithNil, [nil])`, []any{42, nil}},
}

for _, test := range tests {
Expand Down
29 changes: 0 additions & 29 deletions builtin/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,6 @@ func types(types ...any) []reflect.Type {
return ts
}

func deref(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface {
if v.IsNil() {
return v
}
v = v.Elem()
}

loop:
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return v
}
indirect := reflect.Indirect(v)
switch indirect.Kind() {
case reflect.Struct, reflect.Map, reflect.Array, reflect.Slice:
break loop
default:
v = v.Elem()
}
}

if v.IsValid() {
return v
}

panic(fmt.Sprintf("cannot deref %s", v))
}

func toInt(val any) (int, error) {
switch v := val.(type) {
case int:
Expand Down
8 changes: 4 additions & 4 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/internal/deref"
"github.com/expr-lang/expr/parser"
)

Expand Down Expand Up @@ -203,8 +204,7 @@ func (v *checker) ConstantNode(node *ast.ConstantNode) (reflect.Type, info) {

func (v *checker) UnaryNode(node *ast.UnaryNode) (reflect.Type, info) {
t, _ := v.visit(node.Node)

t = deref(t)
t = deref.Type(t)

switch node.Operator {

Expand Down Expand Up @@ -235,8 +235,8 @@ func (v *checker) BinaryNode(node *ast.BinaryNode) (reflect.Type, info) {
l, _ := v.visit(node.Left)
r, ri := v.visit(node.Right)

l = deref(l)
r = deref(r)
l = deref.Type(l)
r = deref.Type(r)

// check operator overloading
if fns, ok := v.config.Operators[node.Operator]; ok {
Expand Down
19 changes: 0 additions & 19 deletions checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,25 +205,6 @@ func fetchField(t reflect.Type, name string) (reflect.StructField, bool) {
return reflect.StructField{}, false
}

func deref(t reflect.Type) reflect.Type {
if t == nil {
return nil
}
if t.Kind() == reflect.Interface {
return t
}
for t != nil && t.Kind() == reflect.Ptr {
e := t.Elem()
switch e.Kind() {
case reflect.Struct, reflect.Map, reflect.Array, reflect.Slice:
return t
default:
t = e
}
}
return t
}

func kind(t reflect.Type) reflect.Kind {
if t == nil {
return reflect.Invalid
Expand Down
21 changes: 3 additions & 18 deletions conf/types_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package conf

import (
"reflect"

"github.com/expr-lang/expr/internal/deref"
)

type Tag struct {
Expand Down Expand Up @@ -77,7 +79,7 @@ func CreateTypesTable(i any) TypesTable {

func FieldsFromStruct(t reflect.Type) TypesTable {
types := make(TypesTable)
t = dereference(t)
t = deref.Type(t)
if t == nil {
return types
}
Expand Down Expand Up @@ -111,23 +113,6 @@ func FieldsFromStruct(t reflect.Type) TypesTable {
return types
}

func dereference(t reflect.Type) reflect.Type {
if t == nil {
return nil
}
if t.Kind() == reflect.Ptr {
t = dereference(t.Elem())
}
return t
}

func kind(t reflect.Type) reflect.Kind {
if t == nil {
return reflect.Invalid
}
return t.Kind()
}

func FieldName(field reflect.StructField) string {
if taggedName := field.Tag.Get("expr"); taggedName != "" {
return taggedName
Expand Down
15 changes: 3 additions & 12 deletions docgen/docgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/internal/deref"
)

// Kind can be any of array, map, struct, func, string, int, float, bool or any.
Expand Down Expand Up @@ -80,7 +81,7 @@ func CreateDoc(i any) *Context {
c := &Context{
Variables: make(map[Identifier]*Type),
Types: make(map[TypeName]*Type),
PkgPath: dereference(reflect.TypeOf(i)).PkgPath(),
PkgPath: deref.Type(reflect.TypeOf(i)).PkgPath(),
}

for name, t := range conf.CreateTypesTable(i) {
Expand Down Expand Up @@ -134,7 +135,7 @@ func (c *Context) use(t reflect.Type, ops ...option) *Type {
methods = append(methods, m)
}

t = dereference(t)
t = deref.Type(t)

// Only named types will have methods defined on them.
// It maybe not even struct, but we gonna call then
Expand Down Expand Up @@ -253,13 +254,3 @@ func isPrivate(s string) bool {
func isProtobuf(s string) bool {
return strings.HasPrefix(s, "XXX_")
}

func dereference(t reflect.Type) reflect.Type {
if t == nil {
return nil
}
if t.Kind() == reflect.Ptr {
t = dereference(t.Elem())
}
return t
}
29 changes: 7 additions & 22 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,9 @@ func ExampleOperator_Decimal() {
code := `A + B - C`

type Env struct {
A, B, C Decimal
Sub func(a, b Decimal) Decimal
Add func(a, b Decimal) Decimal
A, B, C Decimal
Sub func(a, b Decimal) Decimal
Add func(a, b Decimal) Decimal
}

options := []expr.Option{
Expand All @@ -334,11 +334,11 @@ func ExampleOperator_Decimal() {
}

env := Env{
A: Decimal{3},
B: Decimal{2},
C: Decimal{1},
A: Decimal{3},
B: Decimal{2},
C: Decimal{1},
Sub: func(a, b Decimal) Decimal { return Decimal{a.N - b.N} },
Add: func(a, b Decimal) Decimal { return Decimal{a.N + b.N} },
Add: func(a, b Decimal) Decimal { return Decimal{a.N + b.N} },
}

output, err := expr.Run(program, env)
Expand Down Expand Up @@ -1358,21 +1358,6 @@ func TestExpr_fetch_from_func(t *testing.T) {
assert.Contains(t, err.Error(), "cannot fetch Value from func()")
}

func TestExpr_fetch_from_interface(t *testing.T) {
type FooBar struct {
Value string
}
foobar := &FooBar{"waldo"}
var foobarAny any = foobar
var foobarPtrAny any = &foobarAny

res, err := expr.Eval("foo.Value", map[string]any{
"foo": foobarPtrAny,
})
assert.NoError(t, err)
assert.Equal(t, "waldo", res)
}

func TestExpr_map_default_values(t *testing.T) {
env := map[string]any{
"foo": map[string]string{},
Expand Down
Loading

0 comments on commit b9f96a0

Please sign in to comment.