diff --git a/ucl/builtins.go b/ucl/builtins.go index c893cf4..ae1fcdc 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -912,7 +912,7 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { l := t.Len() for i := 0; i < l; i++ { v := t.Index(i) - last, err = args.evalBlock(ctx, blockIdx, []Object{v}, true) // TO INCLUDE: the index + last, err = args.evalBlock(ctx, blockIdx, []Object{v}, false) // TO INCLUDE: the index if err != nil { if errors.As(err, &breakErr) { if !breakErr.isCont { @@ -925,7 +925,7 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { } case hashable: err := t.Each(func(k string, v Object) error { - last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, true) + last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, false) return err }) if errors.As(err, &breakErr) { @@ -942,7 +942,7 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { return nil, err } - last, err = args.evalBlock(ctx, blockIdx, []Object{v}, true) // TO INCLUDE: the index + last, err = args.evalBlock(ctx, blockIdx, []Object{v}, false) // TO INCLUDE: the index if err != nil { if errors.As(err, &breakErr) { if !breakErr.isCont { @@ -959,6 +959,42 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { return last, nil } +func whileBuiltin(ctx context.Context, args macroArgs) (Object, error) { + blockIdx := 1 + loopForever := false + + if args.nargs() < 2 { + blockIdx = 0 + loopForever = true + } + + var ( + breakErr errBreak + ) + + for { + if !loopForever { + guard, err := args.evalArg(ctx, 0) + if err != nil { + return nil, err + } + if !isTruthy(guard) { + return nil, nil + } + } + + if _, err := args.evalBlock(ctx, blockIdx, nil, false); err != nil { + if errors.As(err, &breakErr) { + if !breakErr.isCont { + return breakErr.ret, nil + } + } else { + return nil, err + } + } + } +} + func breakBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) < 1 { return nil, errBreak{} diff --git a/ucl/inst.go b/ucl/inst.go index 697273d..80135dd 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -87,6 +87,8 @@ func New(opts ...InstOption) *Inst { rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) + rootEC.addMacro("for", macroFunc(foreachBuiltin)) + rootEC.addMacro("while", macroFunc(whileBuiltin)) rootEC.addMacro("proc", macroFunc(procBuiltin)) inst := &Inst{ @@ -133,7 +135,7 @@ func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) { return goRes, nil } -func (inst *Inst) eval(ctx context.Context, expr string) (object, error) { +func (inst *Inst) eval(ctx context.Context, expr string) (Object, error) { ast, err := parse(strings.NewReader(expr)) if err != nil { return nil, err diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index c7b3ac7..d4227c7 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -274,6 +274,76 @@ func TestBuiltins_ForEach(t *testing.T) { } } +func TestBuiltins_While(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "iterate while true 1", expr: ` + set x 0 + while (lt $x 5) { + echo $x + set x (add $x 1) + } + echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, + {desc: "iterate while true 2", expr: ` + set x 20 + while (lt $x 5) { + echo $x + set x (add $x 1) + } + echo "done"`, want: "done\n(nil)\n"}, + {desc: "iterate for ever with break 1", expr: ` + set x 0 + while { + echo $x + set x (add $x 1) + if (ge $x 5) { + break + } + } + echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, + {desc: "iterate for ever with break 2", expr: ` + set x 0 + echo (while { + echo $x + set x (add $x 1) + if (ge $x 5) { + break $x + } + }) + `, want: "0\n1\n2\n3\n4\n5\n(nil)\n"}, + {desc: "iterate for ever with continue", expr: ` + set x 0 + while { + set x (add $x 1) + if (or (eq $x 2) (eq $x 4)) { + echo "quack" + continue + } + echo $x + if (ge $x 5) { + break + } + } + echo "done"`, want: "1\nquack\n3\nquack\n5\ndone\n(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 := evalAndDisplay(ctx, inst, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + func TestBuiltins_Break(t *testing.T) { tests := []struct { desc string