Compare commits

...

3 commits

Author SHA1 Message Date
Leon Mika 6cd3e9ff0f Merge remote-tracking branch 'github/main'
All checks were successful
Build / build (push) Successful in 1m44s
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 {
Names []string `parser:"LC NL? (PIPE @Ident+ PIPE NL?)?"`
Statements []*astStatements `parser:"@@ NL? RC"`
Names []string `parser:"LC NL* (PIPE @Ident+ PIPE NL*)?"`
Statements []*astStatements `parser:"@@? NL* RC"`
}
type astMaybeSub struct {

View file

@ -110,6 +110,8 @@ func (b boolObject) Truthy() bool {
func toGoValue(obj object) (interface{}, bool) {
switch v := obj.(type) {
case OpaqueObject:
return v.v, true
case nil:
return nil, true
case strObject:
@ -139,9 +141,9 @@ func toGoValue(obj object) (interface{}, bool) {
case proxyObject:
return v.p, true
case listableProxyObject:
return v.v.Interface(), true
return v.orig.Interface(), true
case structProxyObject:
return v.v.Interface(), true
return v.orig.Interface(), true
}
return nil, false
@ -149,6 +151,8 @@ func toGoValue(obj object) (interface{}, bool) {
func fromGoValue(v any) (object, error) {
switch t := v.(type) {
case OpaqueObject:
return t, nil
case nil:
return nil, nil
case string:
@ -167,10 +171,17 @@ func fromGoReflectValue(resVal reflect.Value) (object, error) {
switch resVal.Kind() {
case reflect.Slice:
return listableProxyObject{resVal}, nil
return listableProxyObject{v: resVal, orig: resVal}, nil
case reflect.Struct:
return newStructProxyObject(resVal), nil
return newStructProxyObject(resVal, resVal), nil
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())
}
@ -407,7 +418,8 @@ func (p proxyObject) Truthy() bool {
}
type listableProxyObject struct {
v reflect.Value
v reflect.Value
orig reflect.Value
}
func (p listableProxyObject) String() string {
@ -431,14 +443,16 @@ func (p listableProxyObject) Index(i int) object {
}
type structProxyObject struct {
v reflect.Value
vf []reflect.StructField
v reflect.Value
orig reflect.Value
vf []reflect.StructField
}
func newStructProxyObject(v reflect.Value) structProxyObject {
func newStructProxyObject(v reflect.Value, orig reflect.Value) structProxyObject {
return structProxyObject{
v: v,
vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }),
v: v,
orig: orig,
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
}
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 {
isCont bool
ret object

View file

@ -394,6 +394,36 @@ func TestBuiltins_Return(t *testing.T) {
expr 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: `
proc greet {
echo "Hello"
@ -403,6 +433,27 @@ func TestBuiltins_Return(t *testing.T) {
greet
`, 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: `
proc greet {
return "Hello, world"
@ -411,6 +462,7 @@ func TestBuiltins_Return(t *testing.T) {
greet
`, want: "Hello, world\n"},
{desc: "only return current frame", expr: `
proc greetWhat {
echo "Greet the"

View file

@ -125,6 +125,23 @@ func (ca CallArgs) bindArg(v interface{}, arg object) error {
}
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:
return bindProxyObject(v, reflect.ValueOf(t.p))
case listableProxyObject:
@ -146,6 +163,18 @@ func canBindArg(v interface{}, arg object) bool {
}
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:
return canBindProxyObject(v, reflect.ValueOf(t.p))
case listableProxyObject:

View file

@ -3,6 +3,7 @@ package ucl_test
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"testing"
@ -114,6 +115,27 @@ func TestInst_SetBuiltin(t *testing.T) {
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) {
type pair struct {
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) {