From 58195738ba283a12ae64fb91ec6509be52f730c1 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 17 Jun 2025 13:37:52 +0200 Subject: [PATCH] Fix bugs with binding to Opaque objects and pointers --- ucl/objs.go | 2 + ucl/userbuiltin.go | 10 ++-- ucl/userbuiltin_test.go | 101 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/ucl/objs.go b/ucl/objs.go index 2ecde0f..b2ddc6a 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -263,6 +263,8 @@ func toGoValue(obj Object) (interface{}, bool) { return v, true case proxyObject: return v.p, true + case OpaqueObject: + return v.v, true case listableProxyObject: return v.orig.Interface(), true case structProxyObject: diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 83d72d6..d915eea 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -163,10 +163,12 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error { switch t := arg.(type) { case proxyObject: return bindProxyObject(v, reflect.ValueOf(t.p)) + case OpaqueObject: + return bindProxyObject(v, reflect.ValueOf(t.v)) case listableProxyObject: - return bindProxyObject(v, t.v) + return bindProxyObject(v, t.orig) case structProxyObject: - return bindProxyObject(v, t.v) + return bindProxyObject(v, t.orig) } return bindProxyObject(v, reflect.ValueOf(arg)) @@ -185,9 +187,9 @@ func canBindArg(v interface{}, arg Object) bool { case proxyObject: return canBindProxyObject(v, reflect.ValueOf(t.p)) case listableProxyObject: - return canBindProxyObject(v, t.v) + return canBindProxyObject(v, t.orig) case structProxyObject: - return canBindProxyObject(v, t.v) + return canBindProxyObject(v, t.orig) } return true diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 5e3fe30..2e2436d 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "testing" + "ucl.lmika.dev/ucl" "github.com/stretchr/testify/assert" @@ -29,6 +30,59 @@ func TestInst_SetBuiltin(t *testing.T) { assert.Equal(t, "Hello, World", res) }) + t.Run("simple builtin accepting and returning pointers", func(t *testing.T) { + type point struct { + x, y int + } + + tests := []struct { + descr string + expr string + want string + }{ + {descr: "pass via args", expr: `vec 1 2 | vadd`, want: "3"}, + {descr: "pass via vars", expr: `x = (vec 2 3) ; vadd $x`, want: "5"}, + {descr: "pass twice", expr: `vadd (vadd2 (vec 1 2) (vec 3 4))`, want: "10"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("vec", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x, y int + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return &point{x, y}, nil + }) + inst.SetBuiltin("vadd", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var v *point + + if err := args.Bind(&v); err != nil { + return nil, err + } + + return v.x + v.y, nil + }) + inst.SetBuiltin("vadd2", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var v, u *point + + if err := args.Bind(&v, &u); err != nil { + return nil, err + } + + return &point{v.x + u.x, v.y + u.y}, nil + }) + + res, err := inst.EvalString(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, fmt.Sprint(res)) + }) + } + }) + t.Run("bind shift arguments", func(t *testing.T) { inst := ucl.New() inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { @@ -105,7 +159,7 @@ func TestInst_SetBuiltin(t *testing.T) { return nil, err } - return pair{x, y}, nil + return ucl.Opaque(pair{x, y}), nil }) res, err := inst.EvalString(context.Background(), `add2 "Hello" "World"`) @@ -137,7 +191,7 @@ func TestInst_SetBuiltin(t *testing.T) { return nil, err } - return pair{x, y}, nil + return ucl.Opaque(pair{x, y}), nil }) inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { var x pair @@ -156,6 +210,49 @@ func TestInst_SetBuiltin(t *testing.T) { } }) + t.Run("builtin operating on and returning proxy object for pointers", func(t *testing.T) { + type pair struct { + x, y string + } + + tests := []struct { + descr string + expr string + want string + }{ + {descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"}, + {descr: "pass via vars", expr: `x = (add2 "blue" "green") ; join $x`, want: "blue:green"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return ucl.Opaque(&pair{x, y}), nil + }) + inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x *pair + + if err := args.Bind(&x); err != nil { + return nil, err + } + + return x.x + ":" + x.y, nil + }) + + res, err := inst.EvalString(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } + }) + t.Run("slices returned by commands treated as lists", func(t *testing.T) { tests := []struct { descr string