diff --git a/ucl/ast.go b/ucl/ast.go index ab620a5..fd4252d 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -57,9 +57,19 @@ type astCmdArg struct { Block *astBlock `parser:"| @@"` } +type astDotSuffix struct { + KeyIdent *astIdentNames `parser:"@@"` + Pipeline *astPipeline `parser:"| LP @@ RP"` +} + +type astDot struct { + Arg astCmdArg `parser:"@@"` + DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` +} + type astCmd struct { - Name astCmdArg `parser:"@@"` - Args []astCmdArg `parser:"@@*"` + Name astDot `parser:"@@"` + Args []astDot `parser:"@@*"` } type astPipeline struct { @@ -84,6 +94,7 @@ var scanner = lexer.MustStateful(lexer.Rules{ {"Int", `[-]?[0-9][0-9]*`, nil}, {"DOLLAR", `\$`, nil}, {"COLON", `\:`, nil}, + {"DOT", `[.]`, nil}, {"LP", `\(`, nil}, {"RP", `\)`, nil}, {"LS", `\[`, nil}, diff --git a/ucl/builtins.go b/ucl/builtins.go index 04009a5..19e7754 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -158,6 +158,27 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(0), nil } +func indexLookup(ctx context.Context, obj, elem object) (object, error) { + switch v := obj.(type) { + case listable: + intIdx, ok := elem.(intObject) + if !ok { + return nil, nil + } + if int(intIdx) >= 0 && int(intIdx) < v.Len() { + return v.Index(int(intIdx)), nil + } + return nil, nil + case hashable: + strIdx, ok := elem.(strObject) + if !ok { + return nil, errors.New("expected string for hashable") + } + return v.Value(string(strIdx)), nil + } + return nil, nil +} + func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(1); err != nil { return nil, err @@ -165,26 +186,11 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { val := args.args[0] for _, idx := range args.args[1:] { - switch v := val.(type) { - case listable: - intIdx, ok := idx.(intObject) - if !ok { - return nil, nil - } - if int(intIdx) >= 0 && int(intIdx) < v.Len() { - val = v.Index(int(intIdx)) - } else { - val = nil - } - case hashable: - strIdx, ok := idx.(strObject) - if !ok { - return nil, errors.New("expected string for hashable") - } - val = v.Value(string(strIdx)) - default: - return val, nil + newVal, err := indexLookup(ctx, val, idx) + if err != nil { + return nil, err } + val = newVal } return val, nil diff --git a/ucl/eval.go b/ucl/eval.go index 33f5157..42b8bf0 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -71,8 +71,8 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object, ast *astCmd) (object, error) { switch { - case ast.Name.Ident != nil: - name := ast.Name.Ident.String() + case (ast.Name.Arg.Ident != nil) && len(ast.Name.DotSuffix) == 0: + name := ast.Name.Arg.Ident.String() // Regular command if cmd := ec.lookupInvokable(name); cmd != nil { @@ -85,7 +85,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object, return nil, errors.New("unknown command: " + name) } case len(ast.Args) > 0: - nameElem, err := e.evalArg(ctx, ec, ast.Name) + nameElem, err := e.evalDot(ctx, ec, ast.Name) if err != nil { return nil, err } @@ -98,7 +98,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object, return e.evalInvokable(ctx, ec, currentPipe, ast, inv) } - nameElem, err := e.evalArg(ctx, ec, ast.Name) + nameElem, err := e.evalDot(ctx, ec, ast.Name) if err != nil { return nil, err } @@ -117,7 +117,7 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe o argsPtr.Append(currentPipe) } for _, arg := range ast.Args { - if ident := arg.Ident; ident != nil && ident.String()[0] == '-' { + if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' { // Arg switch if kwargs == nil { kwargs = make(map[string]*listObject) @@ -126,7 +126,7 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe o argsPtr = &listObject{} kwargs[ident.String()[1:]] = argsPtr } else { - ae, err := e.evalArg(ctx, ec, arg) + ae, err := e.evalDot(ctx, ec, arg) if err != nil { return nil, err } @@ -148,6 +148,33 @@ func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pip }) } +func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (object, error) { + res, err := e.evalArg(ctx, ec, n.Arg) + if err != nil { + return nil, err + } else if len(n.DotSuffix) == 0 { + return res, nil + } + + for _, dot := range n.DotSuffix { + var idx object + if dot.KeyIdent != nil { + idx = strObject(dot.KeyIdent.String()) + } else { + idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) + if err != nil { + return nil, err + } + } + + res, err = indexLookup(ctx, res, idx) + if err != nil { + return nil, err + } + } + return res, nil +} + func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (object, error) { switch { case n.Literal != nil: diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 80a3837..e25e688 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -60,6 +60,22 @@ func TestInst_Eval(t *testing.T) { three:"3" ]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}}, {desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}}, + + // Dots + {desc: "dot 1", expr: `set x [1 2 3] ; $x.(0)`, want: 1}, + {desc: "dot 2", expr: `set x [1 2 3] ; $x.(1)`, want: 2}, + {desc: "dot 3", expr: `set x [1 2 3] ; $x.(2)`, want: 3}, + {desc: "dot 4", expr: `set x [1 2 3] ; $x.(3)`, want: nil}, + {desc: "dot 5", expr: `set x [1 2 3] ; $x.(add 1 1)`, want: 3}, + {desc: "dot 6", expr: `set x [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"}, + {desc: "dot 7", expr: `set x [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"}, + {desc: "dot 8", expr: `set x [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil}, + {desc: "dot 9", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"}, + {desc: "dot 10", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"}, + {desc: "dot 11", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, + {desc: "dot 12", expr: `set x [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, + {desc: "dot 13", expr: `set x [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, + {desc: "dot 14", expr: `set x [MORE:"stuff"] ; x.y`, want: nil}, } for _, tt := range tests { diff --git a/ucl/objs.go b/ucl/objs.go index 5a73720..80ab821 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -189,7 +189,11 @@ func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bo return false } - lit := ma.ast.Args[ma.argShift+n].Ident + if len(ma.ast.Args[ma.argShift+n].DotSuffix) != 0 { + return false + } + + lit := ma.ast.Args[ma.argShift+n].Arg.Ident if lit == nil { return false } @@ -202,7 +206,11 @@ func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) { return "", false } - lit := ma.ast.Args[ma.argShift].Ident + if len(ma.ast.Args[ma.argShift].DotSuffix) != 0 { + return "", false + } + + lit := ma.ast.Args[ma.argShift].Arg.Ident if lit != nil { ma.argShift += 1 return lit.String(), true @@ -215,7 +223,7 @@ func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) { return nil, errors.New("not enough arguments") // FIX } - return ma.eval.evalArg(ctx, ma.ec, ma.ast.Args[ma.argShift+n]) + return ma.eval.evalDot(ctx, ma.ec, ma.ast.Args[ma.argShift+n]) } func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushScope bool) (object, error) {