Alternate signing constructs #1

Merged
lmika merged 12 commits from feature/assign into main 2025-05-24 00:12:24 +00:00
12 changed files with 633 additions and 241 deletions

View file

@ -14,7 +14,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.22.4
go-version: 1.24
- uses: actions/setup-node@v4
with:
node-version: 21.1

View file

@ -105,7 +105,7 @@ type astDot struct {
type astCmd struct {
Pos lexer.Position
Name astDot `parser:"@@"`
Args []astDot `parser:"@@*"`
InvokeArgs []astDot `parser:"@@*"`
}
type astPipeline struct {
@ -113,9 +113,15 @@ type astPipeline struct {
Rest []*astCmd `parser:"( PIPE @@ )*"`
}
type astAssignOrPipeline struct {
First *astCmd `parser:"@@"`
Assign *astPipeline `parser:"( EQ @@ "`
Pipeline []*astCmd `parser:"| ( PIPE @@ )+ )?"`
}
type astStatements struct {
First *astPipeline `parser:"@@"`
Rest []*astPipeline `parser:"( NL+ @@ )*"` // TODO: also add support for newlines
First *astAssignOrPipeline `parser:"@@"`
Rest []*astAssignOrPipeline `parser:"( NL+ @@ )*"` // TODO: also add support for newlines
}
type astScript struct {
@ -141,6 +147,7 @@ var scanner = lexer.MustStateful(lexer.Rules{
{"RC", `\}`, nil},
{"NL", `[;\n][; \n\t]*`, nil},
{"PIPE", `\|`, nil},
{"EQ", `=`, nil},
{"Ident", `[-]*[a-zA-Z_][\w-!?]*`, nil},
},
"String": {

View file

@ -180,64 +180,6 @@ func modBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return IntObject(n), nil
}
// TODO: this may need to be a macro
func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
name, err := args.stringArg(0)
if err != nil {
return nil, err
} else if len(name) == 0 {
return nil, fmt.Errorf("attempt to set empty string")
}
newVal := args.args[1]
if strings.HasPrefix(name, "@") {
pname := name[1:]
pvar, ok := args.ec.getPseudoVar(pname)
if ok {
if err := pvar.set(ctx, pname, newVal); err != nil {
return nil, err
}
return newVal, nil
}
if pvar := args.inst.missingPseudoVarHandler; pvar != nil {
if err := pvar.set(ctx, pname, newVal); err != nil {
return nil, err
}
return newVal, nil
}
return nil, fmt.Errorf("attempt to set '%v' to a non-existent pseudo-variable", name)
}
args.ec.setOrDefineVar(name, newVal)
return newVal, nil
}
func mustSetBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
name, err := args.stringArg(0)
if err != nil {
return nil, err
}
newVal := args.args[1]
if newVal == nil {
return nil, fmt.Errorf("attempt to set '%v' to a nil value", args.args[0])
}
args.ec.setOrDefineVar(name, newVal)
return newVal, nil
}
func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
@ -246,7 +188,7 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0]
r := args.args[1]
return boolObject(objectsEqual(l, r)), nil
return BoolObject(objectsEqual(l, r)), nil
}
func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -257,7 +199,7 @@ func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0]
r := args.args[1]
return boolObject(!objectsEqual(l, r)), nil
return BoolObject(!objectsEqual(l, r)), nil
}
func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -269,7 +211,7 @@ func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
return nil, err
}
return boolObject(isLess), nil
return BoolObject(isLess), nil
}
func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -281,7 +223,7 @@ func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
return nil, err
}
return boolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil
return BoolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil
}
func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -293,7 +235,7 @@ func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
return nil, err
}
return boolObject(isGreater), nil
return BoolObject(isGreater), nil
}
func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -305,7 +247,7 @@ func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
return nil, err
}
return boolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil
return BoolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil
}
func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -339,7 +281,7 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, err
}
return boolObject(!args.args[0].Truthy()), nil
return BoolObject(!args.args[0].Truthy()), nil
}
var errObjectsNotEqual = errors.New("objects not equal")
@ -358,8 +300,8 @@ func objectsEqual(l, r Object) bool {
if rv, ok := r.(IntObject); ok {
return lv == rv
}
case boolObject:
if rv, ok := r.(boolObject); ok {
case BoolObject:
if rv, ok := r.(BoolObject); ok {
return lv == rv
}
case Listable:
@ -446,7 +388,7 @@ func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, errors.New("cannot convert to int")
}
return IntObject(i), nil
case boolObject:
case BoolObject:
if v {
return IntObject(1), nil
}
@ -968,6 +910,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
@ -1103,6 +1111,31 @@ func returnBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, errReturn{ret: args.args[0]}
}
// TODO - add tests
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{args.args[0].String()}
}
func assertBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) < 1 {
return nil, errors.New("need at least one arguments")
}
if isTruthy(args.args[0]) {
return nil, nil
}
if len(args.args) > 1 {
return nil, ErrRuntime{args.args[1].String()}
}
return nil, ErrRuntime{"assertion failed"}
}
func procBuiltin(ctx context.Context, args macroArgs) (Object, error) {
if args.nargs() < 1 {
return nil, errors.New("need at least one arguments")

View file

@ -77,7 +77,7 @@ func split(ctx context.Context, args ucl.CallArgs) (any, error) {
}
}
return StringSlice(strings.SplitN(s, sep, n)), nil
return ucl.StringListObject(strings.SplitN(s, sep, n)), nil
}
func join(ctx context.Context, args ucl.CallArgs) (any, error) {
@ -128,5 +128,3 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) {
return nil, errors.New("expected listable or iterable as arg 1")
}
type StringSlice []string

View file

@ -140,17 +140,17 @@ func TestStrs_Split(t *testing.T) {
want any
wantErr bool
}{
{desc: "split 1", eval: `strs:split "1,2,3" ","`, want: builtins.StringSlice{"1", "2", "3"}},
{desc: "split 2", eval: `strs:split "1,2,3" ";"`, want: builtins.StringSlice{"1,2,3"}},
{desc: "split 3", eval: `strs:split "" ";"`, want: builtins.StringSlice{""}},
{desc: "split 4", eval: `strs:split " " ";"`, want: builtins.StringSlice{" "}},
{desc: "split 1", eval: `strs:split "1,2,3" ","`, want: []string{"1", "2", "3"}},
{desc: "split 2", eval: `strs:split "1,2,3" ";"`, want: []string{"1,2,3"}},
{desc: "split 3", eval: `strs:split "" ";"`, want: []string{""}},
{desc: "split 4", eval: `strs:split " " ";"`, want: []string{" "}},
{desc: "split by char 1", eval: `strs:split "123"`, want: builtins.StringSlice{"1", "2", "3"}},
{desc: "split by char 1", eval: `strs:split "123"`, want: []string{"1", "2", "3"}},
{desc: "split max 1", eval: `strs:split "1,2,3" "," -max 2`, want: builtins.StringSlice{"1", "2,3"}},
{desc: "split max 2", eval: `strs:split "1,2,3" "," -max 5`, want: builtins.StringSlice{"1", "2", "3"}},
{desc: "split max 1", eval: `strs:split "1,2,3" "," -max 2`, want: []string{"1", "2,3"}},
{desc: "split max 2", eval: `strs:split "1,2,3" "," -max 5`, want: []string{"1", "2", "3"}},
{desc: "split by char max 1", eval: `strs:split "12345" -max 3`, want: builtins.StringSlice{"1", "2", "345"}},
{desc: "split by char max 1", eval: `strs:split "12345" -max 3`, want: []string{"1", "2", "345"}},
{desc: "err 1", eval: `strs:split "1,2,3" -max []`, wantErr: true},
}

View file

@ -32,7 +32,7 @@ func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStateme
return nil, nil
}
res, err := e.evalPipeline(ctx, ec, n.First)
res, err := e.evalAssignOrPipeline(ctx, ec, n.First)
if err != nil {
return nil, err
}
@ -41,7 +41,7 @@ func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStateme
}
for _, rest := range n.Rest {
out, err := e.evalPipeline(ctx, ec, rest)
out, err := e.evalAssignOrPipeline(ctx, ec, rest)
if err != nil {
return nil, err
}
@ -50,6 +50,36 @@ func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStateme
return res, nil
}
func (e evaluator) evalAssignOrPipeline(ctx context.Context, ec *evalCtx, n *astAssignOrPipeline) (Object, error) {
switch {
case n.Assign != nil:
// Assignment
assignVal, err := e.evalPipeline(ctx, ec, n.Assign)
if err != nil {
return nil, err
}
return e.assignCmd(ctx, ec, n.First, assignVal)
case len(n.Pipeline) > 0:
res, err := e.evalCmd(ctx, ec, nil, n.First)
if err != nil {
return nil, err
}
for _, rest := range n.Pipeline {
out, err := e.evalCmd(ctx, ec, res, rest)
if err != nil {
return nil, err
}
res = out
}
return res, nil
}
return e.evalCmd(ctx, ec, nil, n.First)
}
func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
res, err := e.evalCmd(ctx, ec, nil, n.First)
if err != nil {
@ -85,7 +115,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object,
} else {
return nil, errors.New("unknown command: " + name)
}
case len(ast.Args) > 0:
case len(ast.InvokeArgs) > 0:
nameElem, err := e.evalDot(ctx, ec, ast.Name)
if err != nil {
return nil, err
@ -106,6 +136,13 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object,
return nameElem, nil
}
func (e evaluator) assignCmd(ctx context.Context, ec *evalCtx, ast *astCmd, toVal Object) (Object, error) {
if len(ast.InvokeArgs) != 0 {
return nil, errors.New("cannot assign to multiple values")
}
return e.assignDot(ctx, ec, ast.Name, toVal)
}
func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) {
var (
pargs ListObject
@ -117,7 +154,7 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe O
if currentPipe != nil {
argsPtr.Append(currentPipe)
}
for _, arg := range ast.Args {
for _, arg := range ast.InvokeArgs {
if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' {
// Arg switch
if kwargs == nil {
@ -176,6 +213,14 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object,
return res, nil
}
func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal Object) (Object, error) {
if len(n.DotSuffix) == 0 {
return e.assignArg(ctx, ec, n.Arg, toVal)
}
return nil, errors.New("TODO")
}
func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) {
switch {
case n.Literal != nil:
@ -211,6 +256,40 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
return nil, errors.New("unhandled arg type")
}
func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
switch {
case n.Literal != nil:
// We may use this for variable setting?
return nil, errors.New("cannot assign to a literal")
case n.Var != nil:
ec.setOrDefineVar(*n.Var, toVal)
return toVal, nil
case n.PseudoVar != nil:
pvar, ok := ec.getPseudoVar(*n.PseudoVar)
if ok {
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
return nil, err
}
return toVal, nil
}
if pvar := e.inst.missingPseudoVarHandler; pvar != nil {
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
return nil, err
}
return toVal, nil
}
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
case n.MaybeSub != nil:
return nil, errors.New("cannot assign to a subexpression")
case n.ListOrHash != nil:
return nil, errors.New("cannot assign to a list or hash")
case n.Block != nil:
return nil, errors.New("cannot assign to a block")
}
return nil, errors.New("unhandled arg type")
}
func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) {
if loh.EmptyList {
return &ListObject{}, nil

View file

@ -57,8 +57,6 @@ func New(opts ...InstOption) *Inst {
rootEC.root = rootEC
rootEC.addCmd("echo", invokableFunc(echoBuiltin))
rootEC.addCmd("set", invokableFunc(setBuiltin))
rootEC.addCmd("set!", invokableFunc(mustSetBuiltin))
rootEC.addCmd("len", invokableFunc(lenBuiltin))
rootEC.addCmd("index", invokableFunc(indexBuiltin))
rootEC.addCmd("call", invokableFunc(callBuiltin))
@ -96,11 +94,14 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("continue", invokableFunc(continueBuiltin))
rootEC.addCmd("return", invokableFunc(returnBuiltin))
rootEC.addCmd("error", invokableFunc(errorBuiltin))
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,
@ -151,7 +152,7 @@ func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) {
goRes, ok := toGoValue(res)
if !ok {
return nil, ErrNotConvertable
return res, nil
}
return goRes, nil
@ -207,7 +208,7 @@ func (n nativePseudoVarHandler) set(ctx context.Context, name string, v Object)
gv, ok := toGoValue(v)
if !ok {
return errors.New("cannot set non-matching type")
return mpvh.Set(ctx, v)
}
return mpvh.Set(ctx, gv)
@ -233,7 +234,7 @@ func (n nativeMissingPseudoVarHandler) set(ctx context.Context, name string, v O
gv, ok := toGoValue(v)
if !ok {
return errors.New("cannot set non-matching type")
return mpvh.Set(ctx, name, v)
}
return mpvh.Set(ctx, name, gv)

View file

@ -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"},
@ -22,15 +23,15 @@ func TestInst_Eval(t *testing.T) {
{desc: "simple ident", expr: `firstarg a-test`, want: "a-test"},
// String interpolation
{desc: "interpolate string 1", expr: `set what "world" ; firstarg "hello $what"`, want: "hello world"},
{desc: "interpolate string 2", expr: `set what "world" ; set when "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
{desc: "interpolate string 3", expr: `set what "world" ; set when "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
{desc: "interpolate string 4", expr: `set crazy [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
{desc: "interpolate string 6", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
{desc: "interpolate string 7", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
{desc: "interpolate string 8", expr: `set words ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
{desc: "interpolate string 9", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
{desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"},
{desc: "interpolate string 2", expr: `$what = "world" ; $when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
{desc: "interpolate string 3", expr: `$what = "world" ; $when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
{desc: "interpolate string 4", expr: `$crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
{desc: "interpolate string 6", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
{desc: "interpolate string 7", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
{desc: "interpolate string 8", expr: `$words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
{desc: "interpolate string 9", expr: `$what = "world" ; firstarg "hello $($what)"`, want: "hello world"},
{desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
{desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
{desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
@ -57,53 +58,53 @@ func TestInst_Eval(t *testing.T) {
// Multi-statements
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
{desc: "multi 2", expr: `list "hello" | toUpper ; firstarg "world"`, want: "world"},
{desc: "multi 3", expr: `set new "this is new" ; firstarg $new`, want: "this is new"},
{desc: "multi 3", expr: `$new = "this is new" ; firstarg $new`, want: "this is new"},
// Lists
{desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}},
{desc: "list 2", expr: `set one "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}},
{desc: "list 2", expr: `$one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}},
{desc: "list 3", expr: `firstarg []`, want: []any{}},
{desc: "list 4", expr: `set x ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}},
{desc: "list 4", expr: `$x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}},
// Maps
{desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
{desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
{desc: "map 3", expr: `
set one "one" ; set n1 "1"
$one = "one" ; $n1 = "1"
firstarg [
$one:$n1
(list "two" | map { |x| toUpper $x } | head):(list "2" | map { |x| toUpper $x } | head)
three:"3"
]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}},
{desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}},
{desc: "map 5", expr: `set x ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}},
{desc: "map 6", expr: `set x [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}},
{desc: "map 5", expr: `$x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}},
{desc: "map 6", expr: `$x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}},
// Dots
{desc: "dot expr 1", expr: `set x [1 2 3] ; $x.(0)`, want: 1},
{desc: "dot expr 2", expr: `set x [1 2 3] ; $x.(1)`, want: 2},
{desc: "dot expr 3", expr: `set x [1 2 3] ; $x.(2)`, want: 3},
{desc: "dot expr 4", expr: `set x [1 2 3] ; $x.(3)`, want: nil},
{desc: "dot expr 5", expr: `set x [1 2 3] ; $x.(add 1 1)`, want: 3},
{desc: "dot expr 6", expr: `set x [1 2 3] ; $x.(-1)`, want: 3},
{desc: "dot expr 7", expr: `set x [1 2 3] ; $x.(-2)`, want: 2},
{desc: "dot expr 8", expr: `set x [1 2 3] ; $x.(-3)`, want: 1},
{desc: "dot expr 9", expr: `set x [1 2 3] ; $x.(-4)`, want: nil},
{desc: "dot expr 1", expr: `$x = [1 2 3] ; $x.(0)`, want: 1},
{desc: "dot expr 2", expr: `$x = [1 2 3] ; $x.(1)`, want: 2},
{desc: "dot expr 3", expr: `$x = [1 2 3] ; $x.(2)`, want: 3},
{desc: "dot expr 4", expr: `$x = [1 2 3] ; $x.(3)`, want: nil},
{desc: "dot expr 5", expr: `$x = [1 2 3] ; $x.(add 1 1)`, want: 3},
{desc: "dot expr 6", expr: `$x = [1 2 3] ; $x.(-1)`, want: 3},
{desc: "dot expr 7", expr: `$x = [1 2 3] ; $x.(-2)`, want: 2},
{desc: "dot expr 8", expr: `$x = [1 2 3] ; $x.(-3)`, want: 1},
{desc: "dot expr 9", expr: `$x = [1 2 3] ; $x.(-4)`, want: nil},
{desc: "dot idents 1", expr: `set x [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"},
{desc: "dot idents 2", expr: `set x [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"},
{desc: "dot idents 3", expr: `set x [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil},
{desc: "dot idents 4", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"},
{desc: "dot idents 5", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"},
{desc: "dot idents 6", expr: `set x [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil},
{desc: "dot idents 7", expr: `set x [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"},
{desc: "dot idents 8", expr: `set x [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"},
{desc: "dot idents 9", expr: `set x [MORE:"stuff"] ; x.y`, want: nil},
{desc: "dot idents 1", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"},
{desc: "dot idents 2", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"},
{desc: "dot idents 3", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil},
{desc: "dot idents 4", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"},
{desc: "dot idents 5", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"},
{desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil},
{desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"},
{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)
@ -136,9 +141,9 @@ func TestInst_SetPseudoVar(t *testing.T) {
{desc: "read var 2", expr: `@bar`, wantRes: "this is bar"},
{desc: "read var 3", expr: `@fla`, wantRes: "missing value of fla"},
{desc: "write var 1", expr: `set "@foo" "hello" ; @foo`, wantRes: "hello"},
{desc: "write var 2", expr: `set "@bar" "world" ; @bar`, wantRes: "world", wantBarVar: "world"},
{desc: "write var 3", expr: `set "@blong" "hello"`, wantErr: true},
{desc: "write var 1", expr: `@foo = "hello" ; @foo`, wantRes: "hello"},
{desc: "write var 2", expr: `@bar = "world" ; @bar`, wantRes: "world", wantBarVar: "world"},
{desc: "write var 3", expr: `@blong = "hello"`, wantErr: true},
}
for _, tt := range tests {

View file

@ -80,6 +80,24 @@ func (s *ListObject) Index(i int) Object {
return (*s)[i]
}
type StringListObject []string
func (ss StringListObject) String() string {
return fmt.Sprintf("[%v]", strings.Join(ss, " "))
}
func (ss StringListObject) Truthy() bool {
return len(ss) > 0
}
func (ss StringListObject) Len() int {
return len(ss)
}
func (ss StringListObject) Index(i int) Object {
return StringObject(ss[i])
}
type iteratorObject struct {
Iterable
}
@ -154,26 +172,26 @@ func (i IntObject) Truthy() bool {
return i != 0
}
type boolObject bool
type BoolObject bool
func (b boolObject) String() string {
func (b BoolObject) String() string {
if b {
return "true"
}
return "false"
}
func (b boolObject) Truthy() bool {
func (b BoolObject) Truthy() bool {
return bool(b)
}
type timeObject time.Time
type TimeObject time.Time
func (t timeObject) String() string {
func (t TimeObject) String() string {
return time.Time(t).Format(time.RFC3339)
}
func (t timeObject) Truthy() bool {
func (t TimeObject) Truthy() bool {
return !time.Time(t).IsZero()
}
@ -185,9 +203,11 @@ func toGoValue(obj Object) (interface{}, bool) {
return string(v), true
case IntObject:
return int(v), true
case boolObject:
case StringListObject:
return []string(v), true
case BoolObject:
return bool(v), true
case timeObject:
case TimeObject:
return time.Time(v), true
case *ListObject:
xs := make([]interface{}, 0, len(*v))
@ -237,9 +257,9 @@ func fromGoValue(v any) (Object, error) {
case int:
return IntObject(t), nil
case bool:
return boolObject(t), nil
return BoolObject(t), nil
case time.Time:
return timeObject(t), nil
return TimeObject(t), nil
}
return fromGoReflectValue(reflect.ValueOf(v))
@ -279,7 +299,7 @@ type macroArgs struct {
}
func (ma macroArgs) nargs() int {
return len(ma.ast.Args[ma.argShift:])
return len(ma.ast.InvokeArgs[ma.argShift:])
}
func (ma *macroArgs) shift(n int) {
@ -287,15 +307,15 @@ func (ma *macroArgs) shift(n int) {
}
func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bool {
if n >= len(ma.ast.Args[ma.argShift:]) {
if n >= len(ma.ast.InvokeArgs[ma.argShift:]) {
return false
}
if len(ma.ast.Args[ma.argShift+n].DotSuffix) != 0 {
if len(ma.ast.InvokeArgs[ma.argShift+n].DotSuffix) != 0 {
return false
}
lit := ma.ast.Args[ma.argShift+n].Arg.Ident
lit := ma.ast.InvokeArgs[ma.argShift+n].Arg.Ident
if lit == nil {
return false
}
@ -304,15 +324,15 @@ func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bo
}
func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) {
if ma.argShift >= len(ma.ast.Args) {
if ma.argShift >= len(ma.ast.InvokeArgs) {
return "", false
}
if len(ma.ast.Args[ma.argShift].DotSuffix) != 0 {
if len(ma.ast.InvokeArgs[ma.argShift].DotSuffix) != 0 {
return "", false
}
lit := ma.ast.Args[ma.argShift].Arg.Ident
lit := ma.ast.InvokeArgs[ma.argShift].Arg.Ident
if lit != nil {
ma.argShift += 1
return lit.String(), true
@ -321,11 +341,11 @@ func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) {
}
func (ma macroArgs) evalArg(ctx context.Context, n int) (Object, error) {
if n >= len(ma.ast.Args[ma.argShift:]) {
if n >= len(ma.ast.InvokeArgs[ma.argShift:]) {
return nil, errors.New("not enough arguments") // FIX
}
return ma.eval.evalDot(ctx, ma.ec, ma.ast.Args[ma.argShift+n])
return ma.eval.evalDot(ctx, ma.ec, ma.ast.InvokeArgs[ma.argShift+n])
}
func (ma macroArgs) evalBlock(ctx context.Context, n int, args []Object, pushScope bool) (Object, error) {
@ -367,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 {
@ -623,6 +643,14 @@ func (p OpaqueObject) Truthy() bool {
return p.v != nil
}
type ErrRuntime struct {
errStr string
}
func (e ErrRuntime) Error() string {
return e.errStr
}
type errBreak struct {
isCont bool
ret Object
@ -644,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)
}

View file

@ -131,12 +131,12 @@ func TestBuiltins_Echo(t *testing.T) {
echo "world" # command after this
;
`, want: "Hello\nworld\n"},
{desc: "interpolated string 1", expr: `set what "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
{desc: "interpolated string 2", expr: `set what "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
{desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
{desc: "interpolated string 2", expr: `$what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
{desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"},
{desc: "interpolated string 4", expr: `set what "Hello" ; set where "world" ; echo "$what, $where"`, want: "Hello, world\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"},
@ -167,19 +167,19 @@ func TestBuiltins_If(t *testing.T) {
want string
}{
{desc: "single then", expr: `
set x "Hello"
$x = "Hello"
if $x {
echo "true"
}`, want: "true\n(nil)\n"},
{desc: "single then and else", expr: `
set x "Hello"
$x = "Hello"
if $x {
echo "true"
} else {
echo "false"
}`, want: "true\n(nil)\n"},
{desc: "single then, elif and else", expr: `
set x "Hello"
$x = "Hello"
if $y {
echo "y is true"
} elif $x {
@ -188,14 +188,14 @@ func TestBuiltins_If(t *testing.T) {
echo "nothings x"
}`, want: "x is true\n(nil)\n"},
{desc: "single then and elif, no else", expr: `
set x "Hello"
$x = "Hello"
if $y {
echo "y is true"
} elif $x {
echo "x is true"
}`, want: "x is true\n(nil)\n"},
{desc: "single then, two elif, and else", expr: `
set x "Hello"
$x = "Hello"
if $z {
echo "z is true"
} elif $y {
@ -213,15 +213,15 @@ func TestBuiltins_If(t *testing.T) {
} else {
echo "none is true"
}`, want: "none is true\n(nil)\n"},
{desc: "compressed then", expr: `set x "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"},
{desc: "compressed then", expr: `$x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"},
{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: `set i (itr) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 2", expr: `set 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: `set i (itr) ; foreach (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 4", expr: `set 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: `set i (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 6", expr: `set i (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\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 ; 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"},
}
for _, tt := range tests {
@ -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 {
@ -281,53 +281,53 @@ func TestBuiltins_While(t *testing.T) {
want string
}{
{desc: "iterate while true 1", expr: `
set x 0
$x = 0
while (lt $x 5) {
echo $x
set x (add $x 1)
$x = (add $x 1)
}
echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"},
{desc: "iterate while true 2", expr: `
set x 20
$x = 20
while (lt $x 5) {
echo $x
set x (add $x 1)
$x = (add $x 1)
}
echo "done"`, want: "done\n(nil)\n"},
{desc: "iterate while true with pipeline", expr: `
set x 0
$x = 0
while (lt $x 5) {
echo $x
set x (add $x 1)
$x = (add $x 1)
if (ge $x 3) {
break "Ahh"
}
} | echo " was the break"
echo "done"`, want: "0\n1\n2\nAhh was the break\ndone\n(nil)\n"},
{desc: "iterate for ever with break 1", expr: `
set x 0
$x = 0
while {
echo $x
set x (add $x 1)
$x = (add $x 1)
if (ge $x 5) {
break
}
}
echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"},
{desc: "iterate for ever with break 2", expr: `
set x 0
$x = 0
echo (while {
echo $x
set x (add $x 1)
$x = add $x 1
if (ge $x 5) {
break $x
}
})
`, want: "0\n1\n2\n3\n4\n5\n(nil)\n"},
{desc: "iterate for ever with continue", expr: `
set x 0
$x = 0
while {
set x (add $x 1)
$x = (add $x 1)
if (or (eq $x 2) (eq $x 4)) {
echo "quack"
continue
@ -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
}
@ -487,10 +487,10 @@ func TestBuiltins_Procs(t *testing.T) {
}
}
set helloGreater (makeGreeter "Hello")
$helloGreater = makeGreeter "Hello"
$helloGreater "world"
set goodbye (makeGreeter "Goodbye cruel")
$goodbye = makeGreeter "Goodbye cruel"
$goodbye "world"
call (makeGreeter "Quick") ["call me"]
@ -498,13 +498,13 @@ func TestBuiltins_Procs(t *testing.T) {
`, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"},
{desc: "modifying closed over variables", expr: `
proc makeSetter {
set bla "X"
$bla = "X"
proc appendToBla { |x|
set bla (cat $bla $x)
$bla = cat $bla $x
}
}
set er (makeSetter)
$er = makeSetter
echo (call $er ["xxx"])
echo (call $er ["yyy"])
`, want: "Xxxx\nXxxxyyy\n(nil)\n"},
@ -606,7 +606,7 @@ func TestBuiltins_Return(t *testing.T) {
echo "world"
}
proc greet {
set what (greetWhat)
$what = (greetWhat)
echo "Hello, " $what
}
@ -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,8 +669,8 @@ func TestBuiltins_Return(t *testing.T) {
}
proc test-thing {
foreach [1 2 3] { |x|
set myClosure (proc { echo $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|
@ -697,13 +697,13 @@ func TestBuiltins_Return(t *testing.T) {
proc test-thing {
[1 2 3] | map { |x|
set myProc (proc { echo $x })
$myProc = proc { echo $x }
proc { do-thing $myProc }
}
}
set hello "xx"
foreach (test-thing) { |y| call $y ; echo $hello }
$hello = "xx"
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|
@ -711,16 +711,16 @@ func TestBuiltins_Return(t *testing.T) {
}
proc test-thing {
set f 0
$f = 0
[1 2 3] | map { |x|
set myProc (proc { echo $f })
set f (add $f 1)
$myProc = proc { echo $f }
$f = (add $f 1)
proc { do-thing $myProc }
}
}
set hello "xx"
foreach (test-thing) { |y| call $y ; echo $hello }
$hello = "xx"
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|
@ -728,17 +728,17 @@ func TestBuiltins_Return(t *testing.T) {
}
proc test-thing {
set f 1
$f = 1
[1 2 3] | map { |x|
set g $f
set myProc (proc { echo $g })
set f (add $f 1)
$g = $f
$myProc = (proc { echo $g })
$f = (add $f 1)
proc { do-thing $myProc }
}
}
set hello "xx"
foreach (test-thing) { |y| call $y ; echo $hello }
$hello = "xx"
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
@ -831,12 +1064,12 @@ func TestBuiltins_Map(t *testing.T) {
map ["a" "b" "c"] (proc { |x| makeUpper $x })
`, want: "A\nB\nC\n"},
{desc: "map list 2", expr: `
set makeUpper (proc { |x| $x | toUpper })
$makeUpper = proc { |x| $x | toUpper }
map ["a" "b" "c"] $makeUpper
`, want: "A\nB\nC\n"},
{desc: "map list with pipe", expr: `
set makeUpper (proc { |x| $x | toUpper })
$makeUpper = proc { |x| $x | toUpper }
["a" "b" "c"] | map $makeUpper
`, want: "A\nB\nC\n"},
@ -844,16 +1077,16 @@ func TestBuiltins_Map(t *testing.T) {
map ["a" "b" "c"] { |x| toUpper $x }
`, want: "A\nB\nC\n"},
{desc: "map list with stream", expr: `
set makeUpper (proc { |x| toUpper $x })
$makeUpper = proc { |x| toUpper $x }
set l (["a" "b" "c"] | map $makeUpper)
$l = ["a" "b" "c"] | map $makeUpper
echo $l
`, want: "[A B C]\n(nil)\n"},
{desc: "map itr stream", expr: `
set add2 (proc { |x| add $x 2 })
$add2 = proc { |x| add $x 2 }
set l (itr | map $add2)
foreach $l { |x| echo $x }
$l = itr | map $add2
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: `set s "" ; itr | filter { |x| ne $x 2 } | foreach { |x| set 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 {
@ -1158,10 +1391,10 @@ func TestBuiltins_Head(t *testing.T) {
{desc: "head list 1", expr: `head [1 2 3]`, want: 1},
{desc: "head itr 1", expr: `head (itr)`, want: 1},
{desc: "head itr 2", expr: `set h (itr) ; head $h`, want: 1},
{desc: "head itr 3", expr: `set h (itr) ; head $h ; head $h`, want: 2},
{desc: "head itr 4", expr: `set h (itr) ; head $h ; head $h ; head $h`, want: 3},
{desc: "head itr 5", expr: `set h (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil},
{desc: "head itr 2", expr: `$h = (itr) ; head $h`, want: 1},
{desc: "head itr 3", expr: `$h = (itr) ; head $h ; head $h`, want: 2},
{desc: "head itr 4", expr: `$h = (itr) ; head $h ; head $h ; head $h`, want: 3},
{desc: "head itr 5", expr: `$h = (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil},
}
for _, tt := range tests {
@ -1497,7 +1730,7 @@ func TestBuiltins_Cat(t *testing.T) {
{desc: "cat 6", expr: `cat "array = " [1 3 2 4]`, want: "array = [1 3 2 4]"},
{desc: "cat 7", expr: `cat 1 $true 3 [4]`, want: "1true3[4]"},
{desc: "cat 8", expr: `cat`, want: ""},
{desc: "cat 9", expr: `set x ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"},
{desc: "cat 9", expr: `$x = ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"},
}
for _, tt := range tests {

View file

@ -165,7 +165,7 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error {
return bindProxyObject(v, t.v)
}
return nil
return bindProxyObject(v, reflect.ValueOf(arg))
}
func canBindArg(v interface{}, arg Object) bool {

View file

@ -124,7 +124,7 @@ func TestInst_SetBuiltin(t *testing.T) {
want string
}{
{descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"},
{descr: "pass via vars", expr: `set x (add2 "blue" "green") ; join $x`, want: "blue:green"},
{descr: "pass via vars", expr: `$x = (add2 "blue" "green") ; join $x`, want: "blue:green"},
}
for _, tt := range tests {
@ -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 {