break and continue in loops

This commit is contained in:
2026-06-26 23:43:57 +02:00
parent f926bbc605
commit 81ae939bf0
11 changed files with 314 additions and 12 deletions
+2
View File
@@ -129,6 +129,8 @@ Stmt_Kind :: enum u8 {
If,
While,
For,
Break,
Continue,
}
Assignment_Op :: enum u8 {
+49 -1
View File
@@ -68,6 +68,9 @@ Build_Ctx :: struct {
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
problematic: ^bool,
// Number of enclosing loops being built. `break`/`continue` are only valid
// when this is > 0; bumped around loop-body builds in `build_block`.
loop_depth: int,
}
Constant_Kind :: enum {
@@ -734,6 +737,7 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
case .For:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
case .Break, .Continue:
case .Invalid:
}
}
@@ -4439,7 +4443,9 @@ build_block :: proc(
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true
}
ctx.loop_depth += 1
loop_body := build_block(ctx, statement.body)
ctx.loop_depth -= 1
update := hir.INVALID_STMT
if statement.update != ast.INVALID_STMT {
update_ast := [1]ast.Stmt_Id{statement.update}
@@ -4531,7 +4537,9 @@ build_block :: proc(
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
}
}
ctx.loop_depth += 1
loop_body := build_block(ctx, statement.body, capture_start)
ctx.loop_depth -= 1
resize(ctx.locals, capture_start)
append(&body, hir.stmt_id(len(checker.module.statements)))
@@ -4562,6 +4570,24 @@ build_block :: proc(
})
ctx.problematic^ = true
}
case .Break, .Continue:
if ctx.loop_depth == 0 {
keyword := "break" if statement.kind == .Break else "continue"
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Break if statement.kind == .Break else .Continue,
span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
@@ -4593,9 +4619,12 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
return true
}
case .While:
// A literal `while true` makes the end of the block unreachable —
// unless its body can `break` out of this loop.
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
condition := module.exprs[statement.expr]
if condition.kind == .Bool && condition.integer != 0 {
if condition.kind == .Bool && condition.integer != 0 &&
!loop_body_breaks(module, statement.then_body) {
return true
}
}
@@ -4604,6 +4633,25 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
return false
}
// Reports whether `stmts` contains a `break` that targets the enclosing loop:
// a `.Break` at this level or inside `if`/`else` branches counts, but a `break`
// inside a nested `.While`/`.For` targets that inner loop, so we do not descend.
loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts {
statement := module.statements[id]
#partial switch statement.kind {
case .Break:
return true
case .If:
if loop_body_breaks(module, statement.then_body) ||
loop_body_breaks(module, statement.else_body) {
return true
}
}
}
return false
}
build_function :: proc(checker: ^Checker, id: Spec_Id) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
+2
View File
@@ -154,6 +154,8 @@ Stmt_Kind :: enum u8 {
If,
While,
For,
Break,
Continue,
}
Assignment_Op :: enum u8 {
+2
View File
@@ -32,6 +32,8 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "if": return .Keyword_If
case "while": return .Keyword_While
case "for": return .Keyword_For
case "break": return .Keyword_Break
case "continue": return .Keyword_Continue
case "else": return .Keyword_Else
case "true": return .Keyword_True
case "false": return .Keyword_False
+15 -6
View File
@@ -652,12 +652,18 @@ emit_instruction_stream :: proc(
sret_name := "",
) -> ir.Instruction_Id {
return_value := ir.INVALID_INSTRUCTION
after_return := false
// Set after any terminator (`ret`, `br`, conditional `br`). Code reachable
// only by falling off a terminator is dead; it needs a fresh label to form a
// well-formed basic block unless the next instruction is already a `.Label`,
// which opens its own block (the normal terminator-then-label sequence).
after_terminator := false
for instruction, instruction_index in instructions {
instruction_id := ir.instruction_id(instruction_index)
if after_return {
fmt.sbprintf(&emitter.builder, "recover_after_return_%d:\n", instruction_index)
after_return = false
if after_terminator {
if instruction.op != .Label {
fmt.sbprintf(&emitter.builder, "recover_after_return_%d:\n", instruction_index)
}
after_terminator = false
}
switch instruction.op {
case .Param, .Const:
@@ -1662,14 +1668,17 @@ emit_instruction_stream :: proc(
fmt.sbprintf(&emitter.builder, "bro_block_%d:\n", instruction.integer)
case .Br:
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", instruction.integer)
after_terminator = true
case .Cond_Br:
if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) {
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", u32(instruction.target))
after_terminator = true
continue
}
strings.write_string(&emitter.builder, " br i1 ")
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", label %%bro_block_%d, label %%bro_block_%d\n", instruction.integer, u32(instruction.target))
after_terminator = true
case .Trap:
message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source")
emit_trap_call(emitter, message)
@@ -1703,7 +1712,7 @@ emit_instruction_stream :: proc(
write_operand(&emitter.builder, instructions, instruction.a, function.result, &emitter.module.types)
strings.write_string(&emitter.builder, "\n")
}
after_return = true
after_terminator = true
case .Return_Void:
if global_initializer {
continue
@@ -1713,7 +1722,7 @@ emit_instruction_stream :: proc(
} else {
strings.write_string(&emitter.builder, " ret void\n")
}
after_return = true
after_terminator = true
}
}
return return_value
+45
View File
@@ -16,10 +16,19 @@ State :: struct {
func_locals: []hir.Local,
func_result: types.Type,
expr_stack: [dynamic]Lower_Expr_Frame,
// Innermost-last stack of enclosing loop targets for `break`/`continue`.
loops: [dynamic]Loop_Ctx,
next_label: i64,
allocator: mem.Allocator,
}
// `break` branches to `exit_lbl`; `continue` branches to `continue_lbl` (the
// loop's update/latch, which runs the update clause then re-tests the condition).
Loop_Ctx :: struct {
exit_lbl: i64,
continue_lbl: i64,
}
fresh_label :: proc(state: ^State) -> i64 {
id := state.next_label
state.next_label += 1
@@ -742,6 +751,18 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
case .Break, .Continue:
// The checker guarantees these only appear inside a loop, so the
// stack is non-empty; guard defensively regardless.
if len(state.loops) > 0 {
target := state.loops[len(state.loops)-1]
label := target.exit_lbl if statement.kind == .Break else target.continue_lbl
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=label,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
case .Expression, .Sink:
_ = lower_expr(state, statement.expr)
case .Trap:
@@ -919,7 +940,9 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl})
lower_statements(state, statement.then_body)
pop(&state.loops)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
@@ -1043,7 +1066,25 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
// `continue` rejoins the normal end-of-iteration path (via a fresh
// label before the bounds/overflow guard) rather than jumping straight
// to the increment, so it behaves exactly like falling off the body
// e.g. `for 0..=255 |b: u8| { ... continue }` exits cleanly instead of
// overflowing the increment on the final element.
continue_lbl := fresh_label(state)
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=continue_lbl})
lower_statements(state, statement.then_body)
pop(&state.loops)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=continue_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=continue_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
after_body := append_instruction(state, ir.Instruction{
op=.Load, span=statement.span, type=child,
target=ir.INVALID_REF, a=current_slot, b=ir.INVALID_INSTRUCTION,
@@ -1216,7 +1257,9 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
target=ir.INVALID_REF, a=capture_slot, b=captured,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl})
lower_statements(state, statement.then_body)
pop(&state.loops)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
@@ -1267,10 +1310,12 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m
}
state.instructions.allocator = allocator
state.expr_stack.allocator = allocator
state.loops.allocator = allocator
defer {
delete(state.local_values, allocator)
delete(state.local_slots, allocator)
delete(state.expr_stack)
delete(state.loops)
}
for _, index in state.local_values {
state.local_values[index] = ir.INVALID_INSTRUCTION
+22 -1
View File
@@ -986,6 +986,21 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
return id
}
// `break` / `continue` carry no value and target the innermost loop; the
// checker rejects them outside a loop.
parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id {
marker := advance(parser) // consume 'break' / 'continue'
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=kind,
span=marker.span,
expr=ast.INVALID_EXPR,
update=ast.INVALID_STMT,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return id
}
starts_declared_type :: proc(parser: ^Parser) -> bool {
if current(parser).kind != .Left_Bracket {
return is_type_token(current(parser).kind)
@@ -1024,6 +1039,12 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_For {
return parse_for(parser)
}
if current(parser).kind == .Keyword_Break {
return parse_loop_control(parser, .Break)
}
if current(parser).kind == .Keyword_Continue {
return parse_loop_control(parser, .Continue)
}
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
start_cursor := parser.cursor
@@ -1312,7 +1333,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
statement := &parser.module.statements[update]
switch statement.kind {
case .Assignment, .Expression:
case .Invalid, .Declaration, .Return, .If, .While, .For:
case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue:
diagnostic := source.add(
parser.diagnostics,
statement.span,
+2
View File
@@ -67,6 +67,8 @@ Kind :: enum u8 {
Keyword_If,
Keyword_While,
Keyword_For,
Keyword_Break,
Keyword_Continue,
Keyword_Else,
Keyword_True,
Keyword_False,