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
+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]