Compare commits

...

3 Commits

Author SHA1 Message Date
Leon Mika 6cd3e9ff0f Merge remote-tracking branch 'github/main'
Build / build (push) Successful in 1m44s Details
2024-09-02 09:17:21 +10:00
Leon Mika 94cb920c51 Fixed a number of bugs
- Fixed the non-opaque slice and struct types returned from builtins as pointers to be returned from eval as pointers
- Fixed syntax problems with empty blocks that contain new-lines
2024-06-05 11:30:45 +00:00
Leon Mika 88417aba95 Added support for "opaque" return types
These are return types that are left alone by the UCL interpretor and cannot be operated on or get fields of.
2024-06-05 03:59:53 +00:00
5 changed files with 252 additions and 12 deletions

View File

@ -40,8 +40,8 @@ type astListOrHash struct {
} }
type astBlock struct { type astBlock struct {
Names []string `parser:"LC NL? (PIPE @Ident+ PIPE NL?)?"` Names []string `parser:"LC NL* (PIPE @Ident+ PIPE NL*)?"`
Statements []*astStatements `parser:"@@ NL? RC"` Statements []*astStatements `parser:"@@? NL* RC"`
} }
type astMaybeSub struct { type astMaybeSub struct {

View File

@ -110,6 +110,8 @@ func (b boolObject) Truthy() bool {
func toGoValue(obj object) (interface{}, bool) { func toGoValue(obj object) (interface{}, bool) {
switch v := obj.(type) { switch v := obj.(type) {
case OpaqueObject:
return v.v, true
case nil: case nil:
return nil, true return nil, true
case strObject: case strObject:
@ -139,9 +141,9 @@ func toGoValue(obj object) (interface{}, bool) {
case proxyObject: case proxyObject:
return v.p, true return v.p, true
case listableProxyObject: case listableProxyObject:
return v.v.Interface(), true return v.orig.Interface(), true
case structProxyObject: case structProxyObject:
return v.v.Interface(), true return v.orig.Interface(), true
} }
return nil, false return nil, false
@ -149,6 +151,8 @@ func toGoValue(obj object) (interface{}, bool) {
func fromGoValue(v any) (object, error) { func fromGoValue(v any) (object, error) {
switch t := v.(type) { switch t := v.(type) {
case OpaqueObject:
return t, nil
case nil: case nil:
return nil, nil return nil, nil
case string: case string:
@ -167,10 +171,17 @@ func fromGoReflectValue(resVal reflect.Value) (object, error) {
switch resVal.Kind() { switch resVal.Kind() {
case reflect.Slice: case reflect.Slice:
return listableProxyObject{resVal}, nil return listableProxyObject{v: resVal, orig: resVal}, nil
case reflect.Struct: case reflect.Struct:
return newStructProxyObject(resVal), nil return newStructProxyObject(resVal, resVal), nil
case reflect.Pointer: case reflect.Pointer:
switch resVal.Elem().Kind() {
case reflect.Slice:
return listableProxyObject{v: resVal.Elem(), orig: resVal}, nil
case reflect.Struct:
return newStructProxyObject(resVal.Elem(), resVal), nil
}
return fromGoReflectValue(resVal.Elem()) return fromGoReflectValue(resVal.Elem())
} }
@ -408,6 +419,7 @@ func (p proxyObject) Truthy() bool {
type listableProxyObject struct { type listableProxyObject struct {
v reflect.Value v reflect.Value
orig reflect.Value
} }
func (p listableProxyObject) String() string { func (p listableProxyObject) String() string {
@ -432,12 +444,14 @@ func (p listableProxyObject) Index(i int) object {
type structProxyObject struct { type structProxyObject struct {
v reflect.Value v reflect.Value
orig reflect.Value
vf []reflect.StructField vf []reflect.StructField
} }
func newStructProxyObject(v reflect.Value) structProxyObject { func newStructProxyObject(v reflect.Value, orig reflect.Value) structProxyObject {
return structProxyObject{ return structProxyObject{
v: v, v: v,
orig: orig,
vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }), vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }),
} }
} }
@ -488,6 +502,22 @@ func (s structProxyObject) Each(fn func(k string, v object) error) error {
return nil return nil
} }
type OpaqueObject struct {
v any
}
func Opaque(v any) OpaqueObject {
return OpaqueObject{v: v}
}
func (p OpaqueObject) String() string {
return fmt.Sprintf("opaque{%T}", p.v)
}
func (p OpaqueObject) Truthy() bool {
return p.v != nil
}
type errBreak struct { type errBreak struct {
isCont bool isCont bool
ret object ret object

View File

@ -394,6 +394,36 @@ func TestBuiltins_Return(t *testing.T) {
expr string expr string
want string want string
}{ }{
// syntax tests
{desc: "empty proc 1", expr: `
proc greet {}
greet
`, want: "(nil)\n"},
{desc: "empty proc 2", expr: `
proc greet {
}
greet
`, want: "(nil)\n"},
{desc: "empty proc 3", expr: `
proc greet {
}
greet
`, want: "(nil)\n"},
{desc: "empty proc 4", expr: `
proc greet {
# bla
# di
# bla!
}
greet
`, want: "(nil)\n"},
{desc: "nil return", expr: ` {desc: "nil return", expr: `
proc greet { proc greet {
echo "Hello" echo "Hello"
@ -403,6 +433,27 @@ func TestBuiltins_Return(t *testing.T) {
greet greet
`, want: "Hello\n(nil)\n"}, `, want: "Hello\n(nil)\n"},
{desc: "simple arg 1", expr: `
proc greet { |x|
return (cat "Hello, " $x)
}
greet "person"
`, want: "Hello, person\n"},
{desc: "simple arg 2", expr: `
proc greet {
# This will greet someone
# here are the args:
|x|
# And here is the code
return (cat "Hello, " $x)
}
greet "person"
`, want: "Hello, person\n"},
{desc: "simple return", expr: ` {desc: "simple return", expr: `
proc greet { proc greet {
return "Hello, world" return "Hello, world"
@ -411,6 +462,7 @@ func TestBuiltins_Return(t *testing.T) {
greet greet
`, want: "Hello, world\n"}, `, want: "Hello, world\n"},
{desc: "only return current frame", expr: ` {desc: "only return current frame", expr: `
proc greetWhat { proc greetWhat {
echo "Greet the" echo "Greet the"

View File

@ -125,6 +125,23 @@ func (ca CallArgs) bindArg(v interface{}, arg object) error {
} }
switch t := arg.(type) { switch t := arg.(type) {
case OpaqueObject:
if v == nil {
return errors.New("opaque object not bindable to nil")
}
vr := reflect.ValueOf(v)
tr := reflect.ValueOf(t.v)
if vr.Kind() != reflect.Pointer {
return errors.New("expected pointer for an opaque object bind")
}
if !tr.Type().AssignableTo(vr.Elem().Type()) {
return errors.New("opaque object not assignable to passed in value")
}
vr.Elem().Set(tr)
return nil
case proxyObject: case proxyObject:
return bindProxyObject(v, reflect.ValueOf(t.p)) return bindProxyObject(v, reflect.ValueOf(t.p))
case listableProxyObject: case listableProxyObject:
@ -146,6 +163,18 @@ func canBindArg(v interface{}, arg object) bool {
} }
switch t := arg.(type) { switch t := arg.(type) {
case OpaqueObject:
vr := reflect.ValueOf(v)
tr := reflect.ValueOf(t.v)
if vr.Kind() != reflect.Pointer {
return false
}
if !tr.Type().AssignableTo(vr.Elem().Type()) {
return false
}
return true
case proxyObject: case proxyObject:
return canBindProxyObject(v, reflect.ValueOf(t.p)) return canBindProxyObject(v, reflect.ValueOf(t.p))
case listableProxyObject: case listableProxyObject:

View File

@ -3,6 +3,7 @@ package ucl_test
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
@ -114,6 +115,27 @@ func TestInst_SetBuiltin(t *testing.T) {
assert.Equal(t, pair{"Hello", "World"}, res) assert.Equal(t, pair{"Hello", "World"}, res)
}) })
t.Run("builtin return proxy object ptr", func(t *testing.T) {
type pair struct {
x, y string
}
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 &pair{x, y}, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`)
assert.NoError(t, err)
assert.Equal(t, &pair{"Hello", "World"}, res)
})
t.Run("builtin operating on and returning proxy object", func(t *testing.T) { t.Run("builtin operating on and returning proxy object", func(t *testing.T) {
type pair struct { type pair struct {
x, y string x, y string
@ -184,6 +206,113 @@ func TestInst_SetBuiltin(t *testing.T) {
}) })
} }
}) })
t.Run("opaques returned as is", func(t *testing.T) {
type opaqueThingType struct {
x string
y string
z string
}
opaqueThing := &opaqueThingType{x: "do", y: "not", z: "touch"}
tests := []struct {
descr string
expr string
wantErr bool
}{
{descr: "return as is", expr: `getOpaque`, wantErr: false},
{descr: "carry around ok", expr: `set x (getOpaque) ; $x`, wantErr: false},
{descr: "iterate over", expr: `foreach (countTo3) { |x| echo $x }`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW))
inst.SetBuiltin("getOpaque", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return ucl.Opaque(opaqueThing), nil
})
res, err := inst.Eval(context.Background(), tt.expr)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Same(t, opaqueThing, res)
}
})
}
})
t.Run("operate on opaques", func(t *testing.T) {
type opaqueThingType struct {
x string
y string
z string
}
opaqueThing := &opaqueThingType{x: "do", y: "not", z: "touch"}
tests := []struct {
descr string
expr string
want opaqueThingType
wantErr bool
}{
{descr: "return as is", expr: `getOpaque`, want: *opaqueThing},
{descr: "update pointer 1", expr: `set x (getOpaque) ; setProp $x -x "do" -y "touch" -z "this"`, want: opaqueThingType{x: "do", y: "touch", z: "this"}},
{descr: "update pointer 2", expr: `set x (getOpaque) ; setProp $x -x "yes" ; setProp $x -y "this" -z "too"`, want: opaqueThingType{x: "yes", y: "this", z: "too"}},
{descr: "bad args", expr: `set x (getOpaque) ; setProp $t -x "yes" ; setProp $bla -y "this" -z "too"`, want: *opaqueThing, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW))
inst.SetBuiltin("getOpaque", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return ucl.Opaque(opaqueThing), nil
})
inst.SetBuiltin("setProp", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var o *opaqueThingType
if err := args.Bind(&o); err != nil {
return nil, err
} else if o == nil {
return nil, errors.New("is nil")
}
if args.HasSwitch("x") {
var s string
_ = args.BindSwitch("x", &s)
o.x = s
}
if args.HasSwitch("y") {
var s string
_ = args.BindSwitch("y", &s)
o.y = s
}
if args.HasSwitch("z") {
var s string
_ = args.BindSwitch("z", &s)
o.z = s
}
return nil, nil
})
_, err := inst.Eval(context.Background(), tt.expr)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, *opaqueThing)
}
})
}
})
} }
func TestCallArgs_Bind(t *testing.T) { func TestCallArgs_Bind(t *testing.T) {