Fixed a panic when not is called with a nil value
All checks were successful
Build / build (push) Successful in 2m2s

Also added the "error" builtin.
This commit is contained in:
Leon Mika 2025-01-28 08:54:48 +11:00
parent 8fa2e3efb9
commit 2d56517de9
5 changed files with 74 additions and 23 deletions

View file

@ -20,6 +20,15 @@ echo [ARGS...]
Displays the string representation of ARGS to stdout followed by a new line. Displays the string representation of ARGS to stdout followed by a new line.
### error
```
error MSG
```
Raises an error with MSG as the given value. This will start unrolling the stack until a `try` block is
encountered, or until it reaches the top level stack, at which it will be displayed as a "runtime error".
### foreach ### foreach
``` ```

View file

@ -297,7 +297,12 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, err return nil, err
} }
return boolObject(!args.args[0].Truthy()), nil v := args.args[0]
if v == nil {
return boolObject(true), nil
}
return boolObject(!v.Truthy()), nil
} }
var errObjectsNotEqual = errors.New("objects not equal") var errObjectsNotEqual = errors.New("objects not equal")
@ -889,6 +894,13 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) {
return last, nil return last, nil
} }
func errorBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) < 1 {
return nil, errors.New("need at least one arguments")
}
return nil, errRuntime{val: args.args[0]}
}
func breakBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func breakBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) < 1 { if len(args.args) < 1 {
return nil, errBreak{} return nil, errBreak{}

View file

@ -92,6 +92,7 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("break", invokableFunc(breakBuiltin)) rootEC.addCmd("break", invokableFunc(breakBuiltin))
rootEC.addCmd("continue", invokableFunc(continueBuiltin)) rootEC.addCmd("continue", invokableFunc(continueBuiltin))
rootEC.addCmd("return", invokableFunc(returnBuiltin)) rootEC.addCmd("return", invokableFunc(returnBuiltin))
rootEC.addCmd("error", invokableFunc(errorBuiltin))
rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin))
rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) rootEC.addMacro("foreach", macroFunc(foreachBuiltin))

View file

@ -622,3 +622,14 @@ func (e errReturn) Error() string {
} }
var ErrHalt = errors.New("halt") var ErrHalt = errors.New("halt")
type errRuntime struct {
val Object
}
func (e errRuntime) Error() string {
if e.val == nil {
return "runtime error: (nil)"
}
return "runtime error: " + e.val.String()
}

View file

