labeled block statements

This commit is contained in:
2026-06-28 09:07:09 +02:00
parent eaa66511a0
commit 3007910526
5 changed files with 181 additions and 21 deletions
+105 -13
View File
@@ -95,9 +95,12 @@ Build_Ctx :: struct {
// only see loops opened within the defer (those past `loop_floor`).
defers: ^[dynamic][]hir.Stmt_Id,
loop_defer_starts: ^[dynamic]int,
// Parallel to `loop_defer_starts`: the label of each enclosing loop (INVALID when
// unlabeled), so a `break :L` / `continue :L` can target an outer labeled loop.
// Parallel to `loop_defer_starts`: the label of each enclosing break target (INVALID
// when unlabeled), so a `break :L` / `continue :L` can target an outer one. A labeled
// block statement is a break target too; `loop_is_loop` distinguishes loops (which
// `continue` and unlabeled `break`/`continue` target) from value/labeled blocks.
loop_labels: ^[dynamic]symbol.Id,
loop_is_loop: ^[dynamic]bool,
defer_depth: int,
loop_floor: int,
}
@@ -4603,7 +4606,9 @@ build_block :: proc(
}
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true)
loop_body := build_block(ctx, statement.body)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
update := hir.INVALID_STMT
@@ -4700,7 +4705,9 @@ build_block :: proc(
}
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true)
loop_body := build_block(ctx, statement.body, capture_start)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
resize(ctx.locals, capture_start)
@@ -4740,14 +4747,22 @@ build_block :: proc(
// hides the loops opened outside the defer.
target_index := -1
if symbol.is_valid(statement.label) {
// `break :L` targets a labeled loop or block; `continue :L` only a loop.
for i := len(ctx.loop_labels^) - 1; i >= ctx.loop_floor; i -= 1 {
if ctx.loop_labels^[i] == statement.label {
if ctx.loop_labels^[i] == statement.label &&
(ctx.loop_is_loop^[i] || statement.kind == .Break) {
target_index = i
break
}
}
} else {
// Unlabeled `break`/`continue` targets the innermost loop, skipping blocks.
for i := len(ctx.loop_defer_starts^) - 1; i >= ctx.loop_floor; i -= 1 {
if ctx.loop_is_loop^[i] {
target_index = i
break
}
}
} else if len(ctx.loop_defer_starts^) > ctx.loop_floor {
target_index = len(ctx.loop_defer_starts^) - 1
}
if target_index < 0 {
keyword := "break" if statement.kind == .Break else "continue"
@@ -4776,6 +4791,25 @@ build_block :: proc(
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
case .Block:
if symbol.is_valid(statement.label) {
// A labeled block statement (`blk: { … break :blk … }`): a break target
// with an exit-label boundary, built as a HIR `.Block`. Not a loop, so
// unlabeled `break`/`continue` and `continue :blk` skip it.
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, false)
built := build_block(ctx, statement.body)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Block, span = statement.span, label = statement.label,
then_body = built, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
continue
}
// A bare `{ ... }` scope: build it (its own locals/defers are scoped by
// the recursive call) and splice its statements in.
block := build_block(ctx, statement.body)
@@ -5364,6 +5398,62 @@ value_loop_element_type :: proc(ctx: ^Build_Ctx, loop_stmt: ast.Stmt) -> types.T
return result
}
// stmt_contains_yield reports whether a statement contains a `yield` anywhere within it
// (recursing through if/block/loop bodies). Used to stop the leading probe build before any
// statement that yields (the block's yield target is not pushed during the probe).
stmt_contains_yield :: proc(checker: ^Checker, id: ast.Stmt_Id) -> bool {
s := checker.ast_module.statements[id]
#partial switch s.kind {
case .Yield:
return true
case .If, .For, .While, .Block:
for sub in s.body {
if stmt_contains_yield(checker, sub) {
return true
}
}
for sub in s.else_body {
if stmt_contains_yield(checker, sub) {
return true
}
}
}
return false
}
// block_element_type pre-types an untyped value block's element from its first concrete
// `yield :blk`, building the block's leading (yield-free) statements first so the probe can
// reference block locals declared before the first yield. The leading build is a throwaway
// (its scope is restored). INVALID when there is no concrete yield, or the concrete yield
// references a local only in scope past the first yield (annotate the binding instead).
block_element_type :: proc(ctx: ^Build_Ctx, block_stmts: []ast.Stmt_Id) -> types.Type {
checker := ctx.checker
concrete := first_concrete_yield_expr(checker, block_stmts)
if concrete == ast.INVALID_EXPR {
return types.INVALID
}
lead_end := len(block_stmts)
for id, i in block_stmts {
if stmt_contains_yield(checker, id) {
lead_end = i
break
}
}
scope_start := len(ctx.locals^)
defer_start := len(ctx.defers^)
lead := build_block(ctx, block_stmts[:lead_end], close = false)
delete(lead, checker.allocator)
probe := build_expr(checker, concrete, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
// Discard the throwaway leading build's scope (its hir stmts/locals are dead but stable).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
return result
}
// build_value_labeled_block turns `x :: blk: { …; yield :blk v }` into a result slot each
// `yield :blk` assigns (via the build_block `.Yield` desugar → `slot = v; break :blk`), then
// reads it after the block. Every path must yield (or otherwise exit); HIR holds a `.Block`
@@ -5388,14 +5478,12 @@ build_value_labeled_block :: proc(
result_optional = types.is_optional(slot_type, &checker.module.types)
} else if result_optional {
// Untyped block that also yields `none`: pre-type the element from the first
// concrete yield (a block has no captures, so a capture-free probe suffices).
elem := first_concrete_yield_expr(checker, block_stmts)
if elem != ast.INVALID_EXPR {
probe := build_expr(checker, elem, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
if checker.module.exprs[probe].kind != .Invalid {
slot_type = types.optional(&checker.module.types, checker.module.exprs[probe].type)
slot = new_value_slot(ctx, slot_type)
}
// concrete yield (regardless of source order) so a `none` yielded first still
// resolves the result to `?T`.
elem := block_element_type(ctx, block_stmts)
if is_runtime_type(checker, elem) {
slot_type = types.optional(&checker.module.types, elem)
slot = new_value_slot(ctx, slot_type)
}
}
append(ctx.yield_targets, Yield_Target{
@@ -5722,6 +5810,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
loop_defer_starts.allocator = checker.allocator
loop_labels: [dynamic]symbol.Id
loop_labels.allocator = checker.allocator
loop_is_loop: [dynamic]bool
loop_is_loop.allocator = checker.allocator
yield_targets: [dynamic]Yield_Target
yield_targets.allocator = checker.allocator
ctx := Build_Ctx{
@@ -5738,6 +5828,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
defers = &defers,
loop_defer_starts = &loop_defer_starts,
loop_labels = &loop_labels,
loop_is_loop = &loop_is_loop,
yield_targets = &yield_targets,
}
block := build_block(&ctx, function.body)
@@ -5789,6 +5880,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
delete(defers)
delete(loop_defer_starts)
delete(loop_labels)
delete(loop_is_loop)
delete(yield_targets)
delete(locals)
}