Merge remote-tracking branch 'origin/main'
All checks were successful
Build / build (push) Successful in 2m1s

This commit is contained in:
Leon Mika 2025-01-30 22:16:25 +11:00
commit e55f0e1ef3
8 changed files with 156 additions and 26 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")
@ -1021,6 +1026,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

@ -186,7 +186,16 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
if v, ok := ec.getVar(*n.Var); ok { if v, ok := ec.getVar(*n.Var); ok {
return v, nil return v, nil
} }
return nil, nil
if e.inst.missingVarHandler != nil {
dv, err := e.inst.missingVarHandler(ctx, *n.Var)
if err != nil {
return nil, err
}
return fromGoValue(dv)
}
return nil, errors.New("undefined variable: " + *n.Var)
case n.MaybeSub != nil: case n.MaybeSub != nil:
sub := n.MaybeSub.Sub sub := n.MaybeSub.Sub
if sub == nil { if sub == nil {

View file

@ -11,6 +11,7 @@ import (
type Inst struct { type Inst struct {
out io.Writer out io.Writer
missingBuiltinHandler MissingBuiltinHandler missingBuiltinHandler MissingBuiltinHandler
missingVarHandler MissingVarHandler
echoPrinter EchoPrinter echoPrinter EchoPrinter
rootEC *evalCtx rootEC *evalCtx
@ -30,6 +31,12 @@ func WithMissingBuiltinHandler(handler MissingBuiltinHandler) InstOption {
} }
} }
func WithMissingVarHandler(handler MissingVarHandler) InstOption {
return func(i *Inst) {
i.missingVarHandler = handler
}
}
func WithModule(module Module) InstOption { func WithModule(module Module) InstOption {
return func(i *Inst) { return func(i *Inst) {
for name, builtin := range module.Builtins { for name, builtin := range module.Builtins {
@ -92,6 +99,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

@ -119,6 +119,64 @@ func TestInst_Eval(t *testing.T) {
} }
} }
func TestInst_MissingVarHandler(t *testing.T) {
tests := []struct {
desc string
expr string
handler ucl.MissingVarHandler
want any
wantErr string
}{
{
desc: "default - error",
expr: `$bla`,
wantErr: "undefined variable: bla",
},
{
desc: "handler - set to nil",
handler: func(ctx context.Context, name string) (any, error) {
return nil, nil
},
expr: `$bla`,
want: nil,
},
{
desc: "handler - set to a string 1",
handler: func(ctx context.Context, name string) (any, error) {
return "I am " + name, nil
},
expr: `$bla`,
want: "I am bla",
},
{
desc: "handler - set to a string 2",
handler: func(ctx context.Context, name string) (any, error) {
return "I am " + name, nil
},
expr: `$something`,
want: "I am something",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin(), ucl.WithMissingVarHandler(tt.handler))
res, err := inst.Eval(ctx, tt.expr)
if tt.wantErr != "" {
assert.Error(t, err)
assert.Equal(t, err.Error(), tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
}
})
}
}
var parseComments1 = ` var parseComments1 = `
proc lookup { |file| proc lookup { |file|
foreach { |toks| foreach { |toks|

View file

@ -648,3 +648,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"
@ -58,13 +57,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,9 @@ func TestBuiltins_If(t *testing.T) {
ctx := context.Background() ctx := context.Background()
outW := bytes.NewBuffer(nil) outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin(), WithMissingVarHandler(func(ctx context.Context, name string) (any, error) {
return nil, nil
}))
err := evalAndDisplay(ctx, inst, tt.expr) err := evalAndDisplay(ctx, inst, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
@ -255,7 +249,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"
@ -276,7 +270,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"
@ -304,18 +298,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"
@ -329,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: "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"
@ -356,7 +350,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"
@ -369,13 +363,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"
@ -384,13 +378,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"
@ -412,7 +406,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 {
@ -420,7 +414,9 @@ func TestBuiltins_Try(t *testing.T) {
ctx := context.Background() ctx := context.Background()
outW := bytes.NewBuffer(nil) outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin(), WithMissingVarHandler(func(ctx context.Context, name string) (any, error) {
return nil, nil
}))
err := evalAndDisplay(ctx, inst, tt.expr) err := evalAndDisplay(ctx, inst, tt.expr)
if tt.wantErr != "" { if tt.wantErr != "" {
@ -1568,6 +1564,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},
@ -1636,6 +1633,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 {

View file

@ -11,6 +11,7 @@ import (
type BuiltinHandler func(ctx context.Context, args CallArgs) (any, error) type BuiltinHandler func(ctx context.Context, args CallArgs) (any, error)
type MissingBuiltinHandler func(ctx context.Context, name string, args CallArgs) (any, error) type MissingBuiltinHandler func(ctx context.Context, name string, args CallArgs) (any, error)
type MissingVarHandler func(ctx context.Context, name string) (any, error)
type CallArgs struct { type CallArgs struct {
args invocationArgs args invocationArgs