diff --git a/ucl/ast.go b/ucl/ast.go index 29478ab..250587d 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -86,6 +86,7 @@ type astCmdArg struct { Literal *astLiteral `parser:"@@"` Ident *astIdentNames `parser:"| @@"` Var *string `parser:"| DOLLAR @Ident"` + PseudoVar *string `parser:"| AT @Ident"` MaybeSub *astMaybeSub `parser:"| LP @@ RP"` ListOrHash *astListOrHash `parser:"| @@"` Block *astBlock `parser:"| @@"` @@ -129,6 +130,7 @@ var scanner = lexer.MustStateful(lexer.Rules{ {"SingleStringStart", `'`, lexer.Push("SingleString")}, {"Int", `[-]?[0-9][0-9]*`, nil}, {"DOLLAR", `\$`, nil}, + {"AT", `@`, nil}, {"COLON", `\:`, nil}, {"DOT", `[.]`, nil}, {"LP", `\(`, lexer.Push("Root")}, diff --git a/ucl/builtins.go b/ucl/builtins.go index 96db044..59faca9 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -180,6 +180,7 @@ func modBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return IntObject(n), nil } +// TODO: this may need to be a macro func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err @@ -188,10 +189,32 @@ func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) { name, err := args.stringArg(0) if err != nil { return nil, err + } else if len(name) == 0 { + return nil, fmt.Errorf("attempt to set empty string") } newVal := args.args[1] + if strings.HasPrefix(name, "@") { + pname := name[1:] + pvar, ok := args.ec.getPseudoVar(pname) + if ok { + if err := pvar.set(ctx, pname, newVal); err != nil { + return nil, err + } + return newVal, nil + } + + if pvar := args.inst.missingPseudoVarHandler; pvar != nil { + if err := pvar.set(ctx, pname, newVal); err != nil { + return nil, err + } + return newVal, nil + } + + return nil, fmt.Errorf("attempt to set '%v' to a non-existent pseudo-variable", name) + } + args.ec.setOrDefineVar(name, newVal) return newVal, nil } diff --git a/ucl/env.go b/ucl/env.go index 0a3d52f..3d217ca 100644 --- a/ucl/env.go +++ b/ucl/env.go @@ -1,11 +1,12 @@ package ucl type evalCtx struct { - root *evalCtx - parent *evalCtx - commands map[string]invokable - macros map[string]macroable - vars map[string]Object + root *evalCtx + parent *evalCtx + commands map[string]invokable + macros map[string]macroable + vars map[string]Object + pseudoVars map[string]pseudoVar } func (ec *evalCtx) forkAndIsolate() *evalCtx { @@ -72,6 +73,14 @@ func (ec *evalCtx) getVar(name string) (Object, bool) { return nil, false } +func (ec *evalCtx) getPseudoVar(name string) (pseudoVar, bool) { + if ec.root.pseudoVars == nil { + return nil, false + } + pvar, ok := ec.root.pseudoVars[name] + return pvar, ok +} + func (ec *evalCtx) lookupInvokable(name string) invokable { if ec == nil { return nil diff --git a/ucl/eval.go b/ucl/eval.go index 237864b..ab9f26a 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -187,6 +187,16 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec return v, nil } return nil, nil + case n.PseudoVar != nil: + if v, ok := ec.getPseudoVar(*n.PseudoVar); ok { + return v.get(ctx, *n.PseudoVar) + } + + if mph := e.inst.missingPseudoVarHandler; mph != nil { + return mph.get(ctx, *n.PseudoVar) + } + + return nil, errors.New("unknown pseudo-variable: " + *n.Var) case n.MaybeSub != nil: sub := n.MaybeSub.Sub if sub == nil { @@ -357,3 +367,8 @@ func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Ob } return pipelineRes, nil } + +type pseudoVar interface { + get(ctx context.Context, name string) (Object, error) + set(ctx context.Context, name string, v Object) error +} diff --git a/ucl/inst.go b/ucl/inst.go index 81974ba..74dbdcf 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -9,9 +9,10 @@ import ( ) type Inst struct { - out io.Writer - missingBuiltinHandler MissingBuiltinHandler - echoPrinter EchoPrinter + out io.Writer + missingBuiltinHandler MissingBuiltinHandler + missingPseudoVarHandler pseudoVar + echoPrinter EchoPrinter rootEC *evalCtx } @@ -121,6 +122,17 @@ func (inst *Inst) SetVar(name string, value any) { inst.rootEC.setOrDefineVar(name, obj) } +func (inst *Inst) SetPseudoVar(name string, h PseudoVarHandler) { + if inst.rootEC.pseudoVars == nil { + inst.rootEC.pseudoVars = make(map[string]pseudoVar) + } + inst.rootEC.pseudoVars[name] = nativePseudoVarHandler{h: h} +} + +func (inst *Inst) SetMissingPseudoVarHandler(h MissingPseudoVarHandler) { + inst.missingPseudoVarHandler = nativeMissingPseudoVarHandler{h: h} +} + func (inst *Inst) Out() io.Writer { if inst.out == nil { return os.Stdout @@ -156,3 +168,73 @@ func (inst *Inst) eval(ctx context.Context, expr string) (Object, error) { // TODO: this should be a separate forkAndIsolate() session return eval.evalScript(ctx, inst.rootEC, ast) } + +type PseudoVarHandler interface { + Get(ctx context.Context) (any, error) +} + +type ModifiablePseudoVarHandler interface { + PseudoVarHandler + Set(ctx context.Context, v any) error +} + +type MissingPseudoVarHandler interface { + Get(ctx context.Context, name string) (any, error) +} + +type MissingModifiablePseudoVarHandler interface { + MissingPseudoVarHandler + Set(ctx context.Context, string, v any) error +} + +type nativePseudoVarHandler struct { + h PseudoVarHandler +} + +func (n nativePseudoVarHandler) get(ctx context.Context, name string) (Object, error) { + gv, err := n.h.Get(ctx) + if err != nil { + return nil, err + } + return fromGoValue(gv) +} + +func (n nativePseudoVarHandler) set(ctx context.Context, name string, v Object) error { + mpvh, ok := n.h.(ModifiablePseudoVarHandler) + if !ok { + return errors.New("cannot set read-only pseudo-var") + } + + gv, ok := toGoValue(v) + if !ok { + return errors.New("cannot set non-matching type") + } + + return mpvh.Set(ctx, gv) +} + +type nativeMissingPseudoVarHandler struct { + h MissingPseudoVarHandler +} + +func (n nativeMissingPseudoVarHandler) get(ctx context.Context, name string) (Object, error) { + gv, err := n.h.Get(ctx, name) + if err != nil { + return nil, err + } + return fromGoValue(gv) +} + +func (n nativeMissingPseudoVarHandler) set(ctx context.Context, name string, v Object) error { + mpvh, ok := n.h.(MissingModifiablePseudoVarHandler) + if !ok { + return errors.New("cannot set read-only pseudo-var") + } + + gv, ok := toGoValue(v) + if !ok { + return errors.New("cannot set non-matching type") + } + + return mpvh.Set(ctx, name, gv) +} diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 429b35b..069927e 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -124,6 +124,49 @@ func TestInst_Eval(t *testing.T) { } } +func TestInst_SetPseudoVar(t *testing.T) { + tests := []struct { + desc string + expr string + wantRes any + wantBarVar string + wantErr bool + }{ + {desc: "read var 1", expr: `@foo`, wantRes: "this is foo"}, + {desc: "read var 2", expr: `@bar`, wantRes: "this is bar"}, + {desc: "read var 3", expr: `@fla`, wantRes: "missing value of fla"}, + + {desc: "write var 1", expr: `set "@foo" "hello" ; @foo`, wantRes: "hello"}, + {desc: "write var 2", expr: `set "@bar" "world" ; @bar`, wantRes: "world", wantBarVar: "world"}, + {desc: "write var 3", expr: `set "@blong" "hello"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bar := &stringPseudoVar{str: "this is bar"} + + inst := ucl.New(ucl.WithTestBuiltin()) + inst.SetPseudoVar("foo", &stringPseudoVar{str: "this is foo"}) + inst.SetPseudoVar("bar", bar) + inst.SetMissingPseudoVarHandler(missingPseudoVarType{}) + + res, err := inst.Eval(t.Context(), tt.expr) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.wantRes, res) + + if tt.wantBarVar != "" { + assert.Equal(t, tt.wantBarVar, bar.str) + } + }) + } +} + var parseComments1 = ` proc lookup { |file| foreach { |toks| @@ -162,3 +205,22 @@ var parseComments4 = ` } } # this use to fail` + +type stringPseudoVar struct { + str string +} + +func (s *stringPseudoVar) Get(ctx context.Context) (any, error) { + return s.str, nil +} + +func (s *stringPseudoVar) Set(ctx context.Context, v any) error { + s.str = v.(string) + return nil +} + +type missingPseudoVarType struct{} + +func (missingPseudoVarType) Get(ctx context.Context, name string) (any, error) { + return "missing value of " + name, nil +}