From d51ca57c9e07f73d67ec9884e19de437ef53a3ab Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 10 Oct 2026 01:26:32 +0200 Subject: [PATCH] feat: execute break and continue in while loops --- README.md | 4 +- programs/loop_control.hon | 13 +++ source/ast/ast.hon | 16 ++- source/ast/renderer/renderer.hon | 2 + source/diagnostics/diagnostics.hon | 4 + source/lexer/scan.hon | 2 + source/lexer/scan.test.hon | 21 ++++ source/lexer/token.hon | 1 + source/lower/lower.hon | 22 ++++ source/lower/lower.test.hon | 179 +++++++++++++++++++++++++++-- source/parser/parse.hon | 30 ++++- source/parser/parse.test.hon | 122 ++++++++++++++++++++ source/sema/check/check.hon | 26 ++++- source/sema/check/check.test.hon | 144 +++++++++++++++++++++++ source/sema/resolve.hon | 1 + source/sema/resolve.test.hon | 46 ++++++++ 16 files changed, 612 insertions(+), 21 deletions(-) create mode 100644 programs/loop_control.hon diff --git a/README.md b/README.md index c50138d..a0abb6c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca `while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry. +`break` exits the nearest enclosing `while`; `continue` restarts its entire condition, including short-circuit expressions and calls. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`. + name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling scopes may independently reuse names. `sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration. @@ -65,4 +67,4 @@ type errors carry source spans and diagnostic identities; invalid operands propa `interpreter.run` consumes only IR and returns a value or a diagnostic trap. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. -recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for`, `break`/`continue` and compound assignment remain unimplemented. +recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for` and compound assignment remain unimplemented. diff --git a/programs/loop_control.hon b/programs/loop_control.hon new file mode 100644 index 0000000..fc22bc3 --- /dev/null +++ b/programs/loop_control.hon @@ -0,0 +1,13 @@ +main proc() int { + number := 0 + total := 0 + + while true { + number = number + 1 + if (number == 3) continue + if (number > 5) break + total = total + number + } + + return total +} diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 6be84a3..66f0584 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -7,7 +7,7 @@ #! * decl ..... -> identifier [type] binding expr #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) -#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'if' expr arm ['else' arm] | 'while' expr arm +#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm #! braceless guards require parentheses unless the condition is a call; else binds to the nearest if. #! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or. #! binary operators associate left; unary operators associate right. @@ -51,6 +51,8 @@ StmtKind :: enum { expression, assignment_stmt, return_stmt, + break_stmt, + continue_stmt, if_stmt, while_stmt, invalid @@ -84,6 +86,8 @@ NodeKind :: enum { stmt_expr, stmt_decl, stmt_return, + stmt_break, + stmt_continue, stmt_if, stmt_assignment, stmt_while, @@ -314,6 +318,14 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me }, span) } +add_break_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError { + return try add_node(builder, Node{ kind = .stmt_break }, span) +} + +add_continue_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError { + return try add_node(builder, Node{ kind = .stmt_continue }, span) +} + add_if_stmt proc( builder @mut Builder, span Span, @@ -491,6 +503,8 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind { .stmt_decl: .declaration .stmt_expr: .expression .stmt_return: .return_stmt + .stmt_break: .break_stmt + .stmt_continue: .continue_stmt .stmt_if: .if_stmt .stmt_assignment: .assignment_stmt .stmt_while: .while_stmt diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index 3d67484..ba4b94b 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -57,6 +57,8 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression))) try append_text(renderer, "\n") } + .break_stmt: try append_text(renderer, "break\n") + .continue_stmt: try append_text(renderer, "continue\n") .return_stmt: { try append_text(renderer, "return") if (ast.return_stmt(tree, statement).value) |value| { diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 57f3ded..ba96dfa 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -30,6 +30,8 @@ Code :: enum { return_type_mismatch, condition_not_bool, invalid_assignment_target, + break_outside_loop, + continue_outside_loop, missing_entry, invalid_entry, literal_out_of_range, @@ -98,6 +100,8 @@ messages :: $enummap.init(Code, []u8, { return_type_mismatch = "return does not match the function result type", condition_not_bool = "condition must have type bool", invalid_assignment_target = "assignment requires a mutable local binding", + break_outside_loop = "break requires an enclosing loop", + continue_outside_loop = "continue requires an enclosing loop", missing_entry = "program requires a main function", invalid_entry = "main must be a function with no parameters", literal_out_of_range = "numeric literal is outside its runtime type's range", diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index be0a046..bda183f 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -22,6 +22,8 @@ SourceError :: enum { source_too_large } keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "proc", .proc }, { "return", .return }, + { "break", .break }, + { "continue", .continue }, { "if", .if }, { "for", .for }, { "else", .else }, diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 87abd38..c08bca0 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -79,3 +79,24 @@ scans_logical_operators_without_splitting_identifiers test { try testing.expect_equal(u32(35), state.tokens.items[7].span.end) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) } + +scans_loop_controls_with_exact_keyword_boundaries test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + state := init(mem.c_allocator) + defer deinit(&state) + source :: "break continue breakfast continued break_1 continue2" + try scan(&state, source, &diagnostic_store) + expected [7]TokenKind :: [.break, .continue, .ident, .ident, .ident, .ident, .eof] + try testing.expect_equal(expected.len, state.tokens.items.len) + for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind) + try testing.expect_equal(u32(0), state.tokens.items[0].span.start) + try testing.expect_equal(u32(5), state.tokens.items[0].span.end) + try testing.expect_equal(u32(6), state.tokens.items[1].span.start) + try testing.expect_equal(u32(14), state.tokens.items[1].span.end) + try testing.expect_equal("breakfast", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?) + try testing.expect_equal("continued", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 27dd4ae..36f4c97 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -21,6 +21,7 @@ TokenKind :: enum { if, else for, while proc, return + break, continue true, false and, or diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 8dca4c9..926a9d7 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -16,6 +16,12 @@ Storage :: struct { index u32 = 0 } +@hide:file +LoopTargets :: struct { + guard ir.BlockId + exit ir.BlockId +} + @hide:file State :: struct { tree @ast.Tree @@ -28,6 +34,7 @@ State :: struct { function usize = 0 block ir.BlockId = ir.BlockId(0) terminated bool = false + loop ?LoopTargets = null } #! lowers checked source into caller-owned executable storage; release with ir.deinit. @@ -357,6 +364,9 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn alternative = exit, span = ast.node_span(state.tree, loop.condition), } + previous_loop :: state.loop + state.loop = LoopTargets{ guard = guard, exit = exit } + defer state.loop = previous_loop state.block = body try lower_block(state, loop.body, true) if (!state.terminated) { @@ -374,6 +384,18 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d match ast.stmt_kind(tree, id) { .if_stmt: try lower_if(state, id) .while_stmt: try lower_while(state, id) + .break_stmt, .continue_stmt: { + if (trap_type(state, id)) return + if (state.loop) |targets| { + target := targets.exit + # continue restarts the whole guard, before any short-circuit branches. + if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.guard + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = target, span = ast.node_span(tree, id), + } + state.terminated = true + } else unreachable + } .assignment_stmt: { assignment :: ast.assignment_stmt(tree, id) if (trap_binding(state, assignment.target) or trap_type(state, id)) return diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index 0e2a322..c9e45b5 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -443,6 +443,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test { ` pass :: identity(text) ` identity(pass) ` remaining = remaining - 1 + ` inner := 2 + ` while inner > 0 { + ` inner = inner - 1 + ` if (inner > 0) continue else break + ` } + ` if (remaining > 0) continue else break ` } ` if computed > 0 and (enabled or false) { ` saved :: identity(text) @@ -475,12 +481,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test { try testing.expect_equal(usize(0), state.live) continue } - outcome :: interpreter.run(mem.c_allocator, &module, &diagnostic_store) catch |error| { - ir.deinit(&module) - return error - } - ir.deinit(&module) + errdefer ir.deinit(&module) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) try testing.expect(outcome.trap == null) + try testing.expect(mem.eql(outcome.value.data.string, "owned\nstring")) + ir.deinit(&module) try testing.expect_equal(usize(0), state.live) succeeded = true break @@ -763,14 +768,26 @@ forward_assignments_check_initialization_after_evaluating_the_rhs test { } loop_and_reentered_arm_locals_are_uninitialized_before_later_declarations test { - sources [5][]u8 :: [ + sources [11][]u8 :: [ "main proc() int {\n n := 2\n while n > 0 {\n if n == 1 { return later }\n later :: 42\n n = n - 1\n }\n return 0\n}\n", "main proc() int {\n n := 2\n while n > 0 {\n if n == 1 {\n later = 42\n return later\n }\n later := 0\n n = n - 1\n }\n return 0\n}\n", "main proc() int {\n n := 2\n while n > 0 {\n if true {\n if n == 1 { return later }\n later :: 42\n }\n n = n - 1\n }\n return 0\n}\n", "main proc() int {\n n := 2\n while n > 0 {\n if true {\n if n == 1 {\n later = 42\n return later\n }\n later := 0\n }\n n = n - 1\n }\n return 0\n}\n", "main proc() int {\n n := 2\n while n > 0 {\n if false {} else {\n if n == 1 { return later }\n later :: 42\n }\n n = n - 1\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if true {\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if true {\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if false {} else {\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n", + "main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if false {} else {\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n", + ] + codes [11]diagnostics.Code :: [ + .read_before_initialization, .write_before_initialization, + .read_before_initialization, .write_before_initialization, .read_before_initialization, + .read_before_initialization, .write_before_initialization, + .read_before_initialization, .write_before_initialization, + .read_before_initialization, .write_before_initialization, ] - codes [5]diagnostics.Code :: [.read_before_initialization, .write_before_initialization, .read_before_initialization, .write_before_initialization, .read_before_initialization] for (sources) |source, index| { diagnostic_store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&diagnostic_store) @@ -835,17 +852,19 @@ zero_iteration_body_errors_are_checked_but_not_executed test { } loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_overflow test { - sources [3][]u8 :: [ + sources [5][]u8 :: [ "main proc() int {\n while true { return 42 }\n}\n", "main proc() int {\n while false { return 42 }\n}\n", "main proc() int {\n value := 9223372036854775807\n while true {\n value = value + 1\n return value\n }\n return 0\n}\n", + "main proc() int {\n while true { break }\n}\n", + "main proc() int {\n while false { continue }\n}\n", ] for (sources) |source, index| { diagnostic_store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) - if (index < 2) { + if (index != 2) { try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code) @@ -855,16 +874,17 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } else { - code diagnostics.Code :: if (index == 1) .missing_return else .integer_overflow + code diagnostics.Code :: if (index == 2) .integer_overflow else .missing_return try expect_trap(outcome, &diagnostic_store, code) + if (index != 2) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) } } } bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage test { sources [2][]u8 :: [ - "main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n", - "main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n", + "main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n if (n == 1) break\n continue\n later :: 0\n }\n return n\n}\n", + "main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n if (n == 10000) break\n continue\n later :: 0\n }\n return n\n}\n", ] expected [2]i64 :: [1, 10000] allocations := usize(0) @@ -1098,6 +1118,11 @@ lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation te `later :: false or !false `both proc(left bool, right bool) bool { return left and right } `main proc() bool { + ` n := 3 + ` while both(first and first, both(false or later, !false and later)) and n > 0 { + ` n = n - 1 + ` if (n > 0) continue else break + ` } ` return both(first and first, both(false or later, !false and later)) `} module := try compile_owned(source, &diagnostic_store) @@ -1144,3 +1169,133 @@ skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test { try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) } } + +break_and_continue_skip_iterations_exit_early_and_restore_outer_targets test { + sources [3][]u8 :: [ + "main proc() int {\n n := 0\n total := 0\n while n < 10 {\n n = n + 1\n if (n == 2 or n == 4) continue\n if (n == 6) break\n total = total + n\n }\n return total + 33\n}\n", + "main proc() int {\n outer := 3\n total := 0\n while outer > 0 {\n outer = outer - 1\n inner := 3\n while inner > 0 {\n inner = inner - 1\n if (inner == 1) continue\n if (inner == 0) break\n total = total + 7\n }\n if (outer > 0) continue else break\n }\n return total + 21\n}\n", + "main proc() int {\n n := 0\n while n < 5 {\n n = n + 1\n if (n < 3) continue else break\n n = n + 100\n }\n return n + 39\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } +} + +continue_restarts_composed_calling_guards_after_branchful_body_calls test { + guards [3][]u8 :: [ + "(positive(3 - n) and !(n == 2)) or n == 2", + "!(n >= 3) and (positive(3 - n) or false)", + "positive(3 - n) and ((n < 3 or false) and !false)", + ] + for (guards) |guard| { + source := arraylist.init(u8, mem.c_allocator) + defer arraylist.deinit(&source) + for ("positive proc(n int) bool { return n > 0 }\nadvance proc(flag bool, n int) int {\n if flag { return n + 1 }\n return n + 1\n}\nmain proc() int {\n n := 0\n while ") |byte| try arraylist.append(&source, byte) + for (guard) |byte| try arraylist.append(&source, byte) + # bound a stale guard too, so incorrect continue lowering fails instead of hanging. + for (" {\n n = advance(n == 0 or n == 1, n)\n if (n > 5) break\n continue\n n = n + 100\n }\n return n + 39\n}\n") |byte| try arraylist.append(&source, byte) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source.items, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } +} + +reached_outside_controls_preserve_diagnostic_identity_and_keyword_span test { + sources [4][]u8 :: [ + "main proc() int {\n break\n return 42\n}\n", + "main proc() int {\n continue\n return 42\n}\n", + "bad proc() void {\n break\n}\nmain proc() int {\n while true { bad()\n break }\n return 42\n}\n", + "main proc() int {\n while true { break }\n continue\n return 42\n}\n", + ] + codes [4]diagnostics.Code :: [.break_outside_loop, .continue_outside_loop, .break_outside_loop, .continue_outside_loop] + keywords [4][]u8 :: ["break", "continue", "break", "continue"] + for (sources) |source, index| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? + try testing.expect_equal(codes[index], original.code) + try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], keywords[index])) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +untaken_outside_controls_remain_checked_without_trapping test { + sources [5][]u8 :: [ + "main proc() int {\n if (false) break\n return 42\n}\n", + "main proc() int {\n if true { return 42 } else continue\n}\n", + "main proc() int {\n return 42\n break\n}\n", + "main proc() int {\n return 42\n continue\n}\n", + "bad proc() void {\n continue\n}\nmain proc() int {\n while false { bad() }\n return 42\n}\n", + ] + codes [5]diagnostics.Code :: [.break_outside_loop, .continue_outside_loop, .break_outside_loop, .continue_outside_loop, .continue_outside_loop] + for (sources) |source, index| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? + try testing.expect_equal(codes[index], original.code) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +source_errors_after_loop_transfers_stay_checked_but_unreached test { + sources [4][]u8 :: [ + "main proc() int {\n n := 2\n while n > 0 {\n n = n - 1\n continue\n missing\n }\n return 42\n}\n", + "main proc() int {\n while true {\n break\n missing\n }\n return 42\n}\n", + "main proc() int {\n n := 2\n while n > 0 {\n n = n - 1\n if (n > 0) continue else break\n missing\n }\n return 42\n}\n", + "main proc() int {\n while true {\n return 42\n break\n continue\n missing\n }\n return 0\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? + try testing.expect_equal(diagnostics.Code.unknown_name, original.code) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +traps_before_loop_controls_keep_the_original_error test { + sources [2][]u8 :: [ + "main proc() int {\n while true {\n missing\n break\n }\n return 42\n}\n", + "main proc() int {\n while true {\n missing\n continue\n }\n return 42\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try expect_trap(outcome, &diagnostic_store, .unknown_name) + try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 5bdde95..1920cc8 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -186,6 +186,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError .if: return try parse_if(state) .while: return try parse_while(state, allow_else) .return: return try parse_return(state, allow_else) + .break, .continue: return try parse_loop_control(state, allow_else) .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: { expression :: try parse_expr(state) return try ast.add_expression_stmt(&state.builder, expression) @@ -219,6 +220,23 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { ) } +@hide:file +parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { + keyword :: current(state) + state.next_token += 1 + if ( + !next_is(state, .newline) + and !next_is(state, .close_curly) + and !next_is(state, .eof) + and !(allow_else and next_is(state, .else)) + ) return .unexpected_token + + if (keyword.kind == .break) + return try ast.add_break_stmt(&state.builder, keyword.span) + + return try ast.add_continue_stmt(&state.builder, keyword.span) +} + @hide:file parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { name :: try consume(state, .ident) @@ -302,6 +320,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError { and !next_is(state, .newline) and !next_is(state, .eof) and !next_is(state, .else) and !next_is(state, .return) and !next_is(state, .if) and !next_is(state, .while) + and !next_is(state, .break) and !next_is(state, .continue) ) { if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break state.next_token += 1 @@ -467,7 +486,7 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) defer arraylist.deinit(&arguments) - while (!is_expression_boundary(current(state).kind)) { + while (!is_call_boundary(current(state).kind)) { argument :: try parse_expr(state) try arraylist.append(&arguments, argument) if (!next_is(state, .comma)) break @@ -552,6 +571,15 @@ is_expression_boundary proc(kind lexer.TokenKind) bool { } } +@hide:file +is_call_boundary proc(kind lexer.TokenKind) bool { + return ( + kind == .break + or kind == .continue + or is_expression_boundary(kind) + ) +} + @hide:file is_expression_recovery_boundary proc(kind lexer.TokenKind) bool { return ( diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 27d0db1..01e8b10 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -961,3 +961,125 @@ recovers_missing_logical_operands_and_preserves_enclosing_nodes test { after :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) try testing.expect_equal("after", ast.source_text(&tree, after.name)) } + +parses_leaf_loop_controls_and_nearest_else test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `main proc() void { + ` while (true) { + ` if (true) if (false) break else continue + ` while (false) continue + ` break + ` } + ` while (false) if (true) break else continue + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(2, ast.block_statement_count(&tree, body)) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0)) + try testing.expect_equal(3, ast.block_statement_count(&tree, loop.body)) + outer_if :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 0)) + try testing.expect(outer_if.else_block == null) + inner_if :: ast.if_stmt(&tree, ast.block_statement(&tree, outer_if.then_block, 0)) + transfer :: ast.block_statement(&tree, inner_if.then_block, 0) + try testing.expect_equal(ast.StmtKind.break_stmt, ast.stmt_kind(&tree, transfer)) + try testing.expect_equal("break", ast.source_text(&tree, ast.node_span(&tree, transfer))) + if (inner_if.else_block) |other| { + alternate :: ast.block_statement(&tree, other, 0) + try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, alternate)) + try testing.expect_equal("continue", ast.source_text(&tree, ast.node_span(&tree, alternate))) + } else return .expectation_failed + nested :: ast.while_stmt(&tree, ast.block_statement(&tree, loop.body, 1)) + try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, nested.body, 0))) + braceless :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1)) + conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, braceless.body, 0)) + try testing.expect(conditional.else_block != null) +} + +rejects_control_tails_and_preserves_following_declarations test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `break + `continue + `bad :: break + `bad_continue :: continue + `main proc() void { + ` while (true) { + ` break 1 + ` first :: 1 + ` continue label + ` second :: 2 + ` if (true) break label else continue + ` if (true) continue 2 else break + ` break 3 } + ` after :: 3 + `} + `last :: 4 + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(9, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(6, ast.package_decl_count(&tree)) + for (usize(0)..usize(2)) |index| try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, index))) + for (usize(2)..usize(4)) |index| { + declaration :: ast.package_decl(&tree, index) + try testing.expect_equal(ast.DeclKind.statement, ast.decl_kind(&tree, declaration)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.statement_decl(&tree, declaration).initializer)) + } + body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body + try testing.expect_equal(2, ast.block_statement_count(&tree, body)) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0)) + try testing.expect_equal(7, ast.block_statement_count(&tree, loop.body)) + for ([usize(1), usize(3)]) |index| { + try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, index))) + } + conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 4)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, conditional.then_block, 0))) + if (conditional.else_block) |other| { + try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, other, 0))) + } else return .expectation_failed + try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1))) + try testing.expect_equal(ast.DeclKind.statement, ast.decl_kind(&tree, ast.package_decl(&tree, 5))) +} + +preserves_control_bodies_after_malformed_guards test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `main proc() void { + ` while (true break + ` while predicate( continue + ` while (true) { continue } + ` after :: 1 + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(4, ast.block_statement_count(&tree, body)) + expected [3]ast.StmtKind :: [.break_stmt, .continue_stmt, .continue_stmt] + for (expected) |kind, index| { + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, index)) + try testing.expect_equal(kind, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0))) + if (index < 2) try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, loop.condition)) + } + try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3))) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index c6a3552..28e86c9 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -40,6 +40,7 @@ Role :: enum { if_stmt, assignment_stmt, while_stmt, + loop_control, } @hide:file @@ -510,11 +511,11 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | state.cells[usize(id)].type = invalid_type(binding.data.diagnostic) state.cells[usize(id)].type.function = id } - try collect_block(state, function.body, function.return_type) + try collect_block(state, function.body, function.return_type, false) } @hide:file -collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { +collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree state.cells[usize(id)].role = .body for (0..ast.block_statement_count(tree, id)) |i| { @@ -540,10 +541,10 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m state.cells[usize(statement)].type = Type{ kind = .void } try collect_expression(state, conditional.condition) try depend(state, conditional.condition, statement) - try collect_block(state, conditional.then_block, return_annotation) + try collect_block(state, conditional.then_block, return_annotation, in_loop) try depend(state, conditional.then_block, id) if (conditional.else_block) |other| { - try collect_block(state, other, return_annotation) + try collect_block(state, other, return_annotation, in_loop) try depend(state, other, id) } } @@ -573,9 +574,22 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m state.cells[usize(statement)].type = Type{ kind = .void } try collect_expression(state, loop.condition) try depend(state, loop.condition, statement) - try collect_block(state, loop.body, return_annotation) + try collect_block(state, loop.body, return_annotation, true) try depend(state, loop.body, id) } + .break_stmt, .continue_stmt: { + state.cells[usize(statement)].role = .loop_control + state.cells[usize(statement)].type = Type{ kind = .void } + if (!in_loop) { + code :: if (ast.stmt_kind(tree, statement) == .break_stmt) + diagnostics.Code.break_outside_loop + else + diagnostics.Code.continue_outside_loop + state.cells[usize(statement)].type = invalid_type(try diagnostics.add( + state.diagnostics, code, ast.node_span(tree, statement), + )) + } + } .invalid: collect_invalid(state, statement) } source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement @@ -889,7 +903,7 @@ block_falls_through proc(state @State, id ast.NodeId) bool { source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement if (state.types[usize(source)].kind == .invalid) return false match kind { - .return_stmt, .invalid: return false + .return_stmt, .break_stmt, .continue_stmt, .invalid: return false .if_stmt: { conditional :: ast.if_stmt(tree, statement) then_falls :: block_falls_through(state, conditional.then_block) diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 6df1e7f..9ffa8da 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -1548,3 +1548,147 @@ short_circuit_skip_paths_do_not_guarantee_a_return test { try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand)) try testing.expect_equal(18, diagnostics.count(&diagnostic_store)) } + +loop_controls_follow_lexical_bodies_and_check_unreachable_code test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `scopes proc(flag bool) void { + ` while flag { + ` if (flag) break else continue + ` while flag { + ` if (flag) continue else break + ` } + ` absent + ` } + ` break + ` continue + `} + `caller proc() void { + ` while true { + ` callee() + ` break + ` } + `} + `callee proc() void { continue } + `dead proc() void { + ` return + ` break + ` continue + `} + `arms proc() void { + ` if (false) break else continue + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + scopes_body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + outer_statement :: ast.block_statement(&tree, scopes_body, 0) + outer :: ast.while_stmt(&tree, outer_statement) + outer_if :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.body, 0)) + inner_statement :: ast.block_statement(&tree, outer.body, 1) + inner :: ast.while_stmt(&tree, inner_statement) + inner_if :: ast.if_stmt(&tree, ast.block_statement(&tree, inner.body, 0)) + caller_body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body + caller_loop :: ast.while_stmt(&tree, ast.block_statement(&tree, caller_body, 0)) + valid [5]ast.NodeId :: [ + ast.block_statement(&tree, outer_if.then_block, 0), + ast.block_statement(&tree, outer_if.else_block?, 0), + ast.block_statement(&tree, inner_if.then_block, 0), + ast.block_statement(&tree, inner_if.else_block?, 0), + ast.block_statement(&tree, caller_loop.body, 1), + ] + for (valid) |statement| try expect_kind(&result, statement, .void) + try expect_kind(&result, outer_statement, .void) + try expect_kind(&result, inner_statement, .void) + unknown :: ast.statement_expr(&tree, ast.block_statement(&tree, outer.body, 2)) + original :: sema.binding(&resolution, unknown).data.diagnostic + try testing.expect_equal(original, type_of(&result, unknown).diagnostic) + outside_break :: ast.block_statement(&tree, scopes_body, 1) + outside_continue :: ast.block_statement(&tree, scopes_body, 2) + _ = try expect_error(&result, &diagnostic_store, outside_break, .break_outside_loop, ast.node_span(&tree, outside_break)) + _ = try expect_error(&result, &diagnostic_store, outside_continue, .continue_outside_loop, ast.node_span(&tree, outside_continue)) + callee_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body + callee_control :: ast.block_statement(&tree, callee_body, 0) + _ = try expect_error(&result, &diagnostic_store, callee_control, .continue_outside_loop, ast.node_span(&tree, callee_control)) + dead_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body + dead_break :: ast.block_statement(&tree, dead_body, 1) + dead_continue :: ast.block_statement(&tree, dead_body, 2) + _ = try expect_error(&result, &diagnostic_store, dead_break, .break_outside_loop, ast.node_span(&tree, dead_break)) + _ = try expect_error(&result, &diagnostic_store, dead_continue, .continue_outside_loop, ast.node_span(&tree, dead_continue)) + arms_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body + arms :: ast.if_stmt(&tree, ast.block_statement(&tree, arms_body, 0)) + arm_break :: ast.block_statement(&tree, arms.then_block, 0) + arm_continue :: ast.block_statement(&tree, arms.else_block?, 0) + _ = try expect_error(&result, &diagnostic_store, arm_break, .break_outside_loop, ast.node_span(&tree, arm_break)) + _ = try expect_error(&result, &diagnostic_store, arm_continue, .continue_outside_loop, ast.node_span(&tree, arm_continue)) + for (usize(0)..usize(5)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function) + try testing.expect_equal(3, code_count(&diagnostic_store, .break_outside_loop)) + try testing.expect_equal(4, code_count(&diagnostic_store, .continue_outside_loop)) + try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name)) + try testing.expect_equal(8, diagnostics.count(&diagnostic_store)) +} + +loop_controls_do_not_guarantee_function_returns test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `breaking proc() int { while true { break } } + `continuing proc() int { while false { continue } } + `mixed proc(flag bool) int { + ` while flag { + ` if (flag) return 42 else break + ` } + `} + `closed proc(flag bool) int { + ` while flag { + ` if (flag) break else continue + ` -"bad" + ` } + ` return 42 + `} + `invalid proc() int { break } + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + for (usize(0)..usize(3)) |index| { + body :: ast.function_decl(&tree, ast.package_decl(&tree, index)).body + _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) + } + closed_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body + loop_statement :: ast.block_statement(&tree, closed_body, 0) + loop :: ast.while_stmt(&tree, loop_statement) + dead_expression :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 1)) + original :: try expect_error(&result, &diagnostic_store, dead_expression, .invalid_operand, ast.node_span(&tree, dead_expression)) + try testing.expect_equal(original, type_of(&result, closed_body).diagnostic) + try expect_kind(&result, loop_statement, .void) + try expect_kind(&result, ast.block_statement(&tree, closed_body, 1), .void) + invalid_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body + invalid_control :: ast.block_statement(&tree, invalid_body, 0) + control_error :: try expect_error(&result, &diagnostic_store, invalid_control, .break_outside_loop, ast.node_span(&tree, invalid_control)) + try testing.expect_equal(control_error, type_of(&result, invalid_body).diagnostic) + for (usize(0)..usize(5)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function) + try testing.expect_equal(3, code_count(&diagnostic_store, .missing_return)) + try testing.expect_equal(1, code_count(&diagnostic_store, .invalid_operand)) + try testing.expect_equal(1, code_count(&diagnostic_store, .break_outside_loop)) + try testing.expect_equal(5, diagnostics.count(&diagnostic_store)) +} diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 4095973..cd2f6ff 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -203,6 +203,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me .return_stmt: { if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value) } + .break_stmt, .continue_stmt: {} .if_stmt: { conditional :: ast.if_stmt(tree, statement) try resolve_expr(state, scope, conditional.condition) diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index 4d61e30..0d6148a 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -439,3 +439,49 @@ resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test { try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind) try testing.expect_equal(5, diagnostics.count(&diagnostic_store)) } + +resolves_all_code_and_preserves_scopes_after_loop_controls test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `main proc() void { + ` while (true) { + ` break + ` before :: later + ` continue + ` later :: 1 + ` missing_after_break + ` if (true) { + ` continue + ` private :: later + ` int :: 2 + ` } + ` private + ` } + ` later + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store) + defer deinit(&resolution) + try testing.expect_equal(4, diagnostics.count(&diagnostic_store)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0)) + before :: ast.block_statement(&tree, loop.body, 1) + later :: ast.block_statement(&tree, loop.body, 3) + reference :: ast.statement_decl(&tree, before).initializer + try testing.expect_equal(BindingKind.declaration, binding(&resolution, reference).kind) + try testing.expect_equal(later, binding(&resolution, reference).data.declaration) + conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 5)) + private_decl :: ast.block_statement(&tree, conditional.then_block, 1) + private_ref :: ast.statement_decl(&tree, private_decl).initializer + try testing.expect_equal(later, binding(&resolution, private_ref).data.declaration) + forbidden :: ast.block_statement(&tree, conditional.then_block, 2) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, forbidden).kind) +}