diff --git a/TODO.md b/TODO.md index 8a3c5d4..2612440 100644 --- a/TODO.md +++ b/TODO.md @@ -360,7 +360,7 @@ fall out of these naturally - follow-ups: a branch that early-`return`s instead of yielding, unwrap-`if` as a value source, and `none`-before-concrete typing in untyped loops are done in 20.6; labeled value - blocks and `yield`/`break` to an outer loop are 20.7 (need labels in the IR) + blocks and `yield`/`break` to an outer loop are done in 20.7 20.6 value if/loop follow-ups (implemented; checker-only) - a value-if branch may end in `yield` **or** exit on every path (`return`/`break`/ @@ -377,16 +377,23 @@ `none` yielded before any concrete value still resolves the result to `?T` - still checker-only; no HIR/lowering change -20.7 labeled value blocks + `yield`/`break` to an outer loop (introduces labels in the IR) +20.7 labels — value blocks + yield/break to an outer loop (implemented; first lowering change) - `x :: blk: { …; yield :blk v }` — a labeled value *block* (the disambiguated form of "an if/loop at the end of a block"; an unlabeled trailing if/loop stays ambiguous and is not a - value source). `yield :blk v` exits the block with a value - - `yield :outer v` / labeled `break` to a non-innermost loop - - both need exit targets to carry a label: add `label` to HIR `.While`/`.For`/`.Break` and a - labeled `.Block`; generalize the lowering's `Loop_Ctx`/`State.loops` into an exit-target - stack keyed by label (plain `break`/`continue` stay innermost-only; a labeled `.Break` - searches by label; a labeled block pushes a non-loop target + an exit label after its body). - First lowering change in the `yield` line + value source). `yield :blk v` exits the block with a value; every path must yield. Carries + the same `{T, none}` → `?T` typing, defer-capture, and reassignment forms as value loops + - `yield :outer v` to an enclosing (non-innermost) value loop/block, plus plain `break :L` / + `continue :L` to an enclosing labeled loop + - a label now names a first-class exit target: `label` added to the HIR `Stmt` (on + `.While`/`.For`/`.Break`/`.Continue`) and a new HIR `.Block` kind (lowers to its body + an + exit label). The lowering's `Loop_Ctx`/`State.loops` became a label-keyed exit-target stack + (`is_loop` distinguishes loops from value blocks; plain `break`/`continue` take the innermost + 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) 21. unions and tagged unions diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index ff2ccb8..a0b170d 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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) } diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 43cbbf9..67d9f17 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -156,6 +156,9 @@ Stmt_Kind :: enum u8 { For, Break, Continue, + // A labeled value block (`blk: { … yield :blk v }`): lowers to its `then_body` + // followed by an exit label, so a `yield :blk` (a labeled break) lands after it. + Block, } Assignment_Op :: enum u8 { @@ -172,6 +175,9 @@ Stmt :: struct { span: source.Span, local: Local_Id, index_local: Local_Id, + // `While`/`For`/`Block` may carry a label; a labeled `Break`/`Continue` targets the + // matching enclosing loop or value block. `INVALID` when absent. + label: symbol.Id, target: Expr_Id, expr: Expr_Id, iterator_type: types.Type, diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index ea2c71f..de44f5c 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -3,6 +3,7 @@ package lower import "../hir" import "../ir" import "../source" +import "../symbol" import "../target" import "../types" import "core:fmt" @@ -22,11 +23,15 @@ State :: struct { allocator: mem.Allocator, } -// `break` branches to `exit_lbl`; `continue` branches to `continue_lbl` (the -// loop's update/latch, which runs the update clause then re-tests the condition). +// An enclosing exit target. `break` branches to `exit_lbl`; `continue` branches to +// `continue_lbl` (the loop's update/latch, which runs the update clause then re-tests +// the condition). A labeled `break`/`continue`/`yield` matches `label`; a value block is +// `is_loop = false` (it has no `continue` and is skipped by plain `break`/`continue`). Loop_Ctx :: struct { + label: symbol.Id, exit_lbl: i64, continue_lbl: i64, + is_loop: bool, } fresh_label :: proc(state: ^State) -> i64 { @@ -752,10 +757,25 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { }) } case .Break, .Continue: - // The checker guarantees these only appear inside a loop, so the - // stack is non-empty; guard defensively regardless. - if len(state.loops) > 0 { - target := state.loops[len(state.loops)-1] + // Find the target: a labeled `break`/`continue` matches the innermost target + // (loop or value block) with that label; an unlabeled one takes the innermost + // loop (`continue` and unlabeled targets skip non-loop block targets). The + // checker guarantees a match exists; guard defensively regardless. + target_index := -1 + for i := len(state.loops) - 1; i >= 0; i -= 1 { + ctx := state.loops[i] + if symbol.is_valid(statement.label) { + if ctx.label == statement.label && (ctx.is_loop || statement.kind == .Break) { + target_index = i + break + } + } else if ctx.is_loop { + target_index = i + break + } + } + if target_index >= 0 { + target := state.loops[target_index] label := target.exit_lbl if statement.kind == .Break else target.continue_lbl append_instruction(state, ir.Instruction{ op=.Br, span=statement.span, type=types.VOID, integer=label, @@ -763,6 +783,27 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { diagnostic=source.INVALID_DIAGNOSTIC, }) } + case .Block: + // A labeled value block: lower its body, then emit the exit label that its + // `yield :blk` (a labeled break) branches to. Not a loop, so plain + // `break`/`continue` skip it (is_loop=false). + exit_lbl := fresh_label(state) + append(&state.loops, Loop_Ctx{label=statement.label, exit_lbl=exit_lbl, continue_lbl=exit_lbl, is_loop=false}) + lower_statements(state, statement.then_body) + pop(&state.loops) + // Explicit fall-through to the exit label so the preceding block is terminated + // (dead code after a terminator gets a fresh recovery block in the emitter), + // mirroring the `br` a `while`/`for` emits before its labels. + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=exit_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=exit_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Expression, .Sink: _ = lower_expr(state, statement.expr) case .Trap: @@ -940,7 +981,7 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) - append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl}) + append(&state.loops, Loop_Ctx{label=statement.label, exit_lbl=exit_lbl, continue_lbl=update_lbl, is_loop=true}) lower_statements(state, statement.then_body) pop(&state.loops) append_instruction(state, ir.Instruction{ @@ -1072,7 +1113,7 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { // e.g. `for 0..=255 |b: u8| { ... continue }` exits cleanly instead of // overflowing the increment on the final element. continue_lbl := fresh_label(state) - append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=continue_lbl}) + append(&state.loops, Loop_Ctx{label=statement.label, exit_lbl=exit_lbl, continue_lbl=continue_lbl, is_loop=true}) lower_statements(state, statement.then_body) pop(&state.loops) append_instruction(state, ir.Instruction{ @@ -1257,7 +1298,7 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { target=ir.INVALID_REF, a=capture_slot, b=captured, diagnostic=source.INVALID_DIAGNOSTIC, }) - append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl}) + append(&state.loops, Loop_Ctx{label=statement.label, exit_lbl=exit_lbl, continue_lbl=update_lbl, is_loop=true}) lower_statements(state, statement.then_body) pop(&state.loops) append_instruction(state, ir.Instruction{ diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 210f0be..dee953d 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1036,10 +1036,20 @@ parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id { // checker rejects them outside a loop. parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id { marker := advance(parser) // consume 'break' / 'continue' + // `break :outer` / `continue :outer` targets the enclosing loop labeled `outer`. + label := symbol.INVALID + if _, ok := allow(parser, .Colon); ok { + if name, name_ok := allow(parser, .Identifier); name_ok { + label = name.symbol + } else { + source.add(parser.diagnostics, current(parser).span, "expected a loop label after ':'") + } + } id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ kind=kind, span=marker.span, + label=label, expr=ast.INVALID_EXPR, update=ast.INVALID_STMT, diagnostic=source.INVALID_DIAGNOSTIC, @@ -1169,6 +1179,26 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { kind = .Declaration immutable = operator.kind == .Colon_Colon } + // A labeled value block (`x :: blk: { … yield :blk v }`): the label lets a + // `yield :blk` exit the block past a nested `if`. Block-init body + label. + if current(parser).kind == .Identifier && peek(parser).kind == .Colon { + label := parse_optional_loop_label(parser) + body := parse_block(parser) + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=kind, + span=span_from(name.span, previous(parser).span), + name=name.symbol, + type=type_syntax, + immutable=immutable, + label=label, + target=ast.INVALID_EXPR, + expr=ast.INVALID_EXPR, + body=body, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } // A `{` on the right is a value block: parse its statements now; the // checker turns its final `yield` into the declared/assigned value. if current(parser).kind == .Left_Brace { @@ -1227,6 +1257,22 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { expr := parse_expression(parser) if _, ok := allow(parser, .Equal); ok { skip_newlines(parser) + // A labeled value block assigned to a complex target (`a[i] = blk: { … }`). + if current(parser).kind == .Identifier && peek(parser).kind == .Colon { + label := parse_optional_loop_label(parser) + body := parse_block(parser) + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Assignment, + span=span_from(parser.module.exprs[expr].span, previous(parser).span), + target=expr, + label=label, + expr=ast.INVALID_EXPR, + body=body, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } // A value block assigned to a complex target (`a[i] = { ... }`, `p.f = { ... }`). if current(parser).kind == .Left_Brace { brace := current(parser) diff --git a/compiler_tests.odin b/compiler_tests.odin index 55e393c..a80d9c3 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2131,11 +2131,10 @@ yield_compiles_and_runs :: proc(t: ^testing.T) { status := compiler_core.compile_package("examples/programs/yield", output) testing.expect_value(t, status, 0) state := run_executable(output) - // Value blocks (untyped/typed, defer-spill, reassignment), value if-statements - // (untyped/typed/reassign/else-if/defer, a branch that `return`s instead of yielding, - // and an unwrap-`if` as a value source), `orelse`, and value loops (a labeled `for` - // search yielding `?usize` on the found/not-found paths, a labeled `while`, and an - // untyped loop that yields `none` before any concrete value) together produce 42. + // 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. testing.expect_value(t, state.exit_code, 42) } @@ -2180,9 +2179,10 @@ yield_misuse_is_diagnosed :: proc(t: ^testing.T) { @(test) yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { - // Value if/loop misuse: an `if` value without an `else`; a branch that neither - // yields nor exits on every path (milestone 20.6); a value loop whose body lacks a - // trailing fall-through `yield`; and a `yield :label` with no matching value loop. + // 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). text := `main :: func() i32 { noelse :: if (true) { yield 1 @@ -2198,7 +2198,14 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { for 0..10 |j| stray: { yield :stray j } - return noelse + badbranch + noloopyield + noblockyield :: blk: { + if (true) yield :blk 1 + k2 :: 5 + } + for 0..10 |m| { + break :nope + } + return noelse + badbranch + noloopyield + noblockyield } ` source_file := source.Source{path="test.bro", text=text} @@ -2217,16 +2224,22 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { branch_no_yield := false loop_no_yield := false stray_label := false + block_no_yield := false + bad_break_label := 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") loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a 'yield'") - stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop is labeled 'stray'") + 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'") } testing.expect(t, no_else) testing.expect(t, branch_no_yield) testing.expect(t, loop_no_yield) testing.expect(t, stray_label) + testing.expect(t, block_no_yield) + testing.expect(t, bad_break_label) } @(test) diff --git a/examples/programs/yield/main.bro b/examples/programs/yield/main.bro index 6b1e250..9e90199 100644 --- a/examples/programs/yield/main.bro +++ b/examples/programs/yield/main.bro @@ -174,6 +174,72 @@ loop_none_first :: func() i32 { return 2 } +# --- labels: value blocks + outer-loop yield/break (milestone 20.7) ----------- + +# A labeled value block: `yield :blk` exits the block (past a nested `if`) with a +# value. Every path must yield. +lblock :: func(sel i32) i32 { + r :: blk: { + base :: 10 + if (sel == 0) { + yield :blk base + } else { + yield :blk base * 2 + } + } + return r +} + +# An early `yield :blk` skips the rest of the block; the value is captured before +# the block's defer runs. +lblock_defer :: func() i32 { + r :: blk: { + n i32 = 5 + defer n = 999 + if (true) yield :blk n + yield :blk 0 + } + return r # 5, not 999 +} + +# A labeled block whose `{T, none}` yields resolve the result to an optional. +lblock_optional :: func(present i32) i32 { + r :: blk: { + if (present == 0) yield :blk none + yield :blk 8 + } + if r |v| { + return v + } + return -1 +} + +# `yield :outer v` exits an OUTER value loop from inside an inner loop. +yield_outer :: func(target i32) i32 { + found :: for 0..3 |row| outer: { + for 0..3 |col| { + if (row * 3 + col == target) yield :outer (row * 10 + col) + } + yield none + } + if found |v| { + return v + } + return -1 +} + +# Plain `break :outer` exits an outer loop from an inner loop. +break_outer :: func() i32 { + count i32 = 0 + for 0..3 |a| outer: { + for 0..3 |b| { + count += 1 + if (a == 1 and b == 1) break :outer + } + } + return count # 5 +} + main :: func() i32 { if (basic() != 42) return 101 if (typed() != 100) return 102 @@ -201,5 +267,14 @@ main :: func() i32 { if (orelse_value(none) != 7) return 121 if (loop_none_first() != 0) return 122 + if (lblock(0) != 10) return 123 + if (lblock(1) != 20) return 124 + if (lblock_defer() != 5) return 125 + if (lblock_optional(0) != -1) return 126 + if (lblock_optional(1) != 8) return 127 + if (yield_outer(4) != 11) return 128 + if (yield_outer(99) != -1) return 129 + if (break_outer() != 5) return 130 + return 42 }