diff --git a/ucl/ast.go b/ucl/ast.go index 54aaa64..719f381 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -98,6 +98,7 @@ type astDotSuffix struct { } type astDot struct { + Pos lexer.Position Arg astCmdArg `parser:"@@"` DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` } diff --git a/ucl/builtins.go b/ucl/builtins.go index cc946b3..0c84824 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -6,6 +6,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/alecthomas/participle/v2/lexer" ) func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -509,7 +511,10 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return IntObject(0), nil } -func indexLookup(ctx context.Context, obj, elem Object) (Object, error) { +func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Object, error) { + if obj == nil { + return nil, nil + } switch v := obj.(type) { case Listable: intIdx, ok := elem.(IntObject) @@ -525,9 +530,11 @@ func indexLookup(ctx context.Context, obj, elem Object) (Object, error) { case Hashable: strIdx, ok := elem.(StringObject) if !ok { - return nil, errors.New("expected string for Hashable") + return nil, nil } return v.Value(string(strIdx)), nil + default: + return nil, notIndexableError(pos) } return nil, nil } @@ -539,7 +546,7 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { val := args.args[0] for _, idx := range args.args[1:] { - newVal, err := indexLookup(ctx, val, idx) + newVal, err := indexLookup(ctx, val, idx, lexer.Position{}) if err != nil { return nil, err } diff --git a/ucl/builtins_test.go b/ucl/builtins_test.go index 9361ae4..effb183 100644 --- a/ucl/builtins_test.go +++ b/ucl/builtins_test.go @@ -5,9 +5,10 @@ import ( "context" "errors" "fmt" - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/stretchr/testify/assert" ) type testIterator struct { @@ -57,6 +58,19 @@ func WithTestBuiltin() InstOption { return &a, nil })) + i.rootEC.addCmd("rearrange", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { + var as ListObject = make([]Object, 0) + + for _, a := range args.args { + vs, ok := args.kwargs[a.String()] + if ok { + as = append(as, vs.Index(0)) + } + } + + return &as, nil + })) + i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return nil, errors.New("an error occurred") @@ -131,10 +145,10 @@ func TestBuiltins_Echo(t *testing.T) { echo "world" # command after this ; `, want: "Hello\nworld\n"}, - {desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"}, - {desc: "interpolated string 2", expr: `$what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"}, + {desc: "interpolated string 1", expr: `what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"}, + {desc: "interpolated string 2", expr: `what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"}, {desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"}, - {desc: "interpolated string 4", expr: `$what = "Hello" ; $where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, + {desc: "interpolated string 4", expr: `what = "Hello" ; where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, {desc: "interpolated string 5", expr: ` for [123 "foo" true ()] { |x| echo "[[$x]]" @@ -167,19 +181,19 @@ func TestBuiltins_If(t *testing.T) { want string }{ {desc: "single then", expr: ` - $x = "Hello" + x = "Hello" if $x { echo "true" }`, want: "true\n(nil)\n"}, {desc: "single then and else", expr: ` - $x = "Hello" + x = "Hello" if $x { echo "true" } else { echo "false" }`, want: "true\n(nil)\n"}, {desc: "single then, elif and else", expr: ` - $x = "Hello" + x = "Hello" if $y { echo "y is true" } elif $x { @@ -188,14 +202,14 @@ func TestBuiltins_If(t *testing.T) { echo "nothings x" }`, want: "x is true\n(nil)\n"}, {desc: "single then and elif, no else", expr: ` - $x = "Hello" + x = "Hello" if $y { echo "y is true" } elif $x { echo "x is true" }`, want: "x is true\n(nil)\n"}, {desc: "single then, two elif, and else", expr: ` - $x = "Hello" + x = "Hello" if $z { echo "z is true" } elif $y { @@ -213,15 +227,15 @@ func TestBuiltins_If(t *testing.T) { } else { echo "none is true" }`, want: "none is true\n(nil)\n"}, - {desc: "compressed then", expr: `$x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, + {desc: "compressed then", expr: `x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, - {desc: "if of itr 1", expr: `$i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 2", expr: `$i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 3", expr: `$i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, - {desc: "if of itr 4", expr: `$i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 5", expr: `$i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, - {desc: "if of itr 6", expr: `$i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 1", expr: `i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 2", expr: `i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 3", expr: `i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, + {desc: "if of itr 4", expr: `i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 5", expr: `i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, + {desc: "if of itr 6", expr: `i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, } for _, tt := range tests { @@ -508,6 +522,9 @@ func TestBuiltins_Procs(t *testing.T) { echo (call $er ["xxx"]) echo (call $er ["yyy"]) `, want: "Xxxx\nXxxxyyy\n(nil)\n"}, + {desc: "calling with kwargs", expr: ` + echo (call rearrange [b a] [a:"ey" b:"bee"]) + `, want: "[bee ey]\n(nil)\n"}, } for _, tt := range tests { diff --git a/ucl/errors.go b/ucl/errors.go index a948b03..f7f80e9 100644 --- a/ucl/errors.go +++ b/ucl/errors.go @@ -3,6 +3,7 @@ package ucl import ( "errors" "fmt" + "github.com/alecthomas/participle/v2/lexer" ) @@ -12,6 +13,7 @@ var ( var ( tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") + notIndexableError = newBadUsage("index only support on lists and hashes") ) type errorWithPos struct { diff --git a/ucl/eval.go b/ucl/eval.go index b2edca2..82313df 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "strings" + + "github.com/alecthomas/participle/v2/lexer" ) type evaluator struct { @@ -205,7 +207,7 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, } } - res, err = indexLookup(ctx, res, idx) + res, err = indexLookup(ctx, res, idx, n.Pos) if err != nil { return nil, err } @@ -258,9 +260,11 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) { switch { + case n.Ident != nil: + ec.setOrDefineVar(n.Ident.String(), toVal) + return toVal, nil case n.Literal != nil: - // We may use this for variable setting? - return nil, errors.New("cannot assign to a literal") + return nil, errors.New("cannot assign to a literal value") case n.Var != nil: ec.setOrDefineVar(*n.Var, toVal) return toVal, nil @@ -428,7 +432,7 @@ func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *ast } } - res, err = indexLookup(ctx, res, idx) + res, err = indexLookup(ctx, res, idx, lexer.Position{}) if err != nil { return "", err } diff --git a/ucl/inst_test.go b/ucl/inst_test.go index e2dbf60..718d0b7 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -4,19 +4,22 @@ import ( "bytes" "context" "strings" + "ucl.lmika.dev/ucl" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestInst_Eval(t *testing.T) { tests := []struct { - desc string - expr string - want any - wantObj bool - wantErr error + desc string + expr string + want any + wantObj bool + wantAnErr bool + wantErr error }{ {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple int 1", expr: `firstarg 123`, want: 123}, @@ -100,7 +103,14 @@ func TestInst_Eval(t *testing.T) { {desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, {desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, {desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, - {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil}, + {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; $x.y`, want: nil}, + + {desc: "dot err 1", expr: `$x = [1 2 3] ; $x.Hello`, want: nil}, + {desc: "dot err 2", expr: `$x = [1 2 3] ; $x.("world")`, want: nil}, + {desc: "dot err 4", expr: `$x = [a:1 b:2] ; $x.(5)`, want: nil}, + {desc: "dot err 3", expr: `$x = [a:1 b:2] ; $x.(0)`, want: nil}, + {desc: "dot err 5", expr: `$x = 123 ; $x.(5)`, wantAnErr: true}, + {desc: "dot err 6", expr: `$x = 123 ; $x.Five`, wantAnErr: true}, {desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil}, {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, @@ -118,6 +128,8 @@ func TestInst_Eval(t *testing.T) { if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) + } else if tt.wantAnErr { + assert.Error(t, err) } else if tt.wantObj { assert.NoError(t, err) _, isObj := res.(ucl.Object)