Skip to content

Commit bb71fa6

Browse files
committed
Add break and next loop control statements
1 parent 8e50b67 commit bb71fa6

7 files changed

Lines changed: 163 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ Goal: improve language ergonomics for complex script logic and recovery behavior
211211

212212
- [x] Add `while` loops.
213213
- [x] Add `until` loops.
214-
- [ ] Add loop control keywords: `break` and `next`.
214+
- [x] Add loop control keywords: `break` and `next`.
215215
- [ ] Add `case/when` expression support (if approved).
216216
- [ ] Define behavior for nested loop control and block boundaries.
217217

vibes/ast.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ type UntilStmt struct {
140140
func (s *UntilStmt) stmtNode() {}
141141
func (s *UntilStmt) Pos() Position { return s.position }
142142

143+
type BreakStmt struct {
144+
position Position
145+
}
146+
147+
func (s *BreakStmt) stmtNode() {}
148+
func (s *BreakStmt) Pos() Position { return s.position }
149+
150+
type NextStmt struct {
151+
position Position
152+
}
153+
154+
func (s *NextStmt) stmtNode() {}
155+
func (s *NextStmt) Pos() Position { return s.position }
156+
143157
type Identifier struct {
144158
Name string
145159
position Position

vibes/execution.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type Execution struct {
6262
capabilityContractsByName map[string]CapabilityMethodContract
6363
receiverStack []Value
6464
envStack []*Env
65+
loopDepth int
6566
strictEffects bool
6667
allowRequire bool
6768
}
@@ -93,6 +94,11 @@ type RuntimeError struct {
9394
Frames []StackFrame
9495
}
9596

97+
var (
98+
errLoopBreak = errors.New("loop break")
99+
errLoopNext = errors.New("loop next")
100+
)
101+
96102
func (re *RuntimeError) Error() string {
97103
var b strings.Builder
98104
b.WriteString(re.Message)
@@ -340,6 +346,16 @@ func (exec *Execution) evalStatement(stmt Statement, env *Env) (Value, bool, err
340346
return exec.evalWhileStatement(s, env)
341347
case *UntilStmt:
342348
return exec.evalUntilStatement(s, env)
349+
case *BreakStmt:
350+
if exec.loopDepth == 0 {
351+
return NewNil(), false, exec.errorAt(s.Pos(), "break used outside of loop")
352+
}
353+
return NewNil(), false, errLoopBreak
354+
case *NextStmt:
355+
if exec.loopDepth == 0 {
356+
return NewNil(), false, exec.errorAt(s.Pos(), "next used outside of loop")
357+
}
358+
return NewNil(), false, errLoopNext
343359
default:
344360
return NewNil(), false, exec.errorAt(stmt.Pos(), "unsupported statement")
345361
}
@@ -1071,6 +1087,11 @@ func (exec *Execution) evalRangeExpr(expr *RangeExpr, env *Env) (Value, error) {
10711087
}
10721088

10731089
func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, error) {
1090+
exec.loopDepth++
1091+
defer func() {
1092+
exec.loopDepth--
1093+
}()
1094+
10741095
iterable, err := exec.evalExpression(stmt.Iterable, env)
10751096
if err != nil {
10761097
return NewNil(), false, err
@@ -1087,6 +1108,12 @@ func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, e
10871108
env.Assign(stmt.Iterator, item)
10881109
val, returned, err := exec.evalStatements(stmt.Body, env)
10891110
if err != nil {
1111+
if errors.Is(err, errLoopBreak) {
1112+
return last, false, nil
1113+
}
1114+
if errors.Is(err, errLoopNext) {
1115+
continue
1116+
}
10901117
return NewNil(), false, err
10911118
}
10921119
if returned {
@@ -1101,6 +1128,12 @@ func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, e
11011128
env.Assign(stmt.Iterator, NewInt(i))
11021129
val, returned, err := exec.evalStatements(stmt.Body, env)
11031130
if err != nil {
1131+
if errors.Is(err, errLoopBreak) {
1132+
return last, false, nil
1133+
}
1134+
if errors.Is(err, errLoopNext) {
1135+
continue
1136+
}
11041137
return NewNil(), false, err
11051138
}
11061139
if returned {
@@ -1113,6 +1146,12 @@ func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, e
11131146
env.Assign(stmt.Iterator, NewInt(i))
11141147
val, returned, err := exec.evalStatements(stmt.Body, env)
11151148
if err != nil {
1149+
if errors.Is(err, errLoopBreak) {
1150+
return last, false, nil
1151+
}
1152+
if errors.Is(err, errLoopNext) {
1153+
continue
1154+
}
11161155
return NewNil(), false, err
11171156
}
11181157
if returned {
@@ -1129,6 +1168,11 @@ func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, e
11291168
}
11301169

11311170
func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, bool, error) {
1171+
exec.loopDepth++
1172+
defer func() {
1173+
exec.loopDepth--
1174+
}()
1175+
11321176
last := NewNil()
11331177
for {
11341178
if err := exec.step(); err != nil {
@@ -1146,6 +1190,12 @@ func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, boo
11461190
}
11471191
val, returned, err := exec.evalStatements(stmt.Body, env)
11481192
if err != nil {
1193+
if errors.Is(err, errLoopBreak) {
1194+
return last, false, nil
1195+
}
1196+
if errors.Is(err, errLoopNext) {
1197+
continue
1198+
}
11491199
return NewNil(), false, err
11501200
}
11511201
if returned {
@@ -1156,6 +1206,11 @@ func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, boo
11561206
}
11571207

11581208
func (exec *Execution) evalUntilStatement(stmt *UntilStmt, env *Env) (Value, bool, error) {
1209+
exec.loopDepth++
1210+
defer func() {
1211+
exec.loopDepth--
1212+
}()
1213+
11591214
last := NewNil()
11601215
for {
11611216
if err := exec.step(); err != nil {
@@ -1173,6 +1228,12 @@ func (exec *Execution) evalUntilStatement(stmt *UntilStmt, env *Env) (Value, boo
11731228
}
11741229
val, returned, err := exec.evalStatements(stmt.Body, env)
11751230
if err != nil {
1231+
if errors.Is(err, errLoopBreak) {
1232+
return last, false, nil
1233+
}
1234+
if errors.Is(err, errLoopNext) {
1235+
continue
1236+
}
11761237
return NewNil(), false, err
11771238
}
11781239
if returned {

vibes/lexer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ func lookupIdent(ident string) TokenType {
419419
return tokenWhile
420420
case "until":
421421
return tokenUntil
422+
case "break":
423+
return tokenBreak
424+
case "next":
425+
return tokenNext
422426
case "in":
423427
return tokenIn
424428
case "if":

vibes/parser.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ func (p *parser) parseStatement() Statement {
130130
return p.parseWhileStatement()
131131
case tokenUntil:
132132
return p.parseUntilStatement()
133+
case tokenBreak:
134+
return p.parseBreakStatement()
135+
case tokenNext:
136+
return p.parseNextStatement()
133137
case tokenIdent:
134138
if p.curToken.Literal == "assert" {
135139
return p.parseAssertStatement()
@@ -420,6 +424,14 @@ func (p *parser) parseUntilStatement() Statement {
420424
return &UntilStmt{Condition: condition, Body: body, position: pos}
421425
}
422426

427+
func (p *parser) parseBreakStatement() Statement {
428+
return &BreakStmt{position: p.curToken.Pos}
429+
}
430+
431+
func (p *parser) parseNextStatement() Statement {
432+
return &NextStmt{position: p.curToken.Pos}
433+
}
434+
423435
func (p *parser) parseBlock(stop ...TokenType) []Statement {
424436
stmts := []Statement{}
425437
stopSet := make(map[TokenType]struct{}, len(stop))

vibes/runtime_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,75 @@ func TestUntilLoops(t *testing.T) {
862862
}
863863
}
864864

865+
func TestLoopControlBreakAndNext(t *testing.T) {
866+
script := compileScript(t, `
867+
def for_break()
868+
out = []
869+
for n in [1, 2, 3, 4]
870+
if n == 3
871+
break
872+
end
873+
out = out + [n]
874+
end
875+
out
876+
end
877+
878+
def for_next()
879+
out = []
880+
for n in [1, 2, 3, 4]
881+
if n % 2 == 0
882+
next
883+
end
884+
out = out + [n]
885+
end
886+
out
887+
end
888+
889+
def while_break_next()
890+
n = 0
891+
out = []
892+
while n < 5
893+
n = n + 1
894+
if n == 3
895+
next
896+
end
897+
if n == 5
898+
break
899+
end
900+
out = out + [n]
901+
end
902+
out
903+
end
904+
905+
def break_outside()
906+
break
907+
end
908+
909+
def next_outside()
910+
next
911+
end
912+
`)
913+
914+
forBreak := callFunc(t, script, "for_break", nil)
915+
compareArrays(t, forBreak, []Value{NewInt(1), NewInt(2)})
916+
917+
forNext := callFunc(t, script, "for_next", nil)
918+
compareArrays(t, forNext, []Value{NewInt(1), NewInt(3)})
919+
920+
whileBreakNext := callFunc(t, script, "while_break_next", nil)
921+
compareArrays(t, whileBreakNext, []Value{NewInt(1), NewInt(2), NewInt(4)})
922+
923+
_, err := script.Call(context.Background(), "break_outside", nil, CallOptions{})
924+
if err == nil || !strings.Contains(err.Error(), "break used outside of loop") {
925+
t.Fatalf("expected outside-loop break error, got %v", err)
926+
}
927+
928+
_, err = script.Call(context.Background(), "next_outside", nil, CallOptions{})
929+
if err == nil || !strings.Contains(err.Error(), "next used outside of loop") {
930+
t.Fatalf("expected outside-loop next error, got %v", err)
931+
}
932+
}
933+
865934
func TestDurationMethods(t *testing.T) {
866935
script := compileScript(t, `
867936
def duration_helpers()

vibes/token.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const (
5858
tokenFor TokenType = "FOR"
5959
tokenWhile TokenType = "WHILE"
6060
tokenUntil TokenType = "UNTIL"
61+
tokenBreak TokenType = "BREAK"
62+
tokenNext TokenType = "NEXT"
6163
tokenIn TokenType = "IN"
6264
tokenIf TokenType = "IF"
6365
tokenElsif TokenType = "ELSIF"

0 commit comments

Comments
 (0)