diff --git a/_docs/mod/core.md b/_docs/mod/core.md index 37bdbbe..c466e7b 100644 --- a/_docs/mod/core.md +++ b/_docs/mod/core.md @@ -190,3 +190,13 @@ set! NAME VALUE Sets the value of variable NAME to VALUE. VALUE must be non-null otherwise `set!` will raise an error. +### while + +``` +while [GUARD] BLOCK +``` + +Iterate over BLOCK while GUARD is true. If GUARD is not included, `while` will loop indefinitely. + +BLOCK can call `break` and `continue` which will exit out of the loop, or jump to the start of the next iteration +respectively. The return value of `while` will be nil, unless `break` is called with a value. diff --git a/ucl/builtins.go b/ucl/builtins.go index 44a14ee..64fc67b 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -1039,7 +1039,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 { @@ -1052,7 +1052,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) { @@ -1069,7 +1069,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 { @@ -1086,6 +1086,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 errorBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) < 1 { return nil, errors.New("need at least one arguments") diff --git a/ucl/inst.go b/ucl/inst.go index d07bcc6..55acc35 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -105,6 +105,7 @@ func New(opts ...InstOption) *Inst { rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("for", macroFunc(foreachBuiltin)) + rootEC.addMacro("while", macroFunc(whileBuiltin)) rootEC.addMacro("proc", macroFunc(procBuiltin)) rootEC.addMacro("try", macroFunc(tryBuiltin)) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 7aa5644..12a445d 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -466,6 +466,76 @@ func TestBuiltins_For(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