labeled block statements
This commit is contained in:
+105
-13
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user