Skip to content

Commit c6640c4

Browse files
committed
Add while loop support to parser and runtime
1 parent 7dd1595 commit c6640c4

7 files changed

Lines changed: 113 additions & 1 deletion

File tree

ROADMAP.md

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

210210
### Control Flow
211211

212-
- [ ] Add `while` loops.
212+
- [x] Add `while` loops.
213213
- [ ] Add `until` loops.
214214
- [ ] Add loop control keywords: `break` and `next`.
215215
- [ ] Add `case/when` expression support (if approved).

vibes/ast.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ type ForStmt struct {
122122
func (s *ForStmt) stmtNode() {}
123123
func (s *ForStmt) Pos() Position { return s.position }
124124

125+
type WhileStmt struct {
126+
Condition Expression
127+
Body []Statement
128+
position Position
129+
}
130+
131+
func (s *WhileStmt) stmtNode() {}
132+
func (s *WhileStmt) Pos() Position { return s.position }
133+
125134
type Identifier struct {
126135
Name string
127136
position Position

vibes/execution.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ func (exec *Execution) evalStatement(stmt Statement, env *Env) (Value, bool, err
336336
return NewNil(), false, nil
337337
case *ForStmt:
338338
return exec.evalForStatement(s, env)
339+
case *WhileStmt:
340+
return exec.evalWhileStatement(s, env)
339341
default:
340342
return NewNil(), false, exec.errorAt(stmt.Pos(), "unsupported statement")
341343
}
@@ -1124,6 +1126,33 @@ func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, e
11241126
return last, false, nil
11251127
}
11261128

1129+
func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, bool, error) {
1130+
last := NewNil()
1131+
for {
1132+
if err := exec.step(); err != nil {
1133+
return NewNil(), false, exec.wrapError(err, stmt.Pos())
1134+
}
1135+
condition, err := exec.evalExpression(stmt.Condition, env)
1136+
if err != nil {
1137+
return NewNil(), false, err
1138+
}
1139+
if err := exec.checkMemoryWith(condition); err != nil {
1140+
return NewNil(), false, err
1141+
}
1142+
if !condition.Truthy() {
1143+
return last, false, nil
1144+
}
1145+
val, returned, err := exec.evalStatements(stmt.Body, env)
1146+
if err != nil {
1147+
return NewNil(), false, err
1148+
}
1149+
if returned {
1150+
return val, true, nil
1151+
}
1152+
last = val
1153+
}
1154+
}
1155+
11271156
func (exec *Execution) getMember(obj Value, property string, pos Position) (Value, error) {
11281157
switch obj.Kind() {
11291158
case KindHash, KindObject:

vibes/lexer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ func lookupIdent(ident string) TokenType {
415415
return tokenDo
416416
case "for":
417417
return tokenFor
418+
case "while":
419+
return tokenWhile
418420
case "in":
419421
return tokenIn
420422
case "if":

vibes/parser.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ func (p *parser) parseStatement() Statement {
126126
return p.parseIfStatement()
127127
case tokenFor:
128128
return p.parseForStatement()
129+
case tokenWhile:
130+
return p.parseWhileStatement()
129131
case tokenIdent:
130132
if p.curToken.Literal == "assert" {
131133
return p.parseAssertStatement()
@@ -386,6 +388,21 @@ func (p *parser) parseForStatement() Statement {
386388
return &ForStmt{Iterator: iterator, Iterable: iterable, Body: body, position: pos}
387389
}
388390

391+
func (p *parser) parseWhileStatement() Statement {
392+
pos := p.curToken.Pos
393+
p.nextToken()
394+
condition := p.parseExpression(lowestPrec)
395+
396+
p.nextToken()
397+
body := p.parseBlock(tokenEnd)
398+
399+
if p.curToken.Type != tokenEnd {
400+
p.errorExpected(p.curToken, "end")
401+
}
402+
403+
return &WhileStmt{Condition: condition, Body: body, position: pos}
404+
}
405+
389406
func (p *parser) parseBlock(stop ...TokenType) []Statement {
390407
stmts := []Statement{}
391408
stopSet := make(map[TokenType]struct{}, len(stop))

vibes/runtime_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,60 @@ func TestIntTimes(t *testing.T) {
753753
}
754754
}
755755

756+
func TestWhileLoops(t *testing.T) {
757+
script := compileScript(t, `
758+
def countdown(n)
759+
out = []
760+
while n > 0
761+
out = out + [n]
762+
n = n - 1
763+
end
764+
out
765+
end
766+
767+
def first_positive(n)
768+
while n > 0
769+
return n
770+
end
771+
0
772+
end
773+
774+
def skip_false()
775+
while false
776+
1
777+
end
778+
end
779+
`)
780+
781+
countdown := callFunc(t, script, "countdown", []Value{NewInt(3)})
782+
compareArrays(t, countdown, []Value{NewInt(3), NewInt(2), NewInt(1)})
783+
784+
if got := callFunc(t, script, "first_positive", []Value{NewInt(4)}); !got.Equal(NewInt(4)) {
785+
t.Fatalf("first_positive mismatch for positive input: %v", got)
786+
}
787+
if got := callFunc(t, script, "first_positive", []Value{NewInt(0)}); !got.Equal(NewInt(0)) {
788+
t.Fatalf("first_positive mismatch for zero input: %v", got)
789+
}
790+
if got := callFunc(t, script, "skip_false", nil); !got.Equal(NewNil()) {
791+
t.Fatalf("skip_false expected nil, got %v", got)
792+
}
793+
794+
engine := MustNewEngine(Config{StepQuota: 40})
795+
spinScript, err := engine.Compile(`
796+
def spin()
797+
while true
798+
end
799+
end
800+
`)
801+
if err != nil {
802+
t.Fatalf("compile error: %v", err)
803+
}
804+
_, err = spinScript.Call(context.Background(), "spin", nil, CallOptions{})
805+
if err == nil || !strings.Contains(err.Error(), "step quota exceeded") {
806+
t.Fatalf("expected step quota error for infinite while loop, got %v", err)
807+
}
808+
}
809+
756810
func TestDurationMethods(t *testing.T) {
757811
script := compileScript(t, `
758812
def duration_helpers()

vibes/token.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656
tokenYield TokenType = "YIELD"
5757
tokenDo TokenType = "DO"
5858
tokenFor TokenType = "FOR"
59+
tokenWhile TokenType = "WHILE"
5960
tokenIn TokenType = "IN"
6061
tokenIf TokenType = "IF"
6162
tokenElsif TokenType = "ELSIF"

0 commit comments

Comments
 (0)