add while loop

This commit is contained in:
Sky Johnson 2025-06-10 11:25:39 -05:00
parent 119cfcecce
commit d8318789e1
4 changed files with 476 additions and 1 deletions

View File

@ -102,6 +102,25 @@ func (is *IfStatement) String() string {
return result return result
} }
// WhileStatement represents while loops: while condition do ... end
type WhileStatement struct {
Condition Expression
Body []Statement
}
func (ws *WhileStatement) statementNode() {}
func (ws *WhileStatement) String() string {
var result string
result += fmt.Sprintf("while %s do\n", ws.Condition.String())
for _, stmt := range ws.Body {
result += "\t" + stmt.String() + "\n"
}
result += "end"
return result
}
// ForStatement represents numeric for loops: for i = start, end, step do ... end // ForStatement represents numeric for loops: for i = start, end, step do ... end
type ForStatement struct { type ForStatement struct {
Variable *Identifier Variable *Identifier

View File

@ -113,6 +113,8 @@ func (p *Parser) parseStatement() Statement {
return p.parseIfStatement() return p.parseIfStatement()
case FOR: case FOR:
return p.parseForStatement() return p.parseForStatement()
case WHILE:
return p.parseWhileStatement()
case ECHO: case ECHO:
return p.parseEchoStatement() return p.parseEchoStatement()
case ASSIGN: case ASSIGN:
@ -185,6 +187,36 @@ func (p *Parser) parseEchoStatement() *EchoStatement {
return stmt return stmt
} }
// parseWhileStatement parses while loops: while condition do ... end
func (p *Parser) parseWhileStatement() *WhileStatement {
stmt := &WhileStatement{}
p.nextToken() // move past 'while'
stmt.Condition = p.ParseExpression(LOWEST)
if stmt.Condition == nil {
p.addError("expected condition after 'while'")
return nil
}
if !p.expectPeek(DO) {
p.addError("expected 'do' after while condition")
return nil
}
p.nextToken() // move past 'do'
// Parse loop body
stmt.Body = p.parseBlockStatements(END)
if !p.curTokenIs(END) {
p.addError("expected 'end' to close while loop")
return nil
}
return stmt
}
// parseForStatement parses for loops (both numeric and for-in) // parseForStatement parses for loops (both numeric and for-in)
func (p *Parser) parseForStatement() Statement { func (p *Parser) parseForStatement() Statement {
p.nextToken() // move past 'for' p.nextToken() // move past 'for'
@ -707,7 +739,7 @@ func (p *Parser) expectPeekIdent() bool {
// isKeyword checks if a token type is a keyword that can be used as identifier // isKeyword checks if a token type is a keyword that can be used as identifier
func (p *Parser) isKeyword(t TokenType) bool { func (p *Parser) isKeyword(t TokenType) bool {
switch t { switch t {
case TRUE, FALSE, NIL, VAR, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, IN, DO: case TRUE, FALSE, NIL, VAR, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, WHILE, IN, DO:
return true return true
default: default:
return false return false
@ -857,6 +889,8 @@ func tokenTypeString(t TokenType) string {
return "echo" return "echo"
case FOR: case FOR:
return "for" return "for"
case WHILE:
return "while"
case IN: case IN:
return "in" return "in"
case DO: case DO:

View File

@ -423,3 +423,423 @@ end`
t.Fatalf("expected DotExpression for iterable, got %T", forInStmt.Iterable) t.Fatalf("expected DotExpression for iterable, got %T", forInStmt.Iterable)
} }
} }
func TestBasicWhileLoop(t *testing.T) {
tests := []struct {
input string
description string
}{
{"while true do\necho \"hello\"\nend", "basic while with boolean"},
{"while x < 10 do\nx = x + 1\nend", "while with comparison"},
{"while count do\ncount = count - 1\nend", "while with identifier"},
{"while table.flag do\necho \"running\"\nend", "while with member access"},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
if stmt.Condition == nil {
t.Error("expected non-nil condition")
}
if len(stmt.Body) == 0 {
t.Error("expected non-empty body")
}
})
}
}
func TestWhileLoopWithComplexConditions(t *testing.T) {
tests := []struct {
input string
description string
}{
{"while x + y < 10 do\necho x\nend", "arithmetic condition"},
{"while arr[i] != nil do\ni = i + 1\nend", "array access condition"},
{"while obj.count > 0 do\nobj.count = obj.count - 1\nend", "member access condition"},
{"while (a + b) * c == d do\necho \"match\"\nend", "grouped expression condition"},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
// Condition should be parsed as an expression
if stmt.Condition == nil {
t.Error("expected non-nil condition")
}
if len(stmt.Body) == 0 {
t.Error("expected non-empty body")
}
})
}
}
func TestWhileLoopWithComplexBody(t *testing.T) {
input := `while running do
echo "processing"
if data[i] then
result = result + data[i]
i = i + 1
else
running = false
end
for j = 1, 3 do
echo j
end
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
if len(stmt.Body) != 3 {
t.Fatalf("expected 3 body statements, got %d", len(stmt.Body))
}
// First: echo statement
_, ok = stmt.Body[0].(*parser.EchoStatement)
if !ok {
t.Fatalf("body[0]: expected EchoStatement, got %T", stmt.Body[0])
}
// Second: if statement
_, ok = stmt.Body[1].(*parser.IfStatement)
if !ok {
t.Fatalf("body[1]: expected IfStatement, got %T", stmt.Body[1])
}
// Third: for statement
_, ok = stmt.Body[2].(*parser.ForStatement)
if !ok {
t.Fatalf("body[2]: expected ForStatement, got %T", stmt.Body[2])
}
}
func TestNestedWhileLoops(t *testing.T) {
input := `while outer do
while inner do
echo "nested"
inner = inner - 1
end
outer = outer - 1
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
outerWhile, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
if len(outerWhile.Body) != 2 {
t.Fatalf("expected 2 body statements, got %d", len(outerWhile.Body))
}
// First body statement should be nested while
innerWhile, ok := outerWhile.Body[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected nested WhileStatement, got %T", outerWhile.Body[0])
}
if len(innerWhile.Body) != 2 {
t.Fatalf("expected 2 inner body statements, got %d", len(innerWhile.Body))
}
// Second body statement should be assignment
_, ok = outerWhile.Body[1].(*parser.AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", outerWhile.Body[1])
}
}
func TestWhileLoopWithMemberAccess(t *testing.T) {
input := `while obj.running do
data[index] = data[index] + 1
index = index + 1
if index >= obj.size then
obj.running = false
end
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
// Check condition is dot expression
dotExpr, ok := stmt.Condition.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression condition, got %T", stmt.Condition)
}
if dotExpr.Key != "running" {
t.Errorf("expected key 'running', got %s", dotExpr.Key)
}
if len(stmt.Body) != 3 {
t.Fatalf("expected 3 body statements, got %d", len(stmt.Body))
}
// First assignment: data[index] = ...
assign1, ok := stmt.Body[0].(*parser.AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", stmt.Body[0])
}
_, ok = assign1.Name.(*parser.IndexExpression)
if !ok {
t.Fatalf("expected IndexExpression for assignment target, got %T", assign1.Name)
}
}
func TestWhileLoopErrors(t *testing.T) {
tests := []struct {
input string
expectedError string
desc string
}{
{"while do end", "expected condition after 'while'", "missing condition"},
{"while true end", "expected 'do' after while condition", "missing do"},
{"while true do", "expected 'end' to close while loop", "missing end"},
{"while + do end", "unexpected operator '+'", "invalid condition"},
{"while true do x =", "expected expression after assignment operator", "incomplete body"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
p.ParseProgram()
if !p.HasErrors() {
t.Fatal("expected parsing errors")
}
errors := p.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, tt.expectedError) {
found = true
break
}
}
if !found {
errorMsgs := make([]string, len(errors))
for i, err := range errors {
errorMsgs[i] = err.Message
}
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
}
})
}
}
func TestWhileLoopStringRepresentation(t *testing.T) {
tests := []struct {
input string
contains []string
desc string
}{
{
"while true do\necho \"hello\"\nend",
[]string{"while true do", "echo \"hello\"", "end"},
"basic while loop",
},
{
"while x < 10 do\nx = x + 1\nend",
[]string{"while (x < 10.00) do", "x = (x + 1.00)", "end"},
"while with condition and assignment",
},
{
"while obj.flag do\necho obj.value\nend",
[]string{"while obj.flag do", "echo obj.value", "end"},
"while with member access",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
programStr := program.String()
for _, contain := range tt.contains {
if !strings.Contains(programStr, contain) {
t.Errorf("expected string representation to contain %q, got:\n%s", contain, programStr)
}
}
})
}
}
func TestWhileLoopInMixedProgram(t *testing.T) {
input := `counter = 0
total = 0
arr = {1, 2, 3, 4, 5}
while counter < 5 do
total = total + arr[counter]
counter = counter + 1
end
echo total
if total > 10 then
echo "sum is large"
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 6 {
t.Fatalf("expected 6 statements, got %d", len(program.Statements))
}
// First three: assignments
for i := 0; i < 3; i++ {
_, ok := program.Statements[i].(*parser.AssignStatement)
if !ok {
t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i])
}
}
// Fourth: while loop
whileStmt, ok := program.Statements[3].(*parser.WhileStatement)
if !ok {
t.Fatalf("statement 3: expected WhileStatement, got %T", program.Statements[3])
}
if len(whileStmt.Body) != 2 {
t.Errorf("expected 2 body statements in while loop, got %d", len(whileStmt.Body))
}
// Fifth: echo statement
_, ok = program.Statements[4].(*parser.EchoStatement)
if !ok {
t.Fatalf("statement 4: expected EchoStatement, got %T", program.Statements[4])
}
// Sixth: if statement
_, ok = program.Statements[5].(*parser.IfStatement)
if !ok {
t.Fatalf("statement 5: expected IfStatement, got %T", program.Statements[5])
}
}
func TestWhileLoopWithAllLoopTypes(t *testing.T) {
input := `while active do
for i = 1, 10 do
echo i
end
for k, v in data do
echo v
end
while inner do
inner = inner - 1
end
active = active - 1
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
whileStmt, ok := program.Statements[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", program.Statements[0])
}
if len(whileStmt.Body) != 4 {
t.Fatalf("expected 4 body statements, got %d", len(whileStmt.Body))
}
// First: numeric for loop
_, ok = whileStmt.Body[0].(*parser.ForStatement)
if !ok {
t.Fatalf("body[0]: expected ForStatement, got %T", whileStmt.Body[0])
}
// Second: for-in loop
_, ok = whileStmt.Body[1].(*parser.ForInStatement)
if !ok {
t.Fatalf("body[1]: expected ForInStatement, got %T", whileStmt.Body[1])
}
// Third: nested while loop
_, ok = whileStmt.Body[2].(*parser.WhileStatement)
if !ok {
t.Fatalf("body[2]: expected WhileStatement, got %T", whileStmt.Body[2])
}
// Fourth: assignment
_, ok = whileStmt.Body[3].(*parser.AssignStatement)
if !ok {
t.Fatalf("body[3]: expected AssignStatement, got %T", whileStmt.Body[3])
}
}

View File

@ -46,6 +46,7 @@ const (
END END
ECHO ECHO
FOR FOR
WHILE
IN IN
DO DO
@ -107,6 +108,7 @@ func lookupIdent(ident string) TokenType {
"end": END, "end": END,
"echo": ECHO, "echo": ECHO,
"for": FOR, "for": FOR,
"while": WHILE,
"in": IN, "in": IN,
"do": DO, "do": DO,
} }