Finished implementing try catch
Build / build (push) Successful in 2m5s Details

This commit is contained in:
Leon Mika 2024-10-21 22:00:06 +11:00
parent 5b913266e9
commit 2f54a9311e
5 changed files with 232 additions and 45 deletions

View File

@ -68,6 +68,7 @@ type astDot struct {
} }
type astCmd struct { type astCmd struct {
Pos lexer.Position
Name astDot `parser:"@@"` Name astDot `parser:"@@"`
Args []astDot `parser:"@@*"` Args []astDot `parser:"@@*"`
} }

View File

@ -770,17 +770,38 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
return nil, errors.New("malformed if-elif-else") 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 { if args.nargs() < 1 {
return nil, errors.New("need at least 1 arguments") 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) res, err := args.evalBlock(ctx, 0, nil, false)
args.shift(1)
if err == nil { if err == nil {
return res, nil return res, nil
} }
args.shift(1) currError = errObject{err: err}
for args.identIs(ctx, 0, "catch") { for args.identIs(ctx, 0, "catch") {
args.shift(1) 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") 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 { if err == nil {
return res, 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 return nil, currError.err
/*
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")
*/
} }
func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) {

37
ucl/errors.go Normal file
View File

@ -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
}

View File

@ -303,6 +303,16 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushSco
return nil, errors.New("expected an invokable arg") 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 { type invocationArgs struct {
eval evaluator eval evaluator
inst *Inst inst *Inst

View File

@ -202,9 +202,10 @@ func TestBuiltins_If(t *testing.T) {
func TestBuiltins_Try(t *testing.T) { func TestBuiltins_Try(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
expr string expr string
want string want string
wantErr string
}{ }{
{desc: "single try - successful", expr: ` {desc: "single try - successful", expr: `
try { try {
@ -215,7 +216,7 @@ func TestBuiltins_Try(t *testing.T) {
try { try {
error "bang" error "bang"
} }
echo "after"`, want: "after\n(nil)\n"}, echo "after"`, wantErr: "bang"},
{desc: "try with catch - successful", expr: ` {desc: "try with catch - successful", expr: `
try { try {
echo "good" echo "good"
@ -230,6 +231,149 @@ func TestBuiltins_Try(t *testing.T) {
echo "something happened" echo "something happened"
} }
echo "after"`, want: "something happened\nafter\n(nil)\n"}, 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 { for _, tt := range tests {
@ -240,7 +384,12 @@ func TestBuiltins_Try(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
err := EvalAndDisplay(ctx, inst, tt.expr) 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()) assert.Equal(t, tt.want, outW.String())
}) })
} }