diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index b442b8a..d7e2a15 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -62,6 +62,23 @@ func toUpperBuiltin(ctx context.Context, inStream stream, args invocationArgs) ( }, nil } +func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + l := args.args[0] + r := args.args[1] + + switch lv := l.(type) { + case strObject: + if rv, ok := r.(strObject); ok { + return boolObject(lv == rv), nil + } + } + return boolObject(false), nil +} + func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { var sb strings.Builder @@ -122,6 +139,23 @@ func mapBuiltin(ctx context.Context, inStream stream, args invocationArgs) (obje }, nil } +func firstBuiltin(ctx context.Context, inStream stream, args invocationArgs) (object, error) { + args, strm, err := args.streamableSource(inStream) + if err != nil { + return nil, err + } + defer strm.close() + + x, err := strm.next() + if errors.Is(err, io.EOF) { + return nil, nil + } else if err != nil { + return x, nil + } + + return x, nil +} + type fileLinesStream struct { filename string f *os.File diff --git a/cmdlang/env.go b/cmdlang/env.go index dc0a627..82d335d 100644 --- a/cmdlang/env.go +++ b/cmdlang/env.go @@ -8,24 +8,30 @@ type evalCtx struct { vars map[string]object } +func (ec *evalCtx) forkAndIsolate() *evalCtx { + newEc := &evalCtx{parent: ec} + newEc.root = newEc + return newEc +} + func (ec *evalCtx) fork() *evalCtx { return &evalCtx{parent: ec, root: ec.root} } func (ec *evalCtx) addCmd(name string, inv invokable) { - if ec.commands == nil { - ec.commands = make(map[string]invokable) + if ec.root.commands == nil { + ec.root.commands = make(map[string]invokable) } - ec.commands[name] = inv + ec.root.commands[name] = inv } func (ec *evalCtx) addMacro(name string, inv macroable) { - if ec.macros == nil { - ec.macros = make(map[string]macroable) + if ec.root.macros == nil { + ec.root.macros = make(map[string]macroable) } - ec.macros[name] = inv + ec.root.macros[name] = inv } func (ec *evalCtx) setVar(name string, val object) { diff --git a/cmdlang/eval.go b/cmdlang/eval.go index df795d1..066e58a 100644 --- a/cmdlang/eval.go +++ b/cmdlang/eval.go @@ -3,6 +3,7 @@ package cmdlang import ( "context" "errors" + "log" "strconv" ) @@ -76,6 +77,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea switch { case ast.Name.Ident != nil: name := *ast.Name.Ident + log.Printf("--> invoking: %v", name) // Regular command if cmd := ec.lookupInvokable(name); cmd != nil { @@ -83,7 +85,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea } else if macro := ec.lookupMacro(name); macro != nil { return e.evalMacro(ctx, ec, currentStream, ast, macro) } else { - return nil, errors.New("unknown command") + return nil, errors.New("unknown command: " + name) } case len(ast.Args) > 0: nameElem, err := e.evalArg(ctx, ec, ast.Name) diff --git a/cmdlang/inst.go b/cmdlang/inst.go index 18c2f86..a9d0b9e 100644 --- a/cmdlang/inst.go +++ b/cmdlang/inst.go @@ -34,7 +34,9 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("call", invokableFunc(callBuiltin)) rootEC.addCmd("map", invokableStreamFunc(mapBuiltin)) + rootEC.addCmd("head", invokableStreamFunc(firstBuiltin)) + rootEC.addCmd("eq", invokableFunc(eqBuiltin)) rootEC.addCmd("cat", invokableFunc(concatBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin)) @@ -86,6 +88,7 @@ func (inst *Inst) eval(ctx context.Context, expr string) (object, error) { eval := evaluator{inst: inst} + // TODO: this should be a separate forkAndIsolate() session return eval.evalScript(ctx, inst.rootEC, ast) } diff --git a/cmdlang/inst_test.go b/cmdlang/inst_test.go index bc5619b..34d6542 100644 --- a/cmdlang/inst_test.go +++ b/cmdlang/inst_test.go @@ -42,7 +42,7 @@ func TestInst_Eval(t *testing.T) { // Lists {desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}}, - {desc: "list 2", expr: `set one "one" ; firstarg [$one (pipe "two" | toUpper) "three"]`, want: []any{"one", "TWO", "three"}}, + {desc: "list 2", expr: `set one "one" ; firstarg [$one (pipe "two" | toUpper | head) "three"]`, want: []any{"one", "TWO", "three"}}, {desc: "list 3", expr: `firstarg []`, want: []any{}}, // Maps @@ -52,7 +52,7 @@ func TestInst_Eval(t *testing.T) { set one "one" ; set n1 "1" firstarg [ $one:$n1 - (firstarg "two" | toUpper):(firstarg "2" | toUpper) + (firstarg "two" | toUpper | head):(firstarg "2" | toUpper | head) three:"3" ]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}}, {desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}}, diff --git a/cmdlang/objs.go b/cmdlang/objs.go index 3de6234..e1c340d 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -46,6 +46,19 @@ func (s strObject) Truthy() bool { return string(s) != "" } +type boolObject bool + +func (b boolObject) String() string { + if b { + return "(true)" + } + return "(false))" +} + +func (b boolObject) Truthy() bool { + return bool(b) +} + func toGoValue(obj object) (interface{}, bool) { switch v := obj.(type) { case nil: diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index c42c218..910cbe0 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -201,3 +201,125 @@ func TestBuiltins_ForEach(t *testing.T) { }) } } + +func TestBuiltins_Procs(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "simple procs", expr: ` + proc greet { + echo "Hello, world" + } + + greet + greet`, want: "Hello, world\nHello, world\n(nil)\n"}, + {desc: "multiple procs", expr: ` + proc greet { |what| + echo "Hello, " $what + } + proc greetWorld { greet "world" } + proc greetMoon { greet "moon" } + proc greetTheThing { |what| greet (cat "the " $what) } + + greetWorld + greetMoon + greetTheThing "sun" + `, want: "Hello, world\nHello, moon\nHello, the sun\n(nil)\n"}, + {desc: "recursive procs", expr: ` + proc four4 { |xs| + if (eq $xs "xxxx") { + $xs + } + four4 (cat $xs "x") + } + + four4 + `, want: "xxxx\n"}, + {desc: "closures", expr: ` + proc makeGreeter { |greeting| + proc { |what| + echo $greeting ", " $what + } + } + + set helloGreater (makeGreeter "Hello") + $helloGreater "world" + + set goodbye (makeGreeter "Goodbye cruel") + $goodbye "world" + + 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"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + +func TestBuiltins_Map(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "map list", expr: ` + proc makeUpper { |x| $x | toUpper } + + map ["a" "b" "c"] (proc { |x| makeUpper $x }) + `, want: "A\nB\nC\n"}, + {desc: "map list 2", expr: ` + set makeUpper (proc { |x| $x | toUpper }) + + map ["a" "b" "c"] $makeUpper + `, want: "A\nB\nC\n"}, + {desc: "map list with stream", expr: ` + set makeUpper (proc { |x| $x | toUpper }) + + ["a" "b" "c"] | map $makeUpper + `, want: "A\nB\nC\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"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +}