diff --git a/parser/ast.go b/parser/ast.go index b19911b..602fd19 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -102,6 +102,25 @@ func (is *IfStatement) String() string { 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 type ForStatement struct { Variable *Identifier diff --git a/parser/parser.go b/parser/parser.go index b25ded0..74f3900 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -113,6 +113,8 @@ func (p *Parser) parseStatement() Statement { return p.parseIfStatement() case FOR: return p.parseForStatement() + case WHILE: + return p.parseWhileStatement() case ECHO: return p.parseEchoStatement() case ASSIGN: @@ -185,6 +187,36 @@ func (p *Parser) parseEchoStatement() *EchoStatement { 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) func (p *Parser) parseForStatement() Statement { 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 func (p *Parser) isKeyword(t TokenType) bool { 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 default: return false @@ -857,6 +889,8 @@ func tokenTypeString(t TokenType) string { return "echo" case FOR: return "for" + case WHILE: + return "while" case IN: return "in" case DO: diff --git a/parser/tests/loops_test.go b/parser/tests/loops_test.go index 1fb2d7b..9d3f847 100644 --- a/parser/tests/loops_test.go +++ b/parser/tests/loops_test.go @@ -423,3 +423,423 @@ end` 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]) + } +} diff --git a/parser/token.go b/parser/token.go index c79821c..f5b0faa 100644 --- a/parser/token.go +++ b/parser/token.go @@ -46,6 +46,7 @@ const ( END ECHO FOR + WHILE IN DO @@ -107,6 +108,7 @@ func lookupIdent(ident string) TokenType { "end": END, "echo": ECHO, "for": FOR, + "while": WHILE, "in": IN, "do": DO, }