Added break, continue, and return

This commit is contained in:
Leon Mika 2024-04-30 20:55:06 +10:00
parent fb2da4928c
commit d5ddecce33
4 changed files with 230 additions and 4 deletions

View file

@ -68,6 +68,10 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) {
if rv, ok := r.(strObject); ok { if rv, ok := r.(strObject); ok {
return boolObject(lv == rv), nil return boolObject(lv == rv), nil
} }
case intObject:
if rv, ok := r.(intObject); ok {
return boolObject(lv == rv), nil
}
} }
return boolObject(false), nil return boolObject(false), nil
} }
@ -309,7 +313,10 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) {
blockIdx = 0 blockIdx = 0
} }
var last object var (
last object
breakErr errBreak
)
switch t := items.(type) { switch t := items.(type) {
case listable: case listable:
@ -318,14 +325,26 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) {
v := t.Index(i) v := t.Index(i)
last, err = args.evalBlock(ctx, blockIdx, []object{v}, true) // TO INCLUDE: the index last, err = args.evalBlock(ctx, blockIdx, []object{v}, true) // TO INCLUDE: the index
if err != nil { if err != nil {
return nil, err if errors.As(err, &breakErr) {
if !breakErr.isCont {
return breakErr.ret, nil
}
} else {
return nil, err
}
} }
} }
case hashObject: case hashObject:
for k, v := range t { for k, v := range t {
last, err = args.evalBlock(ctx, blockIdx, []object{strObject(k), v}, true) last, err = args.evalBlock(ctx, blockIdx, []object{strObject(k), v}, true)
if err != nil { if err != nil {
return nil, err if errors.As(err, &breakErr) {
if !breakErr.isCont {
return breakErr.ret, nil
}
} else {
return nil, err
}
} }
} }
} }
@ -333,6 +352,24 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) {
return last, nil return last, nil
} }
func breakBuiltin(ctx context.Context, args invocationArgs) (object, error) {
if len(args.args) < 1 {
return nil, errBreak{}
}
return nil, errBreak{ret: args.args[0]}
}
func continueBuiltin(ctx context.Context, args invocationArgs) (object, error) {
return nil, errBreak{isCont: true}
}
func returnBuiltin(ctx context.Context, args invocationArgs) (object, error) {
if len(args.args) < 1 {
return nil, errReturn{}
}
return nil, errReturn{ret: args.args[0]}
}
func procBuiltin(ctx context.Context, args macroArgs) (object, error) { func procBuiltin(ctx context.Context, args macroArgs) (object, error) {
if args.nargs() < 1 { if args.nargs() < 1 {
return nil, errors.New("need at least one arguments") return nil, errors.New("need at least one arguments")
@ -388,5 +425,13 @@ func (b procObject) invoke(ctx context.Context, args invocationArgs) (object, er
} }
} }
return b.eval.evalBlock(ctx, newEc, b.block) res, err := b.eval.evalBlock(ctx, newEc, b.block)
if err != nil {
var er errReturn
if errors.As(err, &er) {
return er.ret, nil
}
return nil, err
}
return res, nil
} }

View file

@ -39,6 +39,9 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("eq", invokableFunc(eqBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin))
rootEC.addCmd("cat", invokableFunc(concatBuiltin)) rootEC.addCmd("cat", invokableFunc(concatBuiltin))
rootEC.addCmd("break", invokableFunc(breakBuiltin))
rootEC.addCmd("continue", invokableFunc(continueBuiltin))
rootEC.addCmd("return", invokableFunc(returnBuiltin))
rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin))
rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) rootEC.addMacro("foreach", macroFunc(foreachBuiltin))

View file

@ -445,3 +445,23 @@ func (s structProxyObject) Each(fn func(k string, v object) error) error {
} }
return nil return nil
} }
type errBreak struct {
isCont bool
ret object
}
func (e errBreak) Error() string {
if e.isCont {
return "continue"
}
return "break"
}
type errReturn struct {
ret object
}
func (e errReturn) Error() string {
return "return"
}

View file

@ -217,6 +217,91 @@ func TestBuiltins_ForEach(t *testing.T) {
} }
} }
func TestBuiltins_Break(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "break unconditionally returning nothing", expr: `
foreach ["1" "2" "3"] { |v|
break
echo $v
}`, want: "(nil)\n"},
{desc: "break conditionally returning nothing", expr: `
foreach ["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|
echo $u $v
if (eq $v "2") { break }
}
}`, want: "a1\na2\nb1\nb2\n(nil)\n"},
{desc: "break returning value", expr: `
echo (foreach ["1" "2" "3"] { |v|
echo $v
if (eq $v "2") { break "hello" }
})`, want: "1\n2\nhello\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)
assert.NoError(t, err)
assert.Equal(t, tt.want, outW.String())
})
}
}
func TestBuiltins_Continue(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "continue unconditionally", expr: `
foreach ["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|
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|
if (eq $v "2") { continue }
echo $u $v
}
}`, want: "a1\na3\nb1\nb3\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)
assert.NoError(t, err)
assert.Equal(t, tt.want, outW.String())
})
}
}
func TestBuiltins_Procs(t *testing.T) { func TestBuiltins_Procs(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
@ -297,6 +382,79 @@ func TestBuiltins_Procs(t *testing.T) {
} }
} }
func TestBuiltins_Return(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "nil return", expr: `
proc greet {
echo "Hello"
return
echo "World"
}
greet
`, want: "Hello\n(nil)\n"},
{desc: "simple return", expr: `
proc greet {
return "Hello, world"
echo "But not me"
}
greet
`, want: "Hello, world\n"},
{desc: "only return current frame", expr: `
proc greetWhat {
echo "Greet the"
return "moon"
echo "world"
}
proc greet {
set what (greetWhat)
echo "Hello, " $what
}
greet
`, want: "Greet the\nHello, moon\n(nil)\n"},
{desc: "return in loop", expr: `
proc countdown { |nums|
foreach $nums { |n|
echo $n
if (eq $n 3) {
return "abort"
}
}
}
countdown [5 4 3 2 1]
`, want: "5\n4\n3\nabort\n"},
{desc: "recursive procs", expr: `
proc four4 { |xs|
if (eq $xs "xxxx") {
return $xs
}
four4 (cat $xs "x")
}
four4
`, want: "xxxx\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)
assert.NoError(t, err)
assert.Equal(t, tt.want, outW.String())
})
}
}
func TestBuiltins_Map(t *testing.T) { func TestBuiltins_Map(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string