diff --git a/Makefile b/Makefile index 86d3564..626870d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ clean: -rm -r build test: - go test ./cmdlang/... + go test ./ucl/... site: clean mkdir build diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index a631575..0d8ff7c 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -3,7 +3,7 @@ package main import ( "context" "github.com/chzyer/readline" - "github.com/lmika/cmdlang-proto/cmdlang" + "github.com/lmika/ucl/ucl" "log" ) @@ -14,7 +14,7 @@ func main() { } defer rl.Close() - inst := cmdlang.New() + inst := ucl.New() ctx := context.Background() for { @@ -23,7 +23,7 @@ func main() { break } - if err := inst.EvalAndDisplay(ctx, line); err != nil { + if err := ucl.EvalAndDisplay(ctx, inst, line); err != nil { log.Printf("%T: %v", err, err) } } diff --git a/cmd/playwasm/jsiter.go b/cmd/playwasm/jsiter.go index a5730e1..29c49fd 100644 --- a/cmd/playwasm/jsiter.go +++ b/cmd/playwasm/jsiter.go @@ -7,9 +7,8 @@ import ( "context" "errors" "github.com/alecthomas/participle/v2" - "github.com/lmika/cmdlang-proto/cmdlang" + "github.com/lmika/ucl/ucl" "strings" - "syscall/js" ) func invokeUCLCallback(name string, args ...any) { @@ -21,7 +20,7 @@ func invokeUCLCallback(name string, args ...any) { func initJS(ctx context.Context) { ucl := make(map[string]any) - inst := cmdlang.New(cmdlang.WithOut(&uclOut{ + inst := ucl.New(ucl.WithOut(&uclOut{ lineBuffer: new(bytes.Buffer), writeLine: func(line string) { invokeUCLCallback("onOutLine", line) diff --git a/cmdlang/egbuiltins.go b/cmdlang/egbuiltins.go deleted file mode 100644 index cdbbcc3..0000000 --- a/cmdlang/egbuiltins.go +++ /dev/null @@ -1,7 +0,0 @@ -package cmdlang - -import "context" - -func egLookup(ctx context.Context, args invocationArgs) (object, error) { - return nil, nil -} diff --git a/go.mod b/go.mod index 2769bfd..1986ce3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/lmika/cmdlang-proto +module github.com/lmika/ucl go 1.21.1 diff --git a/cmdlang/ast.go b/ucl/ast.go similarity index 94% rename from cmdlang/ast.go rename to ucl/ast.go index 9e989de..ce2bf2f 100644 --- a/cmdlang/ast.go +++ b/ucl/ast.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "io" @@ -64,12 +64,13 @@ type astStatements struct { } type astScript struct { - Statements *astStatements `parser:"NL* @@ NL*"` + Statements *astStatements `parser:"NL* (@@ NL*)?"` } var scanner = lexer.MustStateful(lexer.Rules{ "Root": { {"Whitespace", `[ \t]+`, nil}, + {"Comment", `[#].*`, nil}, {"String", `"(\\"|[^"])*"`, nil}, {"Int", `[-]?[0-9][0-9]*`, nil}, {"DOLLAR", `\$`, nil}, @@ -86,7 +87,7 @@ var scanner = lexer.MustStateful(lexer.Rules{ }, }) var parser = participle.MustBuild[astScript](participle.Lexer(scanner), - participle.Elide("Whitespace")) + participle.Elide("Whitespace", "Comment")) func parse(r io.Reader) (*astScript, error) { return parser.Parse("test", r) diff --git a/cmdlang/builtins.go b/ucl/builtins.go similarity index 97% rename from cmdlang/builtins.go rename to ucl/builtins.go index 8b8e6fa..2e67655 100644 --- a/cmdlang/builtins.go +++ b/ucl/builtins.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "context" @@ -40,8 +40,7 @@ func setBuiltin(ctx context.Context, args invocationArgs) (object, error) { newVal := args.args[1] - // TODO: if the value is a stream, consume the stream and save it as a list - args.ec.setVar(name, newVal) + args.ec.setOrDefineVar(name, newVal) return newVal, nil } @@ -383,9 +382,9 @@ func (b procObject) invoke(ctx context.Context, args invocationArgs) (object, er for i, name := range b.block.Names { if i < len(args.args) { - newEc.setVar(name, args.args[i]) + newEc.setOrDefineVar(name, args.args[i]) } else { - newEc.setVar(name, nil) + newEc.setOrDefineVar(name, nil) } } diff --git a/cmdlang/env.go b/ucl/env.go similarity index 81% rename from cmdlang/env.go rename to ucl/env.go index 82d335d..5d2697f 100644 --- a/cmdlang/env.go +++ b/ucl/env.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl type evalCtx struct { root *evalCtx @@ -34,7 +34,24 @@ func (ec *evalCtx) addMacro(name string, inv macroable) { ec.root.macros[name] = inv } -func (ec *evalCtx) setVar(name string, val object) { +func (ec *evalCtx) setVar(name string, val object) bool { + if ec == nil || ec.vars == nil { + return false + } + + if _, ok := ec.vars[name]; ok { + ec.vars[name] = val + return true + } + + return ec.parent.setVar(name, val) +} + +func (ec *evalCtx) setOrDefineVar(name string, val object) { + if ec.setVar(name, val) { + return + } + if ec.vars == nil { ec.vars = make(map[string]object) } diff --git a/cmdlang/eval.go b/ucl/eval.go similarity index 99% rename from cmdlang/eval.go rename to ucl/eval.go index 6784afc..ce14fe2 100644 --- a/cmdlang/eval.go +++ b/ucl/eval.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "context" @@ -27,6 +27,10 @@ func (e evaluator) evalScript(ctx context.Context, ec *evalCtx, n *astScript) (l } func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (object, error) { + if n == nil { + return nil, nil + } + res, err := e.evalPipeline(ctx, ec, n.First) if err != nil { return nil, err diff --git a/ucl/evaldisplay.go b/ucl/evaldisplay.go new file mode 100644 index 0000000..32e2002 --- /dev/null +++ b/ucl/evaldisplay.go @@ -0,0 +1,29 @@ +package ucl + +import ( + "context" + "fmt" +) + +func EvalAndDisplay(ctx context.Context, inst *Inst, expr string) error { + res, err := inst.eval(ctx, expr) + if err != nil { + return err + } + + return displayResult(ctx, inst, res) +} + +func displayResult(ctx context.Context, inst *Inst, res object) (err error) { + switch v := res.(type) { + case nil: + if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil { + return err + } + default: + if _, err = fmt.Fprintln(inst.out, v.String()); err != nil { + return err + } + } + return nil +} diff --git a/cmdlang/inst.go b/ucl/inst.go similarity index 77% rename from cmdlang/inst.go rename to ucl/inst.go index 53e35e2..f29c14a 100644 --- a/cmdlang/inst.go +++ b/ucl/inst.go @@ -1,9 +1,8 @@ -package cmdlang +package ucl import ( "context" "errors" - "fmt" "io" "os" "strings" @@ -47,7 +46,7 @@ func New(opts ...InstOption) *Inst { //rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin)) - rootEC.setVar("hello", strObject("world")) + rootEC.setOrDefineVar("hello", strObject("world")) inst := &Inst{ out: os.Stdout, @@ -93,26 +92,3 @@ 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) } - -func (inst *Inst) EvalAndDisplay(ctx context.Context, expr string) error { - res, err := inst.eval(ctx, expr) - if err != nil { - return err - } - - return inst.display(ctx, res) -} - -func (inst *Inst) display(ctx context.Context, res object) (err error) { - switch v := res.(type) { - case nil: - if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil { - return err - } - default: - if _, err = fmt.Fprintln(inst.out, v.String()); err != nil { - return err - } - } - return nil -} diff --git a/cmdlang/inst_test.go b/ucl/inst_test.go similarity index 95% rename from cmdlang/inst_test.go rename to ucl/inst_test.go index ec28d47..8724d60 100644 --- a/cmdlang/inst_test.go +++ b/ucl/inst_test.go @@ -1,11 +1,11 @@ -package cmdlang_test +package ucl_test import ( "bytes" "context" + "github.com/lmika/ucl/ucl" "testing" - "github.com/lmika/cmdlang-proto/cmdlang" "github.com/stretchr/testify/assert" ) @@ -67,7 +67,7 @@ func TestInst_Eval(t *testing.T) { ctx := context.Background() outW := bytes.NewBuffer(nil) - inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin()) + inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin()) res, err := inst.Eval(ctx, tt.expr) assert.NoError(t, err) diff --git a/cmdlang/objs.go b/ucl/objs.go similarity index 98% rename from cmdlang/objs.go rename to ucl/objs.go index 59579a8..0bc520c 100644 --- a/cmdlang/objs.go +++ b/ucl/objs.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "context" @@ -235,7 +235,7 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushSco } for i, n := range block.block.Names { if i < len(args) { - ec.setVar(n, args[i]) + ec.setOrDefineVar(n, args[i]) } } @@ -341,7 +341,7 @@ func (bo blockObject) invoke(ctx context.Context, args invocationArgs) (object, ec := args.ec.fork() for i, n := range bo.block.Names { if i < len(args.args) { - ec.setVar(n, args.args[i]) + ec.setOrDefineVar(n, args.args[i]) } } @@ -370,8 +370,7 @@ func (p proxyObject) String() string { } func (p proxyObject) Truthy() bool { - //TODO implement me - panic("implement me") + return p.p != nil } type listableProxyObject struct { @@ -383,7 +382,7 @@ func (p listableProxyObject) String() string { } func (p listableProxyObject) Truthy() bool { - panic("implement me") + return p.v.Len() > 0 } func (p listableProxyObject) Len() int { diff --git a/cmdlang/testbuiltins_test.go b/ucl/testbuiltins_test.go similarity index 91% rename from cmdlang/testbuiltins_test.go rename to ucl/testbuiltins_test.go index 6b5fe84..31be850 100644 --- a/cmdlang/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "bytes" @@ -53,8 +53,8 @@ func WithTestBuiltin() InstOption { return strObject(sb.String()), nil })) - i.rootEC.setVar("a", strObject("alpha")) - i.rootEC.setVar("bee", strObject("buzz")) + i.rootEC.setOrDefineVar("a", strObject("alpha")) + i.rootEC.setOrDefineVar("bee", strObject("buzz")) } } @@ -84,6 +84,18 @@ func TestBuiltins_Echo(t *testing.T) { ; echo "world" +; + `, want: "Hello\nworld\n"}, + {desc: "multi-line 4", expr: ` +# This is a comment +# + +;;; +# This is another comment + echo "Hello" +; + + echo "world" # command after this ; `, want: "Hello\nworld\n"}, } @@ -167,7 +179,7 @@ func TestBuiltins_If(t *testing.T) { outW := bytes.NewBuffer(nil) inst := New(WithOut(outW), WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) @@ -197,7 +209,7 @@ func TestBuiltins_ForEach(t *testing.T) { outW := bytes.NewBuffer(nil) inst := New(WithOut(outW), WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) @@ -257,18 +269,18 @@ func TestBuiltins_Procs(t *testing.T) { call (makeGreeter "Quick") "call me" `, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"}, - //{desc: "modifying closed over variables", expr: ` - // proc makeSetter { - // set bla "X" - // proc appendToBla { |x| - // set bla (cat $bla $x) - // } - // } - // - // set er (makeSetter) - // call $er "xxx" - // call $er "yyy" - // `, want: "Xxxx\nXxxxyyy(nil)\n"}, + {desc: "modifying closed over variables", expr: ` + proc makeSetter { + set bla "X" + proc appendToBla { |x| + set bla (cat $bla $x) + } + } + + set er (makeSetter) + echo (call $er "xxx") + echo (call $er "yyy") + `, want: "Xxxx\nXxxxyyy\n(nil)\n"}, } for _, tt := range tests { @@ -277,7 +289,7 @@ func TestBuiltins_Procs(t *testing.T) { outW := bytes.NewBuffer(nil) inst := New(WithOut(outW), WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) @@ -309,12 +321,12 @@ func TestBuiltins_Map(t *testing.T) { {desc: "map list with block", expr: ` map ["a" "b" "c"] { |x| toUpper $x } `, want: "[A B C]\n"}, - //{desc: "map list with stream", expr: ` - // set makeUpper (proc { |x| $x | toUpper }) - // - // set l (["a" "b" "c"] | map $makeUpper) - // echo $l - // `, want: "[A B C]\n"}, + {desc: "map list with stream", expr: ` + set makeUpper (proc { |x| toUpper $x }) + + set l (["a" "b" "c"] | map $makeUpper) + echo $l + `, want: "[A B C]\n(nil)\n"}, } for _, tt := range tests { @@ -323,7 +335,7 @@ func TestBuiltins_Map(t *testing.T) { outW := bytes.NewBuffer(nil) inst := New(WithOut(outW), WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) @@ -379,7 +391,7 @@ func TestBuiltins_Index(t *testing.T) { Gamma: []int{22, 33}, }, nil }) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) @@ -438,7 +450,7 @@ func TestBuiltins_Len(t *testing.T) { missing: "missing", }, nil }) - err := inst.EvalAndDisplay(ctx, tt.expr) + err := EvalAndDisplay(ctx, inst, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, outW.String()) diff --git a/cmdlang/userbuiltin.go b/ucl/userbuiltin.go similarity index 92% rename from cmdlang/userbuiltin.go rename to ucl/userbuiltin.go index ab099d1..d101d37 100644 --- a/cmdlang/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -1,4 +1,4 @@ -package cmdlang +package ucl import ( "context" @@ -10,8 +10,8 @@ type CallArgs struct { args invocationArgs } -func (ca CallArgs) Bind(vars ...interface{}) error { - if len(ca.args.args) != len(vars) { +func (ca *CallArgs) Bind(vars ...interface{}) error { + if len(ca.args.args) < len(vars) { return errors.New("wrong number of arguments") } @@ -20,6 +20,7 @@ func (ca CallArgs) Bind(vars ...interface{}) error { return err } } + ca.args = ca.args.shift(len(vars)) return nil } diff --git a/cmdlang/userbuiltin_test.go b/ucl/userbuiltin_test.go similarity index 75% rename from cmdlang/userbuiltin_test.go rename to ucl/userbuiltin_test.go index bb6cbee..87c262b 100644 --- a/cmdlang/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -1,19 +1,19 @@ -package cmdlang_test +package ucl_test import ( "bytes" "context" + "github.com/lmika/ucl/ucl" "strings" "testing" - "github.com/lmika/cmdlang-proto/cmdlang" "github.com/stretchr/testify/assert" ) func TestInst_SetBuiltin(t *testing.T) { t.Run("simple builtin accepting and returning strings", func(t *testing.T) { - inst := cmdlang.New() - inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + 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 { @@ -28,9 +28,29 @@ func TestInst_SetBuiltin(t *testing.T) { assert.Equal(t, "Hello, World", res) }) + t.Run("bind shift arguments", func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x); err != nil { + return nil, err + } + if err := args.Bind(&y); err != nil { + return nil, err + } + + return x + y, nil + }) + + res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`) + assert.NoError(t, err) + assert.Equal(t, "Hello, World", res) + }) + t.Run("simple builtin with optional switches and strings", func(t *testing.T) { - inst := cmdlang.New() - inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst := ucl.New() + inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { var x, y, sep string if err := args.BindSwitch("sep", &sep); err != nil { @@ -75,8 +95,8 @@ func TestInst_SetBuiltin(t *testing.T) { x, y string } - inst := cmdlang.New() - inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + 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 { @@ -107,8 +127,8 @@ func TestInst_SetBuiltin(t *testing.T) { for _, tt := range tests { t.Run(tt.descr, func(t *testing.T) { - inst := cmdlang.New() - inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + 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 { @@ -117,7 +137,7 @@ func TestInst_SetBuiltin(t *testing.T) { return pair{x, y}, nil }) - inst.SetBuiltin("join", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { var x pair if err := args.Bind(&x); err != nil { @@ -148,9 +168,9 @@ func TestInst_SetBuiltin(t *testing.T) { for _, tt := range tests { t.Run(tt.descr, func(t *testing.T) { outW := bytes.NewBuffer(nil) - inst := cmdlang.New(cmdlang.WithOut(outW)) + inst := ucl.New(ucl.WithOut(outW)) - inst.SetBuiltin("countTo3", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst.SetBuiltin("countTo3", func(ctx context.Context, args ucl.CallArgs) (any, error) { return []string{"1", "2", "3"}, nil }) @@ -167,14 +187,14 @@ func TestCallArgs_Bind(t *testing.T) { t.Run("bind to an interface", func(t *testing.T) { ctx := context.Background() - inst := cmdlang.New() - inst.SetBuiltin("sa", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst := ucl.New() + inst.SetBuiltin("sa", func(ctx context.Context, args ucl.CallArgs) (any, error) { return doStringA{this: "a val"}, nil }) - inst.SetBuiltin("sb", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst.SetBuiltin("sb", func(ctx context.Context, args ucl.CallArgs) (any, error) { return doStringB{left: "foo", right: "bar"}, nil }) - inst.SetBuiltin("dostr", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + inst.SetBuiltin("dostr", func(ctx context.Context, args ucl.CallArgs) (any, error) { var ds doStringable if err := args.Bind(&ds); err != nil {