diff --git a/ucl/ast.go b/ucl/ast.go index d2444e1..2e0a493 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -68,6 +68,7 @@ type astDot struct { } type astCmd struct { + Pos lexer.Position Name astDot `parser:"@@"` Args []astDot `parser:"@@*"` } diff --git a/ucl/builtins.go b/ucl/builtins.go index 9d601ff..4ec30b9 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -770,17 +770,38 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { return nil, errors.New("malformed if-elif-else") } -func tryBuiltin(ctx context.Context, args macroArgs) (object, error) { +func tryBuiltin(ctx context.Context, args macroArgs) (_ object, fnErr error) { if args.nargs() < 1 { return nil, errors.New("need at least 1 arguments") } + var currError errObject + defer func() { + if errors.As(fnErr, &errBadUsage{}) { + return + } + + if args.nargs() >= 2 && args.identIs(ctx, args.nargs()-2, "finally") { + var blockArgs []object = nil + if fnErr != nil { + blockArgs = []object{errObject{err: fnErr}} + } + + _, err := args.evalBlock(ctx, args.nargs()-1, blockArgs, false) + if err != nil && fnErr == nil { + fnErr = err + } + } + }() + res, err := args.evalBlock(ctx, 0, nil, false) + args.shift(1) if err == nil { return res, nil } - args.shift(1) + currError = errObject{err: err} + for args.identIs(ctx, 0, "catch") { args.shift(1) @@ -788,51 +809,20 @@ func tryBuiltin(ctx context.Context, args macroArgs) (object, error) { return nil, errors.New("need at least 1 arguments") } - res, err := args.evalBlock(ctx, 0, nil, false) + res, err := args.evalBlock(ctx, 0, []object{currError}, false) if err == nil { return res, nil } - args.shift(2) + currError = errObject{err: err} + args.shift(1) } - // TODO: handle uncaught error + if args.identIs(ctx, 0, "finally") && args.nargs() > 2 { + return nil, tooManyFinallyBlocksError(args.ast.Pos) + } - return nil, nil - - /* - if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { - return args.evalBlock(ctx, 1, nil, false) - } else if err != nil { - return nil, err - } - - args.shift(2) - for args.identIs(ctx, 0, "elif") { - args.shift(1) - - if args.nargs() < 2 { - return nil, errors.New("need at least 2 arguments") - } - - if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { - return args.evalBlock(ctx, 1, nil, false) - } else if err != nil { - return nil, err - } - - args.shift(2) - } - - if args.identIs(ctx, 0, "else") && args.nargs() > 1 { - return args.evalBlock(ctx, 1, nil, false) - } else if args.nargs() == 0 { - // no elif or else - return nil, nil - } - - return nil, errors.New("malformed if-elif-else") - */ + return nil, currError.err } func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { diff --git a/ucl/errors.go b/ucl/errors.go new file mode 100644 index 0000000..6e0adb7 --- /dev/null +++ b/ucl/errors.go @@ -0,0 +1,37 @@ +package ucl + +import ( + "fmt" + "github.com/alecthomas/participle/v2/lexer" +) + +var ( + tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") +) + +type errorWithPos struct { + err error + pos lexer.Position +} + +func (e errorWithPos) Error() string { + return fmt.Sprintf("%v:%v - %v", e.pos.Line, e.pos.Offset, e.err.Error()) +} + +func (e errorWithPos) Unwrap() error { + return e.err +} + +type errBadUsage struct { + msg string +} + +func newBadUsage(msg string) func(pos lexer.Position) error { + return func(pos lexer.Position) error { + return errorWithPos{err: errBadUsage{msg: msg}, pos: pos} + } +} + +func (e errBadUsage) Error() string { + return "bad usage: " + e.msg +} diff --git a/ucl/objs.go b/ucl/objs.go index 1fa18e0..5544cf3 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -303,6 +303,16 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushSco return nil, errors.New("expected an invokable arg") } +type errObject struct{ err error } + +func (eo errObject) String() string { + return "error:" + eo.err.Error() +} + +func (eo errObject) Truthy() bool { + return true +} + type invocationArgs struct { eval evaluator inst *Inst diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index c13e683..81f321f 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -202,9 +202,10 @@ func TestBuiltins_If(t *testing.T) { func TestBuiltins_Try(t *testing.T) { tests := []struct { - desc string - expr string - want string + desc string + expr string + want string + wantErr string }{ {desc: "single try - successful", expr: ` try { @@ -215,7 +216,7 @@ func TestBuiltins_Try(t *testing.T) { try { error "bang" } - echo "after"`, want: "after\n(nil)\n"}, + echo "after"`, wantErr: "bang"}, {desc: "try with catch - successful", expr: ` try { echo "good" @@ -230,6 +231,149 @@ func TestBuiltins_Try(t *testing.T) { echo "something happened" } echo "after"`, want: "something happened\nafter\n(nil)\n"}, + {desc: "try with catch with passed in error - unsuccessful", expr: ` + try { + error "bang" + } catch { |err| + echo (cat "the error was = " $err) + } + echo "after"`, want: "the error was = error:bang\nafter\n(nil)\n"}, + {desc: "try with two catch - successful", expr: ` + try { + echo "i'm good" + } catch { + echo "i'm also good" + } catch { + echo "wow, we made it here" + } + echo "after"`, want: "i'm good\nafter\n(nil)\n"}, + {desc: "try with two catch - first unsuccessful", expr: ` + try { + error "bang" + } catch { + echo "i'm also good" + } catch { + echo "wow, we made it here" + } + echo "after"`, want: "i'm also good\nafter\n(nil)\n"}, + {desc: "try with two catch - both unsuccessful", expr: ` + try { + error "bang" + } catch { + error "boom" + } catch { + echo "wow, we made it here" + } + echo "after"`, want: "wow, we made it here\nafter\n(nil)\n"}, + {desc: "return value - single try", expr: ` + set x (try { error "bang" }) + $x`, wantErr: "bang"}, + {desc: "return value - single try", expr: ` + set x (try { error "bang" } catch { |err| $err }) + $x`, want: "error:bang\n"}, + {desc: "return value - try and catch - successful", expr: ` + set x (try { error "bang" } catch { "hello" }) + $x`, want: "hello\n"}, + {desc: "return value - try and catch - unsuccessful", expr: ` + set x (try { error "bang" } catch { error "boom" }) + $x`, wantErr: "boom"}, + {desc: "try with finally - successful", expr: ` + try { + echo "all good" + } finally { + echo "always at end" + } + echo "after"`, want: "all good\nalways at end\nafter\n(nil)\n"}, + {desc: "try with finally - unsuccessful", expr: ` + try { + error "bang" + } finally { + echo "always at end" + } + echo "after"`, want: "always at end\n", wantErr: "bang"}, + {desc: "try with catch and finally - successful", expr: ` + try { + echo "all good" + } catch { + echo "was caught" + } finally { + echo "always at end" + } + echo "after"`, want: "all good\nalways at end\nafter\n(nil)\n"}, + {desc: "try with catch and finally - unsuccessful", expr: ` + try { + error "bang" + } catch { + echo "was caught" + } finally { + echo "always at end" + } + echo "after"`, want: "was caught\nalways at end\nafter\n(nil)\n"}, + {desc: "try with catch and finally - catch unsuccessful", expr: ` + try { + error "bang" + } catch { + error "boom" + } finally { + echo "always at end" + } + echo "after"`, want: "always at end\n", wantErr: "boom"}, + {desc: "try with finally - finally result discarded", expr: ` + set a (try { + "return me" + } finally { + "not met" + }) + echo $a`, want: "return me\n(nil)\n"}, + {desc: "try with finally - error discarded if try fails result discarded", expr: ` + try { + error "bang" + } finally { + error "kaboom" + }`, wantErr: "bang"}, + {desc: "try with finally - error not discarded if try succeeds", expr: ` + try { + echo "all good" + } finally { + error "kaboom" + }`, want: "all good\n", wantErr: "kaboom"}, + {desc: "try with finally with error - successful", expr: ` + try { + echo "all good" + } finally { |err| + echo (cat "the error was " $err) + if (eq $err ()) { echo "that's nil" } + } + echo "after"`, want: "all good\nthe error was \nthat's nil\nafter\n(nil)\n"}, + {desc: "try with finally - unsuccessful", expr: ` + try { + error "bang" + } finally { |err| + echo (cat "the error was " $err) + } + echo "after"`, want: "the error was error:bang\n", wantErr: "bang"}, + {desc: "try with too many finallies - unsuccessful", expr: ` + try { + error "bang" + } finally { + echo "and do this" + } finally { |err| + echo (cat "the error was " $err) + } + echo "after"`, wantErr: "2:4 - bad usage: try needs at most 1 finally"}, + {desc: "try with finally in catch - unsuccessful", expr: ` + try { + try { + error "bang" + } finally { |err| + echo (cat "the error was " $err) + } + } catch { + echo "outer caught" + } finally { + echo "outer" + } + echo "after"`, want: "the error was error:bang\nouter caught\nouter\nafter\n(nil)\n"}, } for _, tt := range tests { @@ -240,7 +384,12 @@ func TestBuiltins_Try(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) err := EvalAndDisplay(ctx, inst, tt.expr) - assert.NoError(t, err) + 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()) }) }