From 0cf2f816dac204336d44965fa44b318f3a94cdf7 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 24 May 2025 09:52:10 +1000 Subject: [PATCH] Added try blocks back --- ucl/builtins.go | 66 +++++++++ ucl/inst.go | 2 +- ucl/inst_test.go | 13 +- ucl/objs.go | 10 +- ucl/testbuiltins_test.go | 291 +++++++++++++++++++++++++++++++++++---- ucl/userbuiltin_test.go | 2 +- 6 files changed, 348 insertions(+), 36 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index 9a1ac62..78dc19c 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -968,6 +968,72 @@ func ifBuiltin(ctx context.Context, args macroArgs) (Object, error) { return nil, errors.New("malformed if-elif-else") } +func tryBuiltin(ctx context.Context, args macroArgs) (res Object, err error) { + if args.nargs() < 2 { + return nil, errors.New("need at least 2 arguments") + } else if args.nargs()%2 == 0 { + return nil, errors.New("need an odd number of arguments") + } + + // Select catches and finally + catchBlocks := make([]int, 0) + finallyBlocks := make([]int, 0) + for i := 1; i < args.nargs(); i += 2 { + if args.identIs(ctx, i, "catch") { + if len(finallyBlocks) > 0 { + return nil, errors.New("catch cannot be used after finally") + } + catchBlocks = append(catchBlocks, i+1) + } else if args.identIs(ctx, i, "finally") { + finallyBlocks = append(finallyBlocks, i+1) + } + } + + defer func() { + if isBreakErr(err) { + return + } + + var ( + orgErr = err + lastFinallyErr error = nil + ) + + for _, idx := range finallyBlocks { + if _, fErr := args.evalBlock(ctx, idx, nil, false); fErr != nil { + if isBreakErr(fErr) { + if err == nil { + err = fErr + } + return + } + lastFinallyErr = fErr + } + } + if orgErr == nil { + err = lastFinallyErr + } + }() + + res, err = args.evalBlock(ctx, 0, nil, false) + if err == nil { + return res, nil + } else if isBreakErr(err) { + return nil, err + } + + for _, idx := range catchBlocks { + res, err = args.evalBlock(ctx, idx, []Object{errObject{err: err}}, false) + if err == nil { + return res, nil + } else if isBreakErr(err) { + return nil, err + } + } + + return nil, err +} + func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) { var ( items Object diff --git a/ucl/inst.go b/ucl/inst.go index 94c0353..9484809 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -100,10 +100,10 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("assert", invokableFunc(assertBuiltin)) 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)) + rootEC.addMacro("try", macroFunc(tryBuiltin)) inst := &Inst{ out: os.Stdout, diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 2a02821..479c845 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -14,6 +14,7 @@ func TestInst_Eval(t *testing.T) { desc string expr string want any + wantObj bool wantErr error }{ {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, @@ -100,10 +101,10 @@ func TestInst_Eval(t *testing.T) { {desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil}, - {desc: "parse comments 1", expr: parseComments1, wantErr: ucl.ErrNotConvertable}, - {desc: "parse comments 2", expr: parseComments2, wantErr: ucl.ErrNotConvertable}, - {desc: "parse comments 3", expr: parseComments3, wantErr: ucl.ErrNotConvertable}, - {desc: "parse comments 4", expr: parseComments4, wantErr: ucl.ErrNotConvertable}, + {desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil}, + {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, + {desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil}, + {desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil}, } for _, tt := range tests { @@ -116,6 +117,10 @@ func TestInst_Eval(t *testing.T) { if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) + } else if tt.wantObj { + assert.NoError(t, err) + _, isObj := res.(ucl.Object) + assert.True(t, isObj) } else { assert.NoError(t, err) assert.Equal(t, tt.want, res) diff --git a/ucl/objs.go b/ucl/objs.go index 8ca798d..f5894f1 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -387,7 +387,7 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int, args []Object, pushSco type errObject struct{ err error } func (eo errObject) String() string { - return "error:" + eo.err.Error() + return eo.err.Error() } func (eo errObject) Truthy() bool { @@ -672,3 +672,11 @@ func (e errReturn) Error() string { } var ErrHalt = errors.New("halt") + +func isBreakErr(err error) bool { + if err == nil { + return false + } + + return errors.As(err, &errBreak{}) || errors.As(err, &errReturn{}) || errors.Is(err, ErrHalt) +} diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 6cdeb14..096e446 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -136,7 +136,7 @@ func TestBuiltins_Echo(t *testing.T) { {desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"}, {desc: "interpolated string 4", expr: `$what = "Hello" ; $where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, {desc: "interpolated string 5", expr: ` - foreach [123 "foo" true ()] { |x| + for [123 "foo" true ()] { |x| echo "[[$x]]" } `, want: "[[123]]\n[[foo]]\n[[true]]\n[[]]\n"}, @@ -217,8 +217,8 @@ func TestBuiltins_If(t *testing.T) { {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "if of itr 1", expr: `$i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 2", expr: `$i = itr ; foreach (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 3", expr: `$i = itr ; foreach (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, + {desc: "if of itr 2", expr: `$i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 3", expr: `$i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, {desc: "if of itr 4", expr: `$i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, {desc: "if of itr 5", expr: `$i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, {desc: "if of itr 6", expr: `$i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, @@ -245,19 +245,19 @@ func TestBuiltins_ForEach(t *testing.T) { want string }{ {desc: "iterate over list 1", expr: ` - foreach ["1" "2" "3"] { |v| + for ["1" "2" "3"] { |v| echo $v }`, want: "1\n2\n3\n(nil)\n"}, {desc: "iterate over list 2", - expr: `foreach ["1" "2" "3"] echo`, + expr: `for ["1" "2" "3"] echo`, want: "1\n2\n3\n(nil)\n"}, // TODO: hash is not sorted, so need to find a way to sort it {desc: "iterate over map 1", expr: ` - foreach [a:"1"] { |k v| echo $k "=" $v }`, want: "a=1\n(nil)\n"}, + for [a:"1"] { |k v| echo $k "=" $v }`, want: "a=1\n(nil)\n"}, {desc: "iterate over map 2", expr: ` - foreach [a:"1"] echo`, want: "a1\n(nil)\n"}, - {desc: "iterate via pipe", expr: `["2" "4" "6"] | foreach { |x| echo $x }`, want: "2\n4\n6\n(nil)\n"}, - {desc: "iterate from iterator 1", expr: `itr | foreach { |x| echo $x }`, want: "1\n2\n3\n(nil)\n"}, + for [a:"1"] echo`, want: "a1\n(nil)\n"}, + {desc: "iterate via pipe", expr: `["2" "4" "6"] | for { |x| echo $x }`, want: "2\n4\n6\n(nil)\n"}, + {desc: "iterate from iterator 1", expr: `itr | for { |x| echo $x }`, want: "1\n2\n3\n(nil)\n"}, } for _, tt := range tests { @@ -361,29 +361,29 @@ func TestBuiltins_Break(t *testing.T) { want string }{ {desc: "break unconditionally returning nothing", expr: ` - foreach ["1" "2" "3"] { |v| + for ["1" "2" "3"] { |v| break echo $v }`, want: "(nil)\n"}, {desc: "break conditionally returning nothing", expr: ` - foreach ["1" "2" "3"] { |v| + for ["1" "2" "3"] { |v| echo $v if (eq $v "2") { break } }`, want: "1\n2\n(nil)\n"}, {desc: "break inner loop only returning nothing", expr: ` - foreach ["a" "b"] { |u| - foreach ["1" "2" "3"] { |v| + for ["a" "b"] { |u| + for ["1" "2" "3"] { |v| echo $u $v if (eq $v "2") { break } } }`, want: "a1\na2\nb1\nb2\n(nil)\n"}, {desc: "break returning value 1", expr: ` - echo (foreach ["1" "2" "3"] { |v| + echo (for ["1" "2" "3"] { |v| echo $v if (eq $v "2") { break "hello" } })`, want: "1\n2\nhello\n(nil)\n"}, {desc: "break returning value 2", expr: ` - echo (foreach (itr) { |v| + echo (for (itr) { |v| echo $v if (eq $v 2) { break "hello" } })`, want: "1\n2\nhello\n(nil)\n"}, @@ -410,20 +410,20 @@ func TestBuiltins_Continue(t *testing.T) { want string }{ {desc: "continue unconditionally", expr: ` - foreach ["1" "2" "3"] { |v| + for ["1" "2" "3"] { |v| echo $v "s" continue echo $v "e" }`, want: "1s\n2s\n3s\n(nil)\n"}, {desc: "conditionally conditionally", expr: ` - foreach ["1" "2" "3"] { |v| + for ["1" "2" "3"] { |v| echo $v "s" if (eq $v "2") { continue } echo $v "e" }`, want: "1s\n1e\n2s\n3s\n3e\n(nil)\n"}, {desc: "continue inner loop only", expr: ` - foreach ["a" "b"] { |u| - foreach ["1" "2" "3"] { |v| + for ["a" "b"] { |u| + for ["1" "2" "3"] { |v| if (eq $v "2") { continue } echo $u $v } @@ -614,7 +614,7 @@ func TestBuiltins_Return(t *testing.T) { `, want: "Greet the\nHello, moon\n(nil)\n"}, {desc: "return in loop", expr: ` proc countdown { |nums| - foreach $nums { |n| + for $nums { |n| echo $n if (eq $n 3) { return "abort" @@ -639,7 +639,7 @@ func TestBuiltins_Return(t *testing.T) { } proc test-thing { - foreach [1 2 3] { |x| + for [1 2 3] { |x| do-thing { echo $x } @@ -654,7 +654,7 @@ func TestBuiltins_Return(t *testing.T) { } proc test-thing { - foreach [1 2 3] { |x| + for [1 2 3] { |x| do-thing (proc { echo $x }) @@ -669,7 +669,7 @@ func TestBuiltins_Return(t *testing.T) { } proc test-thing { - foreach [1 2 3] { |x| + for [1 2 3] { |x| $myClosure = proc { echo $x } do-thing $myClosure } @@ -688,7 +688,7 @@ func TestBuiltins_Return(t *testing.T) { } } - foreach (test-thing) { |y| call $y } + for (test-thing) { |y| call $y } `, want: "1\n2\n3\n(nil)\n"}, {desc: "check closure 5", expr: ` proc do-thing { |p| @@ -703,7 +703,7 @@ func TestBuiltins_Return(t *testing.T) { } $hello = "xx" - foreach (test-thing) { |y| call $y ; echo $hello } + for (test-thing) { |y| call $y ; echo $hello } `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, {desc: "check closure 7", expr: ` proc do-thing { |p| @@ -720,7 +720,7 @@ func TestBuiltins_Return(t *testing.T) { } $hello = "xx" - foreach (test-thing) { |y| call $y ; echo $hello } + for (test-thing) { |y| call $y ; echo $hello } `, want: "3\nxx\n3\nxx\n3\nxx\n(nil)\n"}, {desc: "check closure 7", expr: ` proc do-thing { |p| @@ -738,7 +738,7 @@ func TestBuiltins_Return(t *testing.T) { } $hello = "xx" - foreach (test-thing) { |y| call $y ; echo $hello } + for (test-thing) { |y| call $y ; echo $hello } `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, } @@ -756,6 +756,239 @@ func TestBuiltins_Return(t *testing.T) { } } +func TestBuiltins_Try(t *testing.T) { + tests := []struct { + desc string + expr string + want string + wantErr string + }{ + {desc: "try 1", expr: ` + try { + echo "Hello" + error "bang" + echo "World" + } catch { + echo "Caught" + } + `, want: "Hello\nCaught\n(nil)\n"}, + {desc: "try 2", expr: ` + try { + echo "Hello" + error "bang" + echo "World" + } finally { + echo "Always" + } + `, want: "Hello\nAlways\n", wantErr: "bang"}, + {desc: "try 3", expr: ` + try { + echo "Hello" + error "bang" + echo "World" + } catch { |e| + echo "Error was: ${e}" + } finally { + echo "Always" + } + `, want: "Hello\nError was: bang\nAlways\n(nil)\n"}, + {desc: "try 4", expr: ` + try { + echo "Hello" + echo "World" + } catch { |e| + echo "Should not call me" + } finally { + echo "Always" + } + `, want: "Hello\nWorld\nAlways\n(nil)\n"}, + {desc: "try 5", expr: ` + try { + echo "Hello" + + try { + echo "Nested" + error "bang" + echo "World" + } catch { |f| + echo "Catch me: $f" + } finally { + echo "Always 2" + } + } catch { |e| + echo "Should not call me" + } finally { + echo "Always" + } + `, want: "Hello\nNested\nCatch me: bang\nAlways 2\nAlways\n(nil)\n"}, + {desc: "try 6", expr: ` + try { + echo "Hello" + + try { + echo "Nested" + error "bang" + echo "World" + } finally { + echo "Always 2" + } + } catch { |e| + echo "Catch me: $e" + } finally { + echo "Always" + } + `, want: "Hello\nNested\nAlways 2\nCatch me: bang\nAlways\n(nil)\n"}, + {desc: "try 7", expr: ` + try { + echo "Hello" + error "bang" + } catch { |e| + echo "Catch me: $e" + error $e + } finally { + echo "Always" + } + `, want: "Hello\nCatch me: bang\nAlways\n", wantErr: "bang"}, + {desc: "try 8", expr: ` + try { + echo "Hello" + error "bang" + } catch { |e| + echo "Catch me: $e" + } catch { |e| + echo "Catch not me: $e" + } finally { + echo "Always" + } + `, want: "Hello\nCatch me: bang\nAlways\n(nil)\n"}, + {desc: "try 9", expr: ` + try { + echo "Hello" + error "bang" + } catch { |e| + echo "Catch me: $e" + error "boom" + } catch { |e| + echo "Catch me too: $e" + } finally { + echo "Always" + } + `, want: "Hello\nCatch me: bang\nCatch me too: boom\nAlways\n(nil)\n"}, + {desc: "try 10", expr: ` + try { + echo "Hello" + error "bang" + } catch { |e| + echo "Catch me: $e" + error "boom" + } catch { |e| + echo "Catch me too: $e" + error "mint" + } finally { + echo "Always" + } + `, want: "Hello\nCatch me: bang\nCatch me too: boom\nAlways\n", wantErr: "mint"}, + {desc: "try 11", expr: ` + try { + echo "Hello" + error "bang" + } catch { |e| + echo "Catch me: $e" + } finally { + echo "Always" + error "boom" + } + `, want: "Hello\nCatch me: bang\nAlways\n", wantErr: "boom"}, + {desc: "try 12", expr: ` + $a = try { "e" } catch { "f" } + echo $a + `, want: "e\n(nil)\n"}, + {desc: "try 13", expr: ` + $a = try { error "bang" } catch { "f" } + echo $a + `, want: "f\n(nil)\n"}, + {desc: "try 14", expr: ` + for [1 2 3] { |x| + try { + echo $x + continue + echo "No" + } catch { |e| + echo "Catch me: $x" + } finally { + echo "Never" + } + } + `, want: "1\n2\n3\n(nil)\n"}, + {desc: "try 15", expr: ` + for [1 2 3] { |x| + try { + echo $x + break + echo "No" + } catch { |e| + echo "Catch me: $x" + } finally { + echo "Never" + } + } + `, want: "1\n(nil)\n"}, + {desc: "try 16", expr: ` + for [1 2 3] { |x| + try { + echo $x + error "bang" + echo "No" + } catch { |e| + echo "Catch me at $x: $e" + } + } + `, want: "1\nCatch me at 1: bang\n2\nCatch me at 2: bang\n3\nCatch me at 3: bang\n(nil)\n"}, + {desc: "try 17", expr: ` + for [1 2 3] { |x| + try { + echo $x + error "bang" + echo "No" + } catch { |e| + echo "Catch me: $e" + break + } finally { + echo "Never" + } + } + `, want: "1\nCatch me: bang\n(nil)\n"}, + {desc: "try 18", expr: ` + for [1 2 3] { |x| + try { + echo $x + } finally { + echo "Always $x" + continue + } + } + `, want: "1\nAlways 1\n2\nAlways 2\n3\nAlways 3\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) + + if tt.wantErr != "" { + assert.Error(t, err) + assert.Equal(t, tt.wantErr, err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, outW.String()) + }) + } +} + func TestBuiltins_Seq(t *testing.T) { tests := []struct { desc string @@ -853,7 +1086,7 @@ func TestBuiltins_Map(t *testing.T) { $add2 = proc { |x| add $x 2 } $l = itr | map $add2 - foreach $l { |x| echo $x } + for $l { |x| echo $x } `, want: "3\n4\n5\n(nil)\n"}, } @@ -1106,7 +1339,7 @@ func TestBuiltins_Filter(t *testing.T) { }}, {desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}}, - {desc: "filter itr 1", expr: `$s = "" ; itr | filter { |x| ne $x 2 } | foreach { |x| $s = "$s $x" }; $s`, want: " 1 3"}, + {desc: "filter itr 1", expr: `$s = "" ; itr | filter { |x| ne $x 2 } | for { |x| $s = "$s $x" }; $s`, want: " 1 3"}, } for _, tt := range tests { diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 508d1e7..8b1951f 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -164,7 +164,7 @@ func TestInst_SetBuiltin(t *testing.T) { wantOut string }{ {descr: "return as is", expr: `countTo3`, want: []string{"1", "2", "3"}}, - {descr: "iterate over", expr: `foreach (countTo3) { |x| echo $x }`, wantOut: "1\n2\n3\n"}, + {descr: "iterate over", expr: `for (countTo3) { |x| echo $x }`, wantOut: "1\n2\n3\n"}, } for _, tt := range tests {