block labels
This commit is contained in:
+136
-32
@@ -67,6 +67,9 @@ Yield_Target :: struct {
|
||||
// True when the loop also yields `none` (a `{T, none}` set → `?T`); set from a
|
||||
// pure-AST scan, used to pick the slot's element type on the first concrete yield.
|
||||
result_optional: bool,
|
||||
// `len(defers)` when this target's body began; a `yield :label` flushes defers down
|
||||
// to here before breaking, so an outer-loop / value-block yield runs inner defers too.
|
||||
defer_floor: int,
|
||||
}
|
||||
|
||||
Build_Ctx :: struct {
|
||||
@@ -92,6 +95,9 @@ 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.
|
||||
loop_labels: ^[dynamic]symbol.Id,
|
||||
defer_depth: int,
|
||||
loop_floor: int,
|
||||
}
|
||||
@@ -4055,7 +4061,7 @@ build_block :: proc(
|
||||
if typed {
|
||||
expected = type_from_syntax(statement.type)
|
||||
}
|
||||
value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span)
|
||||
value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span, statement.label)
|
||||
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
|
||||
id := source.addf(
|
||||
checker.diagnostics, statement.span,
|
||||
@@ -4266,7 +4272,7 @@ build_block :: proc(
|
||||
} else if statement.expr == ast.INVALID_EXPR {
|
||||
// `target = { ... yield v }`: build the value block against the
|
||||
// target's type (build_value_block coerces internally).
|
||||
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span)
|
||||
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label)
|
||||
} else {
|
||||
value = build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
@@ -4286,7 +4292,7 @@ build_block :: proc(
|
||||
if statement.name == checker.sink_symbol {
|
||||
value: hir.Expr_Id
|
||||
if statement.expr == ast.INVALID_EXPR {
|
||||
value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span)
|
||||
value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span, statement.label)
|
||||
} else {
|
||||
value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
|
||||
}
|
||||
@@ -4330,7 +4336,7 @@ build_block :: proc(
|
||||
}
|
||||
value: hir.Expr_Id
|
||||
if statement.expr == ast.INVALID_EXPR {
|
||||
value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span)
|
||||
value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span, statement.label)
|
||||
} else {
|
||||
value = build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
@@ -4596,7 +4602,9 @@ build_block :: proc(
|
||||
ctx.problematic^ = true
|
||||
}
|
||||
append(ctx.loop_defer_starts, len(ctx.defers^))
|
||||
append(ctx.loop_labels, statement.label)
|
||||
loop_body := build_block(ctx, statement.body)
|
||||
pop(ctx.loop_labels)
|
||||
pop(ctx.loop_defer_starts)
|
||||
update := hir.INVALID_STMT
|
||||
if statement.update != ast.INVALID_STMT {
|
||||
@@ -4611,6 +4619,7 @@ build_block :: proc(
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind=.While,
|
||||
span=statement.span,
|
||||
label=statement.label,
|
||||
expr=condition,
|
||||
then_body=loop_body,
|
||||
update=update,
|
||||
@@ -4690,7 +4699,9 @@ build_block :: proc(
|
||||
}
|
||||
}
|
||||
append(ctx.loop_defer_starts, len(ctx.defers^))
|
||||
append(ctx.loop_labels, statement.label)
|
||||
loop_body := build_block(ctx, statement.body, capture_start)
|
||||
pop(ctx.loop_labels)
|
||||
pop(ctx.loop_defer_starts)
|
||||
resize(ctx.locals, capture_start)
|
||||
|
||||
@@ -4699,6 +4710,7 @@ build_block :: proc(
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind=.For,
|
||||
span=statement.span,
|
||||
label=statement.label,
|
||||
local=item_local,
|
||||
index_local=index_local,
|
||||
expr=iterable,
|
||||
@@ -4723,11 +4735,29 @@ build_block :: proc(
|
||||
ctx.problematic^ = true
|
||||
}
|
||||
case .Break, .Continue:
|
||||
// Inside a `defer`, `loop_floor` hides the enclosing loops so only loops
|
||||
// opened within the defer count.
|
||||
if len(ctx.loop_defer_starts^) <= ctx.loop_floor {
|
||||
// `break :L` / `continue :L` targets the innermost enclosing loop labeled `L`;
|
||||
// an unlabeled one targets the innermost loop. Inside a `defer`, `loop_floor`
|
||||
// hides the loops opened outside the defer.
|
||||
target_index := -1
|
||||
if symbol.is_valid(statement.label) {
|
||||
for i := len(ctx.loop_labels^) - 1; i >= ctx.loop_floor; i -= 1 {
|
||||
if ctx.loop_labels^[i] == statement.label {
|
||||
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"
|
||||
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
|
||||
id: source.Diagnostic_Id
|
||||
if symbol.is_valid(statement.label) {
|
||||
id = source.addf(checker.diagnostics, statement.span,
|
||||
"no enclosing loop is labeled '%s'", symbol_text(checker, statement.label))
|
||||
} else {
|
||||
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,
|
||||
@@ -4736,13 +4766,13 @@ build_block :: proc(
|
||||
ctx.problematic^ = true
|
||||
continue
|
||||
}
|
||||
// Exit the loop body and any blocks between here and it: run their
|
||||
// deferred statements down to and including the innermost loop body.
|
||||
flush_defers(ctx, &body, ctx.loop_defer_starts^[len(ctx.loop_defer_starts^) - 1])
|
||||
// Exit the loop body and any blocks between here and the target loop: run their
|
||||
// deferred statements down to and including the target loop body.
|
||||
flush_defers(ctx, &body, ctx.loop_defer_starts^[target_index])
|
||||
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,
|
||||
span = statement.span, label = statement.label, expr = hir.INVALID_EXPR,
|
||||
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Block:
|
||||
@@ -4767,20 +4797,7 @@ build_block :: proc(
|
||||
}
|
||||
if target_index < 0 {
|
||||
id := source.addf(checker.diagnostics, statement.span,
|
||||
"no enclosing value loop is labeled '%s'", symbol_text(checker, statement.label))
|
||||
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
|
||||
}
|
||||
// ponytail: a `yield :blk` lowers to milestone-18 `break`, which targets the
|
||||
// innermost loop only, so the label must name the innermost value loop.
|
||||
if target_index != len(ctx.yield_targets^) - 1 {
|
||||
id := source.addf(checker.diagnostics, statement.span,
|
||||
"'yield :%s' must target the innermost loop", symbol_text(checker, statement.label))
|
||||
"no enclosing value loop or block is labeled '%s'", symbol_text(checker, statement.label))
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
|
||||
@@ -4805,11 +4822,11 @@ build_block :: proc(
|
||||
}
|
||||
// slot = value (the slot is un-nameable, so no defer can mutate it; no spill).
|
||||
emit_slot_assign(checker, &body, target.slot, yielded, statement.span)
|
||||
// Exit the loop: flush defers down to the loop body, then break.
|
||||
flush_defers(ctx, &body, ctx.loop_defer_starts^[len(ctx.loop_defer_starts^) - 1])
|
||||
// Exit the target: flush defers down to its body, then a labeled break.
|
||||
flush_defers(ctx, &body, target.defer_floor)
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Break, span = statement.span, expr = hir.INVALID_EXPR,
|
||||
kind = .Break, span = statement.span, label = target.label, expr = hir.INVALID_EXPR,
|
||||
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
continue
|
||||
@@ -4818,7 +4835,7 @@ build_block :: proc(
|
||||
// is peeled by the value builders (value block / if branch / loop fall-through).
|
||||
id := source.add(
|
||||
checker.diagnostics, statement.span,
|
||||
"'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop",
|
||||
"'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop or block",
|
||||
)
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
@@ -4960,16 +4977,21 @@ build_value_block :: proc(
|
||||
}
|
||||
|
||||
// build_value_source feeds a declaration/assignment RHS into the right value builder:
|
||||
// a `{ ... }` block, an `if` whose branches yield, or a `for`/`while` whose iterations
|
||||
// yield. All three return the produced value and its type for the enclosing binding.
|
||||
// a labeled `blk: { ... }` block, a plain `{ ... }` block, an `if` whose branches yield,
|
||||
// or a `for`/`while` whose iterations yield. All return the produced value and its type
|
||||
// for the enclosing binding. `label` is the labeled-block label (INVALID otherwise).
|
||||
build_value_source :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
body: ^[dynamic]hir.Stmt_Id,
|
||||
body_stmts: []ast.Stmt_Id,
|
||||
expected: types.Type,
|
||||
span: source.Span,
|
||||
label := symbol.INVALID,
|
||||
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||
checker := ctx.checker
|
||||
if symbol.is_valid(label) {
|
||||
return build_value_labeled_block(ctx, body, body_stmts, label, expected, span)
|
||||
}
|
||||
if len(body_stmts) == 1 {
|
||||
#partial switch checker.ast_module.statements[body_stmts[0]].kind {
|
||||
case .If:
|
||||
@@ -5342,6 +5364,83 @@ value_loop_element_type :: proc(ctx: ^Build_Ctx, loop_stmt: ast.Stmt) -> types.T
|
||||
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`
|
||||
// that emits the body and the exit label the labeled breaks branch to. No iteration / no
|
||||
// fall-through (unlike a value loop). The type is the annotation when typed, else the first
|
||||
// concrete `yield :blk`'s type (optional when any yield is `none`).
|
||||
build_value_labeled_block :: proc(
|
||||
ctx: ^Build_Ctx,
|
||||
body: ^[dynamic]hir.Stmt_Id,
|
||||
block_stmts: []ast.Stmt_Id,
|
||||
label: symbol.Id,
|
||||
expected: types.Type,
|
||||
span: source.Span,
|
||||
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||
checker := ctx.checker
|
||||
result_optional := loop_yields_none(checker, block_stmts)
|
||||
slot := hir.INVALID_LOCAL
|
||||
slot_type := types.INVALID
|
||||
if is_runtime_type(checker, expected) {
|
||||
slot_type = expected
|
||||
slot = new_value_slot(ctx, slot_type)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
append(ctx.yield_targets, Yield_Target{
|
||||
label = label, slot = slot, slot_type = slot_type, result_optional = result_optional,
|
||||
defer_floor = len(ctx.defers^),
|
||||
})
|
||||
built := build_block(ctx, block_stmts)
|
||||
target := pop(ctx.yield_targets)
|
||||
|
||||
if target.slot == hir.INVALID_LOCAL {
|
||||
for s in built {
|
||||
append(body, s)
|
||||
}
|
||||
delete(built, checker.allocator)
|
||||
id := source.add(checker.diagnostics, span,
|
||||
"could not determine the value block's yield type; annotate the binding")
|
||||
ctx.problematic^ = true
|
||||
return invalid_hir_expr(checker, span, id), types.INVALID
|
||||
}
|
||||
// Every path must `yield :blk` (or return/break out); otherwise a path falls through
|
||||
// to the slot read with a poison value.
|
||||
if !all_paths_exit(&checker.module, built) {
|
||||
for s in built {
|
||||
append(body, s)
|
||||
}
|
||||
delete(built, checker.allocator)
|
||||
id := source.add(checker.diagnostics, span, "a labeled value block must 'yield' on every path")
|
||||
ctx.problematic^ = true
|
||||
return invalid_hir_expr(checker, span, id), types.INVALID
|
||||
}
|
||||
|
||||
append(body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Declaration, span = span, local = target.slot, expr = hir.INVALID_EXPR,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append(body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Block, span = span, label = label, then_body = built,
|
||||
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
value = slot_read(checker, target.slot, target.slot_type, span)
|
||||
return value, target.slot_type
|
||||
}
|
||||
|
||||
// build_value_loop turns a labeled `for/while ... blk: { … }` whose body ends in a
|
||||
// fall-through `yield` (and may early-exit via `yield :blk x`) into a result slot:
|
||||
// the fall-through value initializes the slot before the loop, each `yield :blk x`
|
||||
@@ -5395,6 +5494,7 @@ build_value_loop :: proc(
|
||||
}
|
||||
append(ctx.yield_targets, Yield_Target{
|
||||
label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional,
|
||||
defer_floor = len(ctx.defers^),
|
||||
})
|
||||
|
||||
// Build the loop with the fall-through peeled off, reusing the normal For/While arm.
|
||||
@@ -5620,6 +5720,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
defers.allocator = checker.allocator
|
||||
loop_defer_starts: [dynamic]int
|
||||
loop_defer_starts.allocator = checker.allocator
|
||||
loop_labels: [dynamic]symbol.Id
|
||||
loop_labels.allocator = checker.allocator
|
||||
yield_targets: [dynamic]Yield_Target
|
||||
yield_targets.allocator = checker.allocator
|
||||
ctx := Build_Ctx{
|
||||
@@ -5635,6 +5737,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
problematic = &problematic,
|
||||
defers = &defers,
|
||||
loop_defer_starts = &loop_defer_starts,
|
||||
loop_labels = &loop_labels,
|
||||
yield_targets = &yield_targets,
|
||||
}
|
||||
block := build_block(&ctx, function.body)
|
||||
@@ -5685,6 +5788,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
}
|
||||
delete(defers)
|
||||
delete(loop_defer_starts)
|
||||
delete(loop_labels)
|
||||
delete(yield_targets)
|
||||
delete(locals)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user