diff --git a/internal/codegen/c/analysis/visit_stmt.go b/internal/codegen/c/analysis/visit_stmt.go index cfdc391..8056f1c 100644 --- a/internal/codegen/c/analysis/visit_stmt.go +++ b/internal/codegen/c/analysis/visit_stmt.go @@ -24,6 +24,8 @@ func (a *Analyzer) visitStmt(current string, stmt ast.Stmt) { a.visitVar(current, n) case *ast.IfStmt: a.visitIf(current, n) + case *ast.WhileStmt: + a.visitWhile(current, n) case *ast.LoopStmt: a.visitLoop(current, n) case *ast.AssignmentStmt: diff --git a/internal/codegen/c/analysis/visit_while.go b/internal/codegen/c/analysis/visit_while.go new file mode 100644 index 0000000..0f98121 --- /dev/null +++ b/internal/codegen/c/analysis/visit_while.go @@ -0,0 +1,11 @@ +package analysis + +import "github.com/azin-lang/Azin/pkg/ast" + +func (a *Analyzer) visitWhile(fn string, stmt *ast.WhileStmt) { + a.visitExpr(fn, stmt.Condition) + + for _, child := range stmt.Body { + a.visitStmt(fn, child) + } +} diff --git a/internal/codegen/c/emit_statements.go b/internal/codegen/c/emit_statements.go index 87776a6..53f393c 100644 --- a/internal/codegen/c/emit_statements.go +++ b/internal/codegen/c/emit_statements.go @@ -61,6 +61,9 @@ func (t *Transpiler) emitStatement( case *ast.IfStmt: t.emitIf(n) + case *ast.WhileStmt: + t.emitWhile(n) + case *ast.LoopStmt: t.emitLoop(n) @@ -173,6 +176,27 @@ func (t *Transpiler) emitIf( t.newline() } +func (t *Transpiler) emitWhile( + stmt *ast.WhileStmt, +) { + t.indentLine() + + t.write("while (") + + t.emitExpression( + stmt.Condition, + ) + + t.write(") {\n") + + t.emitBlock( + stmt.Body, + ) + + t.indentLine() + t.write("}\n") +} + func (t *Transpiler) emitLoop( stmt *ast.LoopStmt, ) { diff --git a/internal/optimizer/statements.go b/internal/optimizer/statements.go index 3af7f26..8c0bc8f 100644 --- a/internal/optimizer/statements.go +++ b/internal/optimizer/statements.go @@ -44,6 +44,8 @@ func (o *Optimizer) optimizeStatement(stmt ast.Stmt) []ast.Stmt { return o.optimizeIf(n) case *ast.LoopStmt: return o.optimizeLoop(n) + case *ast.WhileStmt: + return o.optimizeWhile(n) case *ast.ExpressionStmt: return o.optimizeExpressionStmt(n) case *ast.FuncStmt: @@ -58,6 +60,32 @@ func (o *Optimizer) optimizeStatement(stmt ast.Stmt) []ast.Stmt { return []ast.Stmt{stmt} } +func (o *Optimizer) optimizeWhile(n *ast.WhileStmt) []ast.Stmt { + // Optimization for while loops can be implemented here, but for now, we will just optimize the body of the loop. + if len(n.Body) == 0 { + return nil + } + + o.currentScope.ClearAll() + + o.Enter() + n.Body = o.optimizeStatements(n.Body) + o.Leave() + + if !canUnwrapLoop(n.Body) { + return []ast.Stmt{n} + } + + last := n.Body[len(n.Body)-1] + switch last.(type) { + case *ast.ReturnStmt: + return n.Body + case *ast.StopStmt: + return n.Body[:len(n.Body)-1] + } + return []ast.Stmt{n} +} + func (o *Optimizer) optimizeLoop(n *ast.LoopStmt) []ast.Stmt { if len(n.Body) == 0 { return nil diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index 22048f2..1d668cf 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -225,6 +225,19 @@ func (*IfStmt) Label() string { return "if" } +type WhileStmt struct { + Token token2.Token // while + Condition Expr + Body []Stmt +} + +func (*WhileStmt) stmtNode() {} +func (w *WhileStmt) TokenLiteral() string { return w.Token.Kind.String() } +func (w *WhileStmt) Pos() token2.Position { return w.Token.Position } +func (*WhileStmt) Label() string { + return "while" +} + type LoopStmt struct { Token token2.Token // loop Body []Stmt diff --git a/pkg/ast/ast_test.go b/pkg/ast/ast_test.go index 58f263e..ee35249 100644 --- a/pkg/ast/ast_test.go +++ b/pkg/ast/ast_test.go @@ -97,6 +97,18 @@ func TestIfStmt(t *testing.T) { } } +func TestWhileStmt(t *testing.T) { + s := &ast.WhileStmt{ + Token: tok(token.KwWhile, 0, 5), + Condition: ident("true"), + Body: []ast.Stmt{}, + } + + if s.Label() != "while" { + t.Errorf("Label = %q", s.Label()) + } +} + func TestLoopStmt(t *testing.T) { s := &ast.LoopStmt{ Token: tok(token.KwLoop, 0, 4), diff --git a/pkg/lexer/fuzz_test.go b/pkg/lexer/fuzz_test.go index ec60b77..8f38345 100644 --- a/pkg/lexer/fuzz_test.go +++ b/pkg/lexer/fuzz_test.go @@ -21,7 +21,7 @@ func FuzzLexer(f *testing.F) { "+ - * / % = == ! !=", "< <= > >= += ++ -= -- -> && ||", "( ) { } [ ] , ; : .", - "fn do var mut return end char int bool unit string float if then else struct is importc loop null", + "fn do var mut return end char int bool unit string float if then else struct is importc loop while null", "@", "'\n'", "", diff --git a/pkg/lexer/lexer_test.go b/pkg/lexer/lexer_test.go index f57395b..582f5f7 100644 --- a/pkg/lexer/lexer_test.go +++ b/pkg/lexer/lexer_test.go @@ -33,7 +33,7 @@ func joinKinds(tokens []token2.Token) string { } func TestLexerKeywords(t *testing.T) { - input := "fn do var mut return end char int bool unit string float if then else struct is import importc loop null" + input := "fn do var mut return end char int bool unit string float if then else struct is import importc loop while null" tokens, diag := lex(input) if diag.HasErrors() { @@ -41,7 +41,7 @@ func TestLexerKeywords(t *testing.T) { } got := joinKinds(tokens) - want := "kw_fn kw_do kw_var kw_mut kw_return kw_end kw_char kw_int kw_bool kw_unit kw_string kw_float kw_if kw_then kw_else kw_struct kw_is kw_import kw_importc kw_loop kw_null eof" + want := "kw_fn kw_do kw_var kw_mut kw_return kw_end kw_char kw_int kw_bool kw_unit kw_string kw_float kw_if kw_then kw_else kw_struct kw_is kw_import kw_importc kw_loop kw_while kw_null eof" if got != want { t.Errorf("keywords\ngot: %s\nwant: %s", got, want) diff --git a/pkg/parser/fuzz_test.go b/pkg/parser/fuzz_test.go index e9be1b4..b5ea410 100644 --- a/pkg/parser/fuzz_test.go +++ b/pkg/parser/fuzz_test.go @@ -16,6 +16,7 @@ func FuzzParser(f *testing.F) { "if true then return 1; end", "if true then return 1; else return 2; end", "loop return 0; end", + "while true do return 0; end", "struct Point is x: int; y: int; end", "importc \"stdio.h\"", "x = 42;", diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 8f2dae8..c78a30d 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -147,6 +147,20 @@ func TestParserIfElse(t *testing.T) { } } +func TestParserWhile(t *testing.T) { + program, diag := parseProgram(t, ` + while true loop + return 1 + end + `) + if diag.HasErrors() { + t.Fatalf("unexpected errors: %v", diag.Err()) + } + if _, ok := program.Statements[0].(*ast2.WhileStmt); !ok { + t.Fatalf("expected WhileStmt, got %T", program.Statements[0]) + } +} + func TestParserLoop(t *testing.T) { program, diag := parseProgram(t, "loop\n return 1;\nend\n") if diag.HasErrors() { diff --git a/pkg/parser/statement.go b/pkg/parser/statement.go index bc24de6..ce31c48 100644 --- a/pkg/parser/statement.go +++ b/pkg/parser/statement.go @@ -124,6 +124,8 @@ func (p *Parser) parseStatement() ast.Stmt { stmt = p.parseImportC() case p.check(token.KwImport): stmt = p.parseImport() + case p.check(token.KwWhile): + stmt = p.parseWhile() case p.check(token.KwLoop): stmt = p.parseLoop() case p.check(token.KwStop): @@ -479,6 +481,22 @@ func (p *Parser) parseStop() ast.Stmt { } } +func (p *Parser) parseWhile() ast.Stmt { + tok := p.advance() + condition := p.parseExpression(PrecLowest) + + p.expect(token.KwLoop, "after while condition") + body := p.parseBlock(token.KwEnd) + + p.expect(token.KwEnd, "to close while") + + return &ast.WhileStmt{ + Token: tok, + Condition: condition, + Body: body, + } +} + func (p *Parser) parseLoop() ast.Stmt { tok := p.advance() diff --git a/pkg/sema/analyzer.go b/pkg/sema/analyzer.go index 05aad9e..149df40 100644 --- a/pkg/sema/analyzer.go +++ b/pkg/sema/analyzer.go @@ -653,6 +653,22 @@ func (a *Analyzer) visitStatement(stmt ast.Stmt) { a.popScope() + case *ast.WhileStmt: + a.loopDepth++ + defer func() { a.loopDepth-- }() + + a.pushScope() + defer a.popScope() + + cond := a.inferExprType(n.Condition) + if !types2.IsAssignable(cond, types2.BoolType()) { + a.errorf(n.Condition, "while condition must be bool, got %s", cond.Name) + } + + for _, stmt := range n.Body { + a.visitStatement(stmt) + } + case *ast.LoopStmt: a.loopDepth++ defer func() { a.loopDepth-- }() diff --git a/pkg/sema/analyzer_test.go b/pkg/sema/analyzer_test.go index 4bed054..6d45eb4 100644 --- a/pkg/sema/analyzer_test.go +++ b/pkg/sema/analyzer_test.go @@ -79,6 +79,17 @@ end` _ = mustHaveError(t, input) } +func TestSemanticWhileLoopConditionTypeMismatch(t *testing.T) { + input := `fn main: int do + var mut x: int = 0; + while x loop + x = x + 1; + end + return x; + end` + _ = mustHaveError(t, input) +} + func TestSemanticImmutableAssign(t *testing.T) { input := `fn main: int do var x: int = 42; @@ -206,6 +217,30 @@ end` validProgram(t, input) } +func TestSemanticWhileLoop(t *testing.T) { + input := ` + fn main: int do + var mut x: int = 0; + while x < 10 loop + x = x + 1; + end + return x; + end` + validProgram(t, input) +} + +func TestSemanticWhileLoopBreak(t *testing.T) { + input := `fn main: int do + var mut x: int = 0 + while x < 10 loop + stop + end + + return x + end` + validProgram(t, input) +} + func TestSemanticLoopBreak(t *testing.T) { input := `fn main: int do loop @@ -337,6 +372,18 @@ end` mustNotHaveWarning(t, input) } +func TestSemanticUnusedVarInWhileLoop(t *testing.T) { + input := `fn main: int do + var mut x: int = 0; + while x < 10 loop + var y: int = 42; + x = x + 1; + end + return 0; +end` + mustHaveWarning(t, input, "unused variable: y") +} + func TestSemanticUnusedVarInLoop(t *testing.T) { input := `fn main: int do loop @@ -346,6 +393,18 @@ end` mustHaveWarning(t, input, "unused variable: x") } +func TestSemanticUsedVarInWhileLoop(t *testing.T) { + input := `fn main: int do + var mut x: int = 0; + while x < 10 loop + var mut y: int = 42; + x = x + y + 1; + end + return x; + end` + mustNotHaveWarning(t, input) +} + func TestSemanticUsedVarInLoop(t *testing.T) { input := `fn main: int do loop diff --git a/pkg/token/keywords.go b/pkg/token/keywords.go index 3ebde53..48c1b19 100644 --- a/pkg/token/keywords.go +++ b/pkg/token/keywords.go @@ -21,6 +21,7 @@ var Keywords = map[string]Kind{ "is": KwIs, "import": KwImport, "importc": KwImportC, + "while": KwWhile, "loop": KwLoop, "stop": KwStop, "defer": KwDefer, diff --git a/pkg/token/keywords_test.go b/pkg/token/keywords_test.go index 72eda00..3822b02 100644 --- a/pkg/token/keywords_test.go +++ b/pkg/token/keywords_test.go @@ -28,6 +28,7 @@ func TestKeywordsContainAllRegistered(t *testing.T) { "import": tok.KwImport, "importc": tok.KwImportC, "loop": tok.KwLoop, + "while": tok.KwWhile, "stop": tok.KwStop, "null": tok.KwNull, "enum": tok.KwEnum, @@ -52,7 +53,7 @@ func TestKeywordsNoExtraEntries(t *testing.T) { "return": true, "end": true, "char": true, "int": true, "bool": true, "unit": true, "string": true, "float": true, "if": true, "then": true, "else": true, "struct": true, - "is": true, "import": true, "importc": true, "loop": true, "stop": true, + "is": true, "import": true, "importc": true, "loop": true, "while": true, "stop": true, "null": true, "enum": true, "defer": true, } diff --git a/pkg/token/kind.go b/pkg/token/kind.go index da839da..f87f190 100644 --- a/pkg/token/kind.go +++ b/pkg/token/kind.go @@ -34,6 +34,7 @@ const ( KwIs // kw_is KwImportC // kw_importc KwImport // kw_import + KwWhile // kw_while KwLoop // kw_loop KwStop // kw_stop KwDefer // kw_defer @@ -132,6 +133,8 @@ func (k Kind) DisplayName() string { return "'importC'" case KwImport: return "'import'" + case KwWhile: + return "'while'" case KwEnum: return "'enum'" case KwDefer: diff --git a/pkg/token/kind_string.go b/pkg/token/kind_string.go index 607757d..96bbca7 100644 --- a/pkg/token/kind_string.go +++ b/pkg/token/kind_string.go @@ -34,60 +34,61 @@ func _() { _ = x[KwIs-23] _ = x[KwImportC-24] _ = x[KwImport-25] - _ = x[KwLoop-26] - _ = x[KwStop-27] - _ = x[KwDefer-28] - _ = x[KwEnum-29] - _ = x[Plus-30] - _ = x[Minus-31] - _ = x[Star-32] - _ = x[Slash-33] - _ = x[Equal-34] - _ = x[EqualEqual-35] - _ = x[Bang-36] - _ = x[BangEqual-37] - _ = x[Less-38] - _ = x[LessEqual-39] - _ = x[Greater-40] - _ = x[GreaterEqual-41] - _ = x[Arrow-42] - _ = x[Modulo-43] - _ = x[Pipe-44] - _ = x[LogicalOr-45] - _ = x[LogicalAnd-46] - _ = x[Ampersand-47] - _ = x[Caret-48] - _ = x[Tilde-49] - _ = x[PlusEqual-50] - _ = x[MinusEqual-51] - _ = x[StarEqual-52] - _ = x[SlashEqual-53] - _ = x[ModuloEqual-54] - _ = x[CaretEqual-55] - _ = x[PipeEqual-56] - _ = x[AmpersandEqual-57] - _ = x[PlusPlus-58] - _ = x[MinusMinus-59] - _ = x[LessLess-60] - _ = x[GreaterGreater-61] - _ = x[LeftParen-62] - _ = x[RightParen-63] - _ = x[LeftBrace-64] - _ = x[RightBrace-65] - _ = x[Comma-66] - _ = x[Semicolon-67] - _ = x[Colon-68] - _ = x[Dot-69] - _ = x[LeftBracket-70] - _ = x[RightBracket-71] - _ = x[Newline-72] - _ = x[EOF-73] - _ = x[Error-74] + _ = x[KwWhile-26] + _ = x[KwLoop-27] + _ = x[KwStop-28] + _ = x[KwDefer-29] + _ = x[KwEnum-30] + _ = x[Plus-31] + _ = x[Minus-32] + _ = x[Star-33] + _ = x[Slash-34] + _ = x[Equal-35] + _ = x[EqualEqual-36] + _ = x[Bang-37] + _ = x[BangEqual-38] + _ = x[Less-39] + _ = x[LessEqual-40] + _ = x[Greater-41] + _ = x[GreaterEqual-42] + _ = x[Arrow-43] + _ = x[Modulo-44] + _ = x[Pipe-45] + _ = x[LogicalOr-46] + _ = x[LogicalAnd-47] + _ = x[Ampersand-48] + _ = x[Caret-49] + _ = x[Tilde-50] + _ = x[PlusEqual-51] + _ = x[MinusEqual-52] + _ = x[StarEqual-53] + _ = x[SlashEqual-54] + _ = x[ModuloEqual-55] + _ = x[CaretEqual-56] + _ = x[PipeEqual-57] + _ = x[AmpersandEqual-58] + _ = x[PlusPlus-59] + _ = x[MinusMinus-60] + _ = x[LessLess-61] + _ = x[GreaterGreater-62] + _ = x[LeftParen-63] + _ = x[RightParen-64] + _ = x[LeftBrace-65] + _ = x[RightBrace-66] + _ = x[Comma-67] + _ = x[Semicolon-68] + _ = x[Colon-69] + _ = x[Dot-70] + _ = x[LeftBracket-71] + _ = x[RightBracket-72] + _ = x[Newline-73] + _ = x[EOF-74] + _ = x[Error-75] } -const _Kind_name = "unknownidentifierinteger_literalstring_literalfloat_literalcharacter_literalkw_fnkw_dokw_varkw_mutkw_returnkw_endkw_charkw_intkw_boolkw_nullkw_unitkw_stringkw_floatkw_ifkw_thenkw_elsekw_structkw_iskw_importckw_importkw_loopkw_stopkw_deferkw_enumplusminusstarslashequalequal_equalbangbang_equallessless_equalgreatergreater_equalarrowmodulopipelogical_orlogical_andampersandcarettildeplus_equalminus_equalstar_equalslash_equalmodulo_equalcaret_equalpipe_equalampersand_equalplus_plusminus_minusless_lessgreater_greaterleft_parenright_parenleft_braceright_bracecommasemicoloncolondotleft_bracketright_bracketnewlineeoferror" +const _Kind_name = "unknownidentifierinteger_literalstring_literalfloat_literalcharacter_literalkw_fnkw_dokw_varkw_mutkw_returnkw_endkw_charkw_intkw_boolkw_nullkw_unitkw_stringkw_floatkw_ifkw_thenkw_elsekw_structkw_iskw_importckw_importkw_whilekw_loopkw_stopkw_deferkw_enumplusminusstarslashequalequal_equalbangbang_equallessless_equalgreatergreater_equalarrowmodulopipelogical_orlogical_andampersandcarettildeplus_equalminus_equalstar_equalslash_equalmodulo_equalcaret_equalpipe_equalampersand_equalplus_plusminus_minusless_lessgreater_greaterleft_parenright_parenleft_braceright_bracecommasemicoloncolondotleft_bracketright_bracketnewlineeoferror" -var _Kind_index = [...]uint16{0, 7, 17, 32, 46, 59, 76, 81, 86, 92, 98, 107, 113, 120, 126, 133, 140, 147, 156, 164, 169, 176, 183, 192, 197, 207, 216, 223, 230, 238, 245, 249, 254, 258, 263, 268, 279, 283, 293, 297, 307, 314, 327, 332, 338, 342, 352, 363, 372, 377, 382, 392, 403, 413, 424, 436, 447, 457, 472, 481, 492, 501, 516, 526, 537, 547, 558, 563, 572, 577, 580, 592, 605, 612, 615, 620} +var _Kind_index = [...]uint16{0, 7, 17, 32, 46, 59, 76, 81, 86, 92, 98, 107, 113, 120, 126, 133, 140, 147, 156, 164, 169, 176, 183, 192, 197, 207, 216, 224, 231, 238, 246, 253, 257, 262, 266, 271, 276, 287, 291, 301, 305, 315, 322, 335, 340, 346, 350, 360, 371, 380, 385, 390, 400, 411, 421, 432, 444, 455, 465, 480, 489, 500, 509, 524, 534, 545, 555, 566, 571, 580, 585, 588, 600, 613, 620, 623, 628} func (i Kind) String() string { idx := int(i) - 0 diff --git a/pkg/token/kind_test.go b/pkg/token/kind_test.go index 863ac9a..de57bde 100644 --- a/pkg/token/kind_test.go +++ b/pkg/token/kind_test.go @@ -30,6 +30,7 @@ func TestKindDisplayName(t *testing.T) { {tok.KwIs, "'is'"}, {tok.KwImportC, "'importC'"}, {tok.KwImport, "'import'"}, + {tok.KwWhile, "'while'"}, {tok.KwChar, "'char'"}, {tok.KwInt, "'int'"}, {tok.KwBool, "'bool'"}, diff --git a/tests/codegen/c_codegen_test.go b/tests/codegen/c_codegen_test.go index cfd5ab9..f8c962c 100644 --- a/tests/codegen/c_codegen_test.go +++ b/tests/codegen/c_codegen_test.go @@ -275,6 +275,21 @@ end "for (;;)", }, }, + { + name: "while", + input: ` +fn main: int do + var mut x: int = 0 + while x < 10 loop + x = x + 1 + end + return x +end +`, + contains: []string{ + "while (x < 10)", + }, + }, } for _, tt := range tests {