From 4c470fca718412986f3eb475547d8e84f2afc2aa Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:27:07 +0200 Subject: [PATCH 01/15] feat: added while keyword and kind, regenerated the `kind_string.go` via `stringer -type=Kind -linecomment` and updated the according tests --- pkg/token/keywords.go | 1 + pkg/token/keywords_test.go | 3 +- pkg/token/kind.go | 1 + pkg/token/kind_string.go | 103 +++++++++++++++++++------------------ pkg/token/kind_test.go | 1 + 5 files changed, 57 insertions(+), 52 deletions(-) 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..6f0f006 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 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'"}, From 3e0c63a8a504eaa10c56425254669c17c41c4275 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:27:35 +0200 Subject: [PATCH 02/15] feat: added the WhileStmt AST node --- pkg/ast/ast.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 From 5397066ac9ae8d51c26d0ebec4ca869cee25b413 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:57:40 +0200 Subject: [PATCH 03/15] feat: added `while..do..end` parsing logic --- pkg/parser/statement.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/parser/statement.go b/pkg/parser/statement.go index bc24de6..b5ed4d5 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.KwDo, "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() From 05b6af9e0e14c56058180e3d506a8a6e83da8ae8 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:58:03 +0200 Subject: [PATCH 04/15] feat: added semantic analysis for while loop --- pkg/sema/analyzer.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/sema/analyzer.go b/pkg/sema/analyzer.go index 05aad9e..ce90399 100644 --- a/pkg/sema/analyzer.go +++ b/pkg/sema/analyzer.go @@ -653,6 +653,23 @@ 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) + cond = types2.ErrorType() + } + + for _, stmt := range n.Body { + a.visitStatement(stmt) + } + case *ast.LoopStmt: a.loopDepth++ defer func() { a.loopDepth-- }() From b44d2f93b87f2f560ade57900234ad94cdd00ba1 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:58:25 +0200 Subject: [PATCH 05/15] feat: added while loop analysis --- internal/codegen/c/analysis/visit_stmt.go | 2 ++ internal/codegen/c/analysis/visit_while.go | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 internal/codegen/c/analysis/visit_while.go 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) + } +} From 66b41c298a05a92c249bdb5124d5bf09eea0ad9c Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:59:09 +0200 Subject: [PATCH 06/15] feat: added emission of while loop statement in the transpiler --- internal/codegen/c/emit_statements.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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, ) { From 2e9ea3e285e76f0985d3cbb53c6f8bc2b9af717d Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 13:59:35 +0200 Subject: [PATCH 07/15] feat: added body optimization for the while loop statement. --- internal/optimizer/statements.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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 From 3175969453394aa4f8513b6654bef3b262bd2422 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:11:38 +0200 Subject: [PATCH 08/15] fix: display name for while loop added in `TokenKind.DisplayName` accessor --- pkg/token/kind.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/token/kind.go b/pkg/token/kind.go index 6f0f006..f87f190 100644 --- a/pkg/token/kind.go +++ b/pkg/token/kind.go @@ -133,6 +133,8 @@ func (k Kind) DisplayName() string { return "'importC'" case KwImport: return "'import'" + case KwWhile: + return "'while'" case KwEnum: return "'enum'" case KwDefer: From d8bd917ff6449eacef1b583af3b809d2124ff180 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:12:14 +0200 Subject: [PATCH 09/15] tests: added while loop ast test --- pkg/ast/ast_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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), From aa1aef85e3226b6fb6de3ba2a621a08a135b1046 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:12:56 +0200 Subject: [PATCH 10/15] feat: added "while" keyword in lexer tests --- pkg/lexer/fuzz_test.go | 2 +- pkg/lexer/lexer_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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) From eb2314e18b9319de6a618f0f11768959db3210c1 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:13:36 +0200 Subject: [PATCH 11/15] tests: added parsing tests for `while..do..end` statement --- pkg/parser/fuzz_test.go | 1 + pkg/parser/parser_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+) 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..cb87b34 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 do + 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() { From 1dc537344348bcb86d1938d3cb2964ca10b744db Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:16:24 +0200 Subject: [PATCH 12/15] tests: added semantic analysis tests for while loop statement --- pkg/sema/analyzer_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/pkg/sema/analyzer_test.go b/pkg/sema/analyzer_test.go index 4bed054..94ecaa0 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 do + 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 do + 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 do + 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 do + 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 do + 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 From d67e209730e7b9b27aa99950db65f3c6b97571c2 Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 14:16:41 +0200 Subject: [PATCH 13/15] feat: added codegen tests for while loop statement --- tests/codegen/c_codegen_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/codegen/c_codegen_test.go b/tests/codegen/c_codegen_test.go index cfd5ab9..40e35c4 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 do + x = x + 1 + end + return x +end +`, + contains: []string{ + "while (x < 10)", + }, + }, } for _, tt := range tests { From 4db8ae1e7646c2754db0e3c726ad283a0ecbd2fb Mon Sep 17 00:00:00 2001 From: GraySS Date: Fri, 31 Jul 2026 15:05:31 +0200 Subject: [PATCH 14/15] fix: removed the cond assignment to `ErrorType` to avoid linting issues --- pkg/sema/analyzer.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/sema/analyzer.go b/pkg/sema/analyzer.go index ce90399..149df40 100644 --- a/pkg/sema/analyzer.go +++ b/pkg/sema/analyzer.go @@ -663,7 +663,6 @@ func (a *Analyzer) visitStatement(stmt ast.Stmt) { cond := a.inferExprType(n.Condition) if !types2.IsAssignable(cond, types2.BoolType()) { a.errorf(n.Condition, "while condition must be bool, got %s", cond.Name) - cond = types2.ErrorType() } for _, stmt := range n.Body { From 7353a591b670fccc1af15dea8f79b65f8d0809d9 Mon Sep 17 00:00:00 2001 From: GraySS Date: Sat, 1 Aug 2026 00:14:11 +0200 Subject: [PATCH 15/15] fix: updated syntax in parser to use `loop` keyword as loop block starter instead of `do` --- pkg/parser/parser_test.go | 2 +- pkg/parser/statement.go | 2 +- pkg/sema/analyzer_test.go | 10 +++++----- tests/codegen/c_codegen_test.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index cb87b34..c78a30d 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -149,7 +149,7 @@ func TestParserIfElse(t *testing.T) { func TestParserWhile(t *testing.T) { program, diag := parseProgram(t, ` - while true do + while true loop return 1 end `) diff --git a/pkg/parser/statement.go b/pkg/parser/statement.go index b5ed4d5..ce31c48 100644 --- a/pkg/parser/statement.go +++ b/pkg/parser/statement.go @@ -485,7 +485,7 @@ func (p *Parser) parseWhile() ast.Stmt { tok := p.advance() condition := p.parseExpression(PrecLowest) - p.expect(token.KwDo, "after while condition") + p.expect(token.KwLoop, "after while condition") body := p.parseBlock(token.KwEnd) p.expect(token.KwEnd, "to close while") diff --git a/pkg/sema/analyzer_test.go b/pkg/sema/analyzer_test.go index 94ecaa0..6d45eb4 100644 --- a/pkg/sema/analyzer_test.go +++ b/pkg/sema/analyzer_test.go @@ -82,7 +82,7 @@ end` func TestSemanticWhileLoopConditionTypeMismatch(t *testing.T) { input := `fn main: int do var mut x: int = 0; - while x do + while x loop x = x + 1; end return x; @@ -221,7 +221,7 @@ func TestSemanticWhileLoop(t *testing.T) { input := ` fn main: int do var mut x: int = 0; - while x < 10 do + while x < 10 loop x = x + 1; end return x; @@ -232,7 +232,7 @@ func TestSemanticWhileLoop(t *testing.T) { func TestSemanticWhileLoopBreak(t *testing.T) { input := `fn main: int do var mut x: int = 0 - while x < 10 do + while x < 10 loop stop end @@ -375,7 +375,7 @@ end` func TestSemanticUnusedVarInWhileLoop(t *testing.T) { input := `fn main: int do var mut x: int = 0; - while x < 10 do + while x < 10 loop var y: int = 42; x = x + 1; end @@ -396,7 +396,7 @@ end` func TestSemanticUsedVarInWhileLoop(t *testing.T) { input := `fn main: int do var mut x: int = 0; - while x < 10 do + while x < 10 loop var mut y: int = 42; x = x + y + 1; end diff --git a/tests/codegen/c_codegen_test.go b/tests/codegen/c_codegen_test.go index 40e35c4..f8c962c 100644 --- a/tests/codegen/c_codegen_test.go +++ b/tests/codegen/c_codegen_test.go @@ -280,7 +280,7 @@ end input: ` fn main: int do var mut x: int = 0 - while x < 10 do + while x < 10 loop x = x + 1 end return x