@ -3,7 +3,6 @@ package ucl
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
@ -43,13 +42,6 @@ func WithTestBuiltin() InstOption {
return &a, nil return &a, nil
})) }))
i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return nil, errors.New("an error occurred")
}
return nil, errors.New(args.args[0].String())
}))
i.rootEC.addCmd("joinpipe", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { i.rootEC.addCmd("joinpipe", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
sb := strings.Builder{} sb := strings.Builder{}
@ -230,7 +222,7 @@ func TestBuiltins_Try(t *testing.T) {
try { try {
error "bang" error "bang"
} }
echo "after"`, wantErr: "bang"}, echo "after"`, wantErr: "runtime error: bang"},
{desc: "try with catch - successful", expr: ` {desc: "try with catch - successful", expr: `
try { try {
echo "good" echo "good"
@ -251,7 +243,7 @@ func TestBuiltins_Try(t *testing.T) {
} catch { |err| } catch { |err|
echo (cat "the error was = " $err) echo (cat "the error was = " $err)
} }
echo "after"`, want: "the error was = error:bang\nafter\n(nil)\n"}, echo "after"`, want: "the error was = error:runtime error: bang\nafter\n(nil)\n"},
{desc: "try with two catch - successful", expr: ` {desc: "try with two catch - successful", expr: `
try { try {
echo "i'm good" echo "i'm good"
@ -279,18 +271,18 @@ func TestBuiltins_Try(t *testing.T) {
echo "wow, we made it here" echo "wow, we made it here"
} }
echo "after"`, want: "wow, we made it here\nafter\n(nil)\n"}, echo "after"`, want: "wow, we made it here\nafter\n(nil)\n"},
{desc: "return value - single try", expr: ` {desc: "return value - single try 1", expr: `
set x (try { error "bang" }) set x (try { error "bang" })
$x`, wantErr: "bang"}, $x`, wantErr: "runtime error: bang"},
{desc: "return value - single try", expr: ` {desc: "return value - single try 2", expr: `
set x (try { error "bang" } catch { |err| $err }) set x (try { error "bang" } catch { |err| $err })
$x`, want: "error:bang\n"}, $x`, want: "error:runtime error: bang\n"},
{desc: "return value - try and catch - successful", expr: ` {desc: "return value - try and catch - successful", expr: `
set x (try { error "bang" } catch { "hello" }) set x (try { error "bang" } catch { "hello" })
$x`, want: "hello\n"}, $x`, want: "hello\n"},
{desc: "return value - try and catch - unsuccessful", expr: ` {desc: "return value - try and catch - unsuccessful", expr: `
set x (try { error "bang" } catch { error "boom" }) set x (try { error "bang" } catch { error "boom" })
$x`, wantErr: "boom"}, $x`, wantErr: "runtime error: boom"},
{desc: "try with finally - successful", expr: ` {desc: "try with finally - successful", expr: `
try { try {
echo "all good" echo "all good"
@ -304,7 +296,7 @@ func TestBuiltins_Try(t *testing.T) {
} finally { } finally {
echo "always at end" echo "always at end"
} }
echo "after"`, want: "always at end\n", wantErr: "bang"}, echo "after"`, want: "always at end\n", wantErr: "runtime error: bang"},
{desc: "try with catch and finally - successful", expr: ` {desc: "try with catch and finally - successful", expr: `
try { try {
echo "all good" echo "all good"
@ -331,7 +323,7 @@ func TestBuiltins_Try(t *testing.T) {
} finally { } finally {
echo "always at end" echo "always at end"
} }
echo "after"`, want: "always at end\n", wantErr: "boom"}, echo "after"`, want: "always at end\n", wantErr: "runtime error: boom"},
{desc: "try with finally - finally result discarded", expr: ` {desc: "try with finally - finally result discarded", expr: `
set a (try { set a (try {
"return me" "return me"
@ -344,13 +336,13 @@ func TestBuiltins_Try(t *testing.T) {
error "bang" error "bang"
} finally { } finally {
error "kaboom" error "kaboom"
}`, wantErr: "bang"}, }`, wantErr: "runtime error: bang"},
{desc: "try with finally - error not discarded if try succeeds", expr: ` {desc: "try with finally - error not discarded if try succeeds", expr: `
try { try {
echo "all good" echo "all good"
} finally { } finally {
error "kaboom" error "kaboom"
}`, want: "all good\n", wantErr: "kaboom"}, }`, want: "all good\n", wantErr: "runtime error: kaboom"},
{desc: "try with finally with error - successful", expr: ` {desc: "try with finally with error - successful", expr: `
try { try {
echo "all good" echo "all good"
@ -359,13 +351,13 @@ func TestBuiltins_Try(t *testing.T) {
if (eq $err ()) { echo "that's nil" } if (eq $err ()) { echo "that's nil" }
} }
echo "after"`, want: "all good\nthe error was \nthat's nil\nafter\n(nil)\n"}, echo "after"`, want: "all good\nthe error was \nthat's nil\nafter\n(nil)\n"},
{desc: "try with finally - unsuccessful", expr: ` {desc: "try with finally - unsuccessful 2", expr: `
try { try {
error "bang" error "bang"
} finally { |err| } finally { |err|
echo (cat "the error was " $err) echo (cat "the error was " $err)
} }
echo "after"`, want: "the error was error:bang\n", wantErr: "bang"}, echo "after"`, want: "the error was error:runtime error: bang\n", wantErr: "runtime error: bang"},
{desc: "try with too many finallies - unsuccessful", expr: ` {desc: "try with too many finallies - unsuccessful", expr: `
try { try {
error "bang" error "bang"
@ -387,7 +379,7 @@ func TestBuiltins_Try(t *testing.T) {
} finally { } finally {
echo "outer" echo "outer"
} }
echo "after"`, want: "the error was error:bang\nouter caught\nouter\nafter\n(nil)\n"}, echo "after"`, want: "the error was error:runtime error: bang\nouter caught\nouter\nafter\n(nil)\n"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -1496,6 +1488,7 @@ func TestBuiltins_AndOrNot(t *testing.T) {
{desc: "not 1", expr: `not $true`, want: false}, {desc: "not 1", expr: `not $true`, want: false},
{desc: "not 2", expr: `not $false`, want: true}, {desc: "not 2", expr: `not $false`, want: true},
{desc: "not 3", expr: `not $false $true`, want: true}, {desc: "not 3", expr: `not $false $true`, want: true},
{desc: "not 4", expr: `not ()`, want: true},
{desc: "short circuit and 1", expr: `and "hello" "world"`, want: "world"}, {desc: "short circuit and 1", expr: `and "hello" "world"`, want: "world"},
{desc: "short circuit and 2", expr: `and () "world"`, want: nil}, {desc: "short circuit and 2", expr: `and () "world"`, want: nil},
@ -1564,6 +1557,31 @@ func TestBuiltins_Cat(t *testing.T) {
} }
} }
func TestBuiltins_Error(t *testing.T) {
tests := []struct {
desc string
expr string
wantErr string
}{
{desc: "error 1", expr: `error "bang"`, wantErr: "runtime error: bang"},
{desc: "error 2", expr: `error 123`, wantErr: "runtime error: 123"},
{desc: "error 3", expr: `error ()`, wantErr: "runtime error: (nil)"},
}
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 := inst.Eval(ctx, tt.expr)
assert.Error(t, err)
assert.Equal(t, tt.wantErr, err.Error())
})
}
}
func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error { func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error {
res, err := inst.eval(ctx, expr) res, err := inst.eval(ctx, expr)
if err != nil { if err != nil {