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
+10 -3
View File
@@ -391,9 +391,16 @@
loop, a labeled one searches by label). The checker tracks a `loop_labels` stack and a
`Yield_Target.defer_floor`; a `yield :L v` desugars to `slot = v; flush defers to L's body;
break :L`, reusing the milestone-18 break lowering — **no new IR opcode, no emitter change**
- deferred (`// ponytail:`): a labeled bare block as a *plain statement* with `break :blk`;
`none`-before-concrete typing in an untyped block whose first concrete yield references a
block local (annotate instead)
- a labeled bare block as a *plain statement* is also exitable with `break :blk` (a HIR
`.Block` break target; not a loop, so unlabeled `break`/`continue` and `continue :blk` skip
it). The checker tracks a parallel `loop_is_loop` stack so labeled `break` reaches a loop or
block while `continue` and unlabeled `break`/`continue` reach only the innermost loop
- untyped block `none`-before-concrete typing now builds the block's leading (yield-free)
statements first (a throwaway probe), so a first concrete `yield :blk` that references a
block local still resolves the result to `?T`
- deferred (`// ponytail:`): the same `none`-before-concrete typing in an untyped block (or
loop) whose concrete yield references a local declared *past* the first yield (annotate);
same-label loop/block shadowing resolves innermost-wins
21. unions and tagged unions
+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)
}
+10 -2
View File
@@ -1058,14 +1058,16 @@ parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id
}
// A bare `{ ... }` introduces a nested scope. Locals declared inside are not
// visible after it, and any `defer`s inside it run at the closing brace.
parse_block_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
// visible after it, and any `defer`s inside it run at the closing brace. A labeled
// `blk: { ... }` can be exited early with `break :blk`.
parse_block_statement :: proc(parser: ^Parser, label := symbol.INVALID) -> ast.Stmt_Id {
start := current(parser).span // the '{'
body := parse_block(parser)
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Block,
span=start,
label=label,
body=body,
expr=ast.INVALID_EXPR,
update=ast.INVALID_STMT,
@@ -1159,6 +1161,12 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Left_Brace {
return parse_block_statement(parser)
}
// `blk: { ... }` is a labeled block statement (exitable via `break :blk`). At
// statement start, `Identifier ':'` (a single colon, not `::`) opens one.
if current(parser).kind == .Identifier && peek(parser).kind == .Colon {
label := parse_optional_loop_label(parser)
return parse_block_statement(parser, label)
}
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
start_cursor := parser.cursor
+12 -3
View File
@@ -2133,8 +2133,9 @@ yield_compiles_and_runs :: proc(t: ^testing.T) {
state := run_executable(output)
// Value blocks, value if-statements (incl. `return` branches and unwrap-`if`),
// `orelse`, value loops, labeled value blocks (`blk: { … yield :blk v }`, incl.
// defer-capture and `{T,none}` optional), and outer-loop control (`yield :outer v`
// and `break :outer` from an inner loop) together produce 42.
// defer-capture, `{T,none}` optional, and `none` before a concrete yield that uses a
// block local), outer-loop control (`yield :outer v` / `break :outer`), and labeled
// block statements exited via `break :blk` together produce 42.
testing.expect_value(t, state.exit_code, 42)
}
@@ -2182,7 +2183,8 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
// Value if/loop/block misuse: an `if` value without an `else`; a branch that neither
// yields nor exits on every path (20.6); a value loop whose body lacks a trailing
// fall-through `yield`; a `yield :label` with no matching value loop; a labeled value
// block that does not yield on every path, and a `break :label` naming no loop (20.7).
// block that does not yield on every path; a `break :label` naming no loop; and a
// `continue :label` targeting a block (not a loop).
text := `main :: func() i32 {
noelse :: if (true) {
yield 1
@@ -2205,6 +2207,9 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
for 0..10 |m| {
break :nope
}
scope: {
continue :scope
}
return noelse + badbranch + noloopyield + noblockyield
}
`
@@ -2226,6 +2231,7 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
stray_label := false
block_no_yield := false
bad_break_label := false
continue_block := false
for diagnostic in diagnostics.items {
no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'")
branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value branch must end with 'yield' or exit on every path")
@@ -2233,6 +2239,8 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop or block is labeled 'stray'")
block_no_yield = block_no_yield || strings.contains(diagnostic.message, "a labeled value block must 'yield' on every path")
bad_break_label = bad_break_label || strings.contains(diagnostic.message, "no enclosing loop is labeled 'nope'")
// `continue :scope` targets a labeled block, which is not a loop.
continue_block = continue_block || strings.contains(diagnostic.message, "no enclosing loop is labeled 'scope'")
}
testing.expect(t, no_else)
testing.expect(t, branch_no_yield)
@@ -2240,6 +2248,7 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
testing.expect(t, stray_label)
testing.expect(t, block_no_yield)
testing.expect(t, bad_break_label)
testing.expect(t, continue_block)
}
@(test)
+44
View File
@@ -240,6 +240,45 @@ break_outer :: func() i32 {
return count # 5
}
# A labeled block *statement* (not a value source): `break :blk` exits it early.
stmt_block :: func(early i32) i32 {
x i32 = 0
blk: {
x = 1
if (early == 1) break :blk
x = 2
}
return x # early == 1 -> 1, else 2
}
# `break :search` escapes a nested loop and the block in one jump; the block's
# defer still runs on the way out.
stmt_block_escape :: func() i32 {
hits i32 = 0
search: {
defer hits += 1000
for 0..10 |i| {
hits += 1
if (i == 3) break :search
}
hits += 100 # skipped by break :search
}
return hits # 4 + 1000 (defer) = 1004
}
# Item B: a `none` yielded before a concrete `yield :blk` that references a block local.
lblock_local :: func() i32 {
r :: blk: {
val :: 9
if (false) yield :blk none
yield :blk val
}
if r |v| {
return v
}
return -1
}
main :: func() i32 {
if (basic() != 42) return 101
if (typed() != 100) return 102
@@ -276,5 +315,10 @@ main :: func() i32 {
if (yield_outer(99) != -1) return 129
if (break_outer() != 5) return 130
if (stmt_block(1) != 1) return 131
if (stmt_block(0) != 2) return 132
if (stmt_block_escape() != 1004) return 133
if (lblock_local() != 9) return 134
return 42
}