diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index 36baf44..69d6207 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -75,6 +75,19 @@ func catBuiltin(ctx context.Context, args invocationArgs) (object, error) { return &fileLinesStream{filename: filename}, nil } +func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + inv, ok := args.args[0].(invokable) + if !ok { + return nil, errors.New("expected invokable") + } + + return inv.invoke(ctx, args.shift(1)) +} + type fileLinesStream struct { filename string f *os.File @@ -187,3 +200,61 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { return last, nil } + +func procBuiltin(ctx context.Context, args macroArgs) (object, error) { + if args.nargs() < 1 { + return nil, errors.New("need at least one arguments") + } + + var procName string + if args.nargs() == 2 { + name, ok := args.shiftIdent(ctx) + if !ok { + return nil, errors.New("malformed procedure: expected identifier as first argument") + } + procName = name + } + + block, err := args.evalArg(ctx, 0) + if err != nil { + return nil, err + } + blockObj, ok := block.(blockObject) + if !ok { + return nil, fmt.Errorf("malformed procedure: expected block object, was %v", block.String()) + } + + obj := procObject{args.eval, args.ec, blockObj.block} + if procName != "" { + args.ec.addCmd(procName, obj) + } + return obj, nil +} + +type procObject struct { + eval evaluator + ec *evalCtx + block *astBlock +} + +func (b procObject) String() string { + return "(proc)" +} + +func (b procObject) Truthy() bool { + return true +} + +func (b procObject) invoke(ctx context.Context, args invocationArgs) (object, error) { + newEc := b.ec.fork() + + for i, name := range b.block.Names { + if i < len(args.args) { + newEc.setVar(name, args.args[i]) + } else { + newEc.setVar(name, nil) + } + } + + return b.eval.evalBlock(ctx, newEc, b.block) +} diff --git a/cmdlang/env.go b/cmdlang/env.go index 165c15b..dc0a627 100644 --- a/cmdlang/env.go +++ b/cmdlang/env.go @@ -1,6 +1,7 @@ package cmdlang type evalCtx struct { + root *evalCtx parent *evalCtx commands map[string]invokable macros map[string]macroable @@ -8,7 +9,7 @@ type evalCtx struct { } func (ec *evalCtx) fork() *evalCtx { - return &evalCtx{parent: ec} + return &evalCtx{parent: ec, root: ec.root} } func (ec *evalCtx) addCmd(name string, inv invokable) { diff --git a/cmdlang/inst.go b/cmdlang/inst.go index 1703289..e333c1d 100644 --- a/cmdlang/inst.go +++ b/cmdlang/inst.go @@ -24,15 +24,18 @@ func WithOut(out io.Writer) InstOption { } func New(opts ...InstOption) *Inst { - rootEC := evalCtx{} + rootEC := &evalCtx{} + rootEC.root = rootEC rootEC.addCmd("echo", invokableFunc(echoBuiltin)) rootEC.addCmd("set", invokableFunc(setBuiltin)) rootEC.addCmd("toUpper", invokableStreamFunc(toUpperBuiltin)) rootEC.addCmd("cat", invokableFunc(catBuiltin)) + rootEC.addCmd("call", invokableFunc(callBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) + rootEC.addMacro("proc", macroFunc(procBuiltin)) //rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin)) @@ -40,7 +43,7 @@ func New(opts ...InstOption) *Inst { inst := &Inst{ out: os.Stdout, - rootEC: &rootEC, + rootEC: rootEC, } for _, opt := range opts { diff --git a/cmdlang/objs.go b/cmdlang/objs.go index f71074d..77edbf6 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -119,6 +119,19 @@ func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bo return *lit == expectedIdent } +func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) { + if ma.argShift >= len(ma.ast.Args) { + return "", false + } + + lit := ma.ast.Args[ma.argShift].Ident + if lit != nil { + ma.argShift += 1 + return *lit, true + } + return "", false +} + func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) { if n >= len(ma.ast.Args[ma.argShift:]) { return nil, errors.New("not enough arguments") // FIX @@ -177,6 +190,16 @@ func (ia invocationArgs) stringArg(i int) (string, error) { return s.String(), nil } +func (ia invocationArgs) shift(i int) invocationArgs { + return invocationArgs{ + inst: ia.inst, + ec: ia.ec, + currentStream: ia.currentStream, + args: ia.args[i:], + kwargs: ia.kwargs, + } +} + // invokable is an object that can be executed as a command type invokable interface { invoke(ctx context.Context, args invocationArgs) (object, error)