From 61293a23e7e446f7e084d4aaf4263548ffab3b6f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 27 Jun 2026 10:57:35 +0200 Subject: [PATCH] expand yield to if-statements and loops --- TODO.md | 37 ++- compiler/ast/ast.odin | 4 + compiler/checker/checker.odin | 397 ++++++++++++++++++++++++++++++- compiler/parser/parser.odin | 80 +++++++ compiler_tests.odin | 57 ++++- examples/programs/yield/main.bro | 111 ++++++++- 6 files changed, 669 insertions(+), 17 deletions(-) diff --git a/TODO.md b/TODO.md index 9386a38..a693d6d 100644 --- a/TODO.md +++ b/TODO.md @@ -330,7 +330,39 @@ (`blk: { yield :blk v }`), implicit trailing-expression yield, and yield in match arms / `catch` handlers (milestones 21–22) -20.5 `yield` from if-statements and loops (see below) +20.5 `yield` from if-statements and loops (implemented; see below) + - an `if`/`for`/`while` on the right of a declaration or assignment is now a *value + source*, governed by the rule **if one path yields, all paths must yield** (no + optionals-as-a-crutch, so the value is always present and never needs unwrapping): + - value-if: `result :: if a { yield 1 } else if b { yield 2 } else { yield 3 }` — a + mandatory `else`, every branch ends in `yield`, all branches share a type (the first + branch fixes it when untyped; later branches coerce). Typed `T =` and reassignment + `target = if …` are supported too + - value-loop: a labeled body `for/while … blk: { … }` whose early exits are + `yield :blk x` and whose body ends in an unlabeled fall-through `yield` (the value + when the loop completes). The `{T, none}` yields resolve the result to `?T` + (a pure-AST `none`-scan picks optionality; the first concrete yield fixes the element + type). E.g. `active_ent_idx :: for 0..10 |i| blk: { if (cond) yield :blk i; yield none }` + resolves to `?usize` + - new `blk:` / `yield :blk` label surface adds one `label` field to the AST `Stmt`; no new + token (`blk:` is `Identifier Colon`, `:blk` is `Colon Identifier`). The parser carries a + value `if`/`for`/`while` as a one-element block-init `body` (the same `expr`-invalid + signal a value block uses) + - **no HIR/lowering change** (like 18/19/20): a value-if/loop desugars in the checker to a + mutable result *slot* (a poison-/fall-through-initialized local) that branches/iterations + assign and that is read after the construct — the existing alloca-backed local flow. A + `yield :blk x` desugars to `slot = x; break`, reusing the milestone-18 `Break` lowering. + Each value-if branch is a `build_value_block` call; the value-loop reuses the ordinary + `.For`/`.While` build via a peeled-body copy. HIR never holds a `.Yield` + - errors: an `if` value without `else`; a branch/value-block not ending in `yield`; a value + loop body without a trailing fall-through `yield`; a `yield :blk` with no matching value + loop. The TODO "BAD" loops (unlabeled yield from inside an `if`, an unbound labeled loop) + fall out of these naturally + - deferred to a later pass (`// ponytail:`): a value-if/loop nested as the *trailing* + statement of a value block (first pass is the direct `x :: if …`/`for …` form); a branch + that early-`return`s instead of yielding; unwrap-`if` (`if v |x| { yield … }`) as a value + source; `yield none` before any concrete yield in an untyped loop (annotate instead); + `yield :blk` to an outer (non-innermost) loop 21. unions and tagged unions @@ -608,6 +640,7 @@ Yielding is also possible from loops with the same constraint. # get active entity active_ent_idx :: for 0..10 |i| blk: { if is_active(some_entity, i) yield :blk i + yield none # fall-through: no active ent was found (this should imply a return type matching both the index value and `none`, meaning it should resolve to an optional in this case) # note that in this case, we have to use the `blk` label to yield from the correct scope. # otherwise, the yield should return directly from the if-statement's scope (which would be incorrect in this case). @@ -616,11 +649,13 @@ active_ent_idx :: for 0..10 |i| blk: { # BAD: yield returned from if-statement, but no name binds it: should miscompile similar to unused return values from functions. active_ent_idx :: for 0..10 |i| { if is_active(some_entity, i) yield i # bad + yield none } # BAD: likewise for loops for 0..10 |i| blk: { # bad, no name binds returned value if is_active(some_entity, i) yield :blk i + yield none } ``` diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index d04800c..7a88574 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -149,6 +149,10 @@ Stmt :: struct { span: source.Span, name: symbol.Id, index_name: symbol.Id, + // `For`/`While` loop bodies may carry a label (`blk: { ... }`); a `Yield` + // may target one (`yield :blk x`), letting a yield reach past an enclosing + // `if` to exit the labeled loop. `INVALID` when absent. + label: symbol.Id, type: Type_Syntax, immutable: bool, pointer_capture: bool, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index e359f39..e2010a5 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -57,6 +57,18 @@ Build_Local :: struct { // nested control-flow blocks (if/else) can be built recursively. `locals` is a // scope stack: each block records its entry length and truncates back to it on // exit, while `hir_locals` keeps every allocated slot for the function. +// A labeled value-loop currently being built. A `yield :label x` inside the loop +// body assigns `x` to the loop's result `slot` (typed `slot_type`) and `break`s. +// Pushed by `build_value_loop` while its body is built; innermost is last. +Yield_Target :: struct { + label: symbol.Id, + slot: hir.Local_Id, + slot_type: types.Type, + // 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, +} + Build_Ctx :: struct { checker: ^Checker, pkg: ast.Package_Id, @@ -68,6 +80,8 @@ Build_Ctx :: struct { global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, problematic: ^bool, + // Stack of labeled value-loops being built (innermost last); see Yield_Target. + yield_targets: ^[dynamic]Yield_Target, // `defer` lowering. Deferred statements are built once at the `defer` site and // their hir stmt ids stored here as a flat stack across scopes (one entry per // deferred statement); they are replayed (appended) at each scope exit in LIFO @@ -4041,7 +4055,7 @@ build_block :: proc( if typed { expected = type_from_syntax(statement.type) } - value, value_type := build_value_block(ctx, &body, statement.body, expected, statement.span) + value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span) if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { id := source.addf( checker.diagnostics, statement.span, @@ -4252,7 +4266,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_block(ctx, &body, statement.body, target_type, statement.span) + value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, @@ -4272,7 +4286,7 @@ build_block :: proc( if statement.name == checker.sink_symbol { value: hir.Expr_Id if statement.expr == ast.INVALID_EXPR { - value, _ = build_value_block(ctx, &body, statement.body, types.INVALID, statement.span) + value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span) } else { value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) } @@ -4316,7 +4330,7 @@ build_block :: proc( } value: hir.Expr_Id if statement.expr == ast.INVALID_EXPR { - value, _ = build_value_block(ctx, &body, statement.body, local.type, statement.span) + value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, @@ -4740,13 +4754,71 @@ build_block :: proc( } delete(block, checker.allocator) case .Yield: - // A legitimate yield is peeled off by build_value_block as the value - // block's final statement; reaching it here means it is misplaced - // (nested in an if/loop/inner block, or in a non-value block). - // ponytail: yield from if/loops/labeled blocks is a later milestone. + // A labeled `yield :blk x` exits the value-loop labeled `blk`: assign the + // result slot, then `break` (which flushes defers down to the loop body and + // branches to its exit). HIR holds no `.Yield` — it becomes Assignment + Break. + if symbol.is_valid(statement.label) { + target_index := -1 + for i := len(ctx.yield_targets^) - 1; i >= 0; i -= 1 { + if ctx.yield_targets^[i].label == statement.label { + target_index = i + break + } + } + 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)) + 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 + } + target := &ctx.yield_targets^[target_index] + yielded := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) + yielded = resolve_loop_slot(ctx, target, yielded, checker.module.exprs[yielded].type if yielded != hir.INVALID_EXPR else types.INVALID, statement.span) + if target.slot == hir.INVALID_LOCAL || yielded == hir.INVALID_EXPR { + id := source.add(checker.diagnostics, statement.span, + "could not determine the value loop's yield type; annotate the binding or yield a concrete value first") + 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 + } + // 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]) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Break, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, + }) + continue + } + // An unlabeled yield reaching here is misplaced: a legitimate trailing yield + // 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", + "'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop", ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ @@ -4887,6 +4959,309 @@ build_value_block :: proc( return value, value_type } +// 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. +build_value_source :: proc( + ctx: ^Build_Ctx, + body: ^[dynamic]hir.Stmt_Id, + body_stmts: []ast.Stmt_Id, + expected: types.Type, + span: source.Span, +) -> (value: hir.Expr_Id, value_type: types.Type) { + checker := ctx.checker + if len(body_stmts) == 1 { + #partial switch checker.ast_module.statements[body_stmts[0]].kind { + case .If: + return build_value_if(ctx, body, body_stmts[0], expected, span) + case .For, .While: + return build_value_loop(ctx, body, body_stmts[0], expected, span) + } + } + return build_value_block(ctx, body, body_stmts, expected, span) +} + +// new_value_slot allocates a fresh, un-nameable mutable local to hold a value-if/loop +// result. Branches/iterations assign it; the construct's value is a read of it. +new_value_slot :: proc(ctx: ^Build_Ctx, slot_type: types.Type) -> hir.Local_Id { + slot := hir.local_id(len(ctx.hir_locals^)) + append(ctx.hir_locals, hir.Local{name = ctx.checker.sink_symbol, type = slot_type, mutable = true}) + return slot +} + +// slot_read builds a `.Local` read of a result slot. +slot_read :: proc(checker: ^Checker, slot: hir.Local_Id, slot_type: types.Type, span: source.Span) -> hir.Expr_Id { + id := hir.expr_id(len(checker.module.exprs)) + append(&checker.module.exprs, hir.Expr{ + kind = .Local, span = span, type = slot_type, target = hir.local_ref(slot), + }) + return id +} + +// emit_slot_assign appends a bare-local `slot = value` assignment to `out`. +emit_slot_assign :: proc(checker: ^Checker, out: ^[dynamic]hir.Stmt_Id, slot: hir.Local_Id, value: hir.Expr_Id, span: source.Span) { + append(out, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Assignment, span = span, expr = value, local = slot, + target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, + }) +} + +// build_value_if turns `if c { … yield A } else { … yield B }` into a result slot +// each branch assigns, read after the if. Every path must yield: a mandatory `else`, +// each branch ends in `yield`, and all branches share a type (the first establishes it +// when untyped; later branches coerce). HIR holds an ordinary `.If` + a `.Local` read. +build_value_if :: proc( + ctx: ^Build_Ctx, + body: ^[dynamic]hir.Stmt_Id, + if_id: ast.Stmt_Id, + expected: types.Type, + span: source.Span, +) -> (value: hir.Expr_Id, value_type: types.Type) { + checker := ctx.checker + slot := hir.INVALID_LOCAL + slot_type := types.INVALID + if is_runtime_type(checker, expected) { + slot_type = expected + slot = new_value_slot(ctx, slot_type) + } + subtree: [dynamic]hir.Stmt_Id + subtree.allocator = checker.allocator + ok := emit_value_if(ctx, &subtree, if_id, &slot, &slot_type, span) + if !ok || slot == hir.INVALID_LOCAL { + for s in subtree { + append(body, s) + } + delete(subtree) + ctx.problematic^ = true + return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID + } + // The slot's poison declaration precedes the if; every path assigns it. + append(body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + for s in subtree { + append(body, s) + } + delete(subtree) + value = slot_read(checker, slot, slot_type, span) + return value, slot_type +} + +// emit_value_if builds one `if`/`else if`/`else` level of a value-if, appending the +// assembled `.If` to `out`. `slot`/`slot_type` thread through so the first branch can +// fix an untyped slot and `else if` chains share it. +emit_value_if :: proc( + ctx: ^Build_Ctx, + out: ^[dynamic]hir.Stmt_Id, + if_id: ast.Stmt_Id, + slot: ^hir.Local_Id, + slot_type: ^types.Type, + span: source.Span, +) -> bool { + checker := ctx.checker + if_stmt := checker.ast_module.statements[if_id] + // ponytail: an unwrap `if` (captures) as a value source is a later milestone. + if len(if_stmt.captures) > 0 { + source.add(checker.diagnostics, if_stmt.span, "an unwrap 'if' cannot yet be used as a value") + ctx.problematic^ = true + return false + } + condition := build_expr(checker, if_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) + if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) { + id := source.add(checker.diagnostics, if_stmt.span, "'if' condition must be a bool") + condition = invalid_hir_expr(checker, if_stmt.span, id, types.BOOL) + ctx.problematic^ = true + } + then_body: [dynamic]hir.Stmt_Id + then_body.allocator = checker.allocator + if !emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span) { + delete(then_body) + return false + } + if if_stmt.else_body == nil { + source.add(checker.diagnostics, if_stmt.span, "an 'if' used as a value must have an 'else' so every path yields") + delete(then_body) + ctx.problematic^ = true + return false + } + else_body: [dynamic]hir.Stmt_Id + else_body.allocator = checker.allocator + branch_ok := true + if len(if_stmt.else_body) == 1 && checker.ast_module.statements[if_stmt.else_body[0]].kind == .If { + branch_ok = emit_value_if(ctx, &else_body, if_stmt.else_body[0], slot, slot_type, span) + } else { + branch_ok = emit_value_branch(ctx, &else_body, if_stmt.else_body, slot, slot_type, span) + } + if !branch_ok { + delete(then_body) + delete(else_body) + return false + } + append(out, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .If, span = if_stmt.span, expr = condition, guard = hir.INVALID_EXPR, + then_body = then_body[:], else_body = else_body[:], + local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, + }) + return true +} + +// emit_value_branch builds one branch of a value-if as a value block (leading stmts + +// trailing yield) and appends `slot = `. The first branch of an untyped value-if +// fixes the slot type; later branches coerce to it (a mismatch is the "same type" error). +emit_value_branch :: proc( + ctx: ^Build_Ctx, + out: ^[dynamic]hir.Stmt_Id, + branch_stmts: []ast.Stmt_Id, + slot: ^hir.Local_Id, + slot_type: ^types.Type, + span: source.Span, +) -> bool { + checker := ctx.checker + value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span) + if checker.module.exprs[value].kind == .Invalid { + return false + } + if slot^ == hir.INVALID_LOCAL { + slot_type^ = vtype + slot^ = new_value_slot(ctx, slot_type^) + } else { + value = coerce_expr(checker, value, slot_type^, span) + } + emit_slot_assign(checker, out, slot^, value, span) + return true +} + +// loop_yields_none reports whether any `yield` that targets this loop (a labeled +// `yield :blk` inside `if`/block branches, or the trailing fall-through) yields the +// literal `none` — making the loop's result optional. Pure AST walk; does not descend +// into nested loops or value sources, whose yields belong to them. +loop_yields_none :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> bool { + for id in stmts { + s := checker.ast_module.statements[id] + #partial switch s.kind { + case .Yield: + if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind == .None { + return true + } + case .If: + if loop_yields_none(checker, s.body) || loop_yields_none(checker, s.else_body) { + return true + } + case .Block: + if loop_yields_none(checker, s.body) { + return true + } + } + } + return false +} + +// resolve_loop_slot fixes a value-loop's result slot from its first concrete yield (an +// optional element type when the loop also yields `none`) and coerces `value` into it. +// Returns INVALID when the type can't be fixed yet (a `none`/invalid first yield). +resolve_loop_slot :: proc(ctx: ^Build_Ctx, target: ^Yield_Target, value: hir.Expr_Id, vtype: types.Type, span: source.Span) -> hir.Expr_Id { + checker := ctx.checker + if target.slot == hir.INVALID_LOCAL { + if !is_runtime_type(checker, vtype) { + return hir.INVALID_EXPR + } + target.slot_type = types.optional(&checker.module.types, vtype) if target.result_optional else vtype + target.slot = new_value_slot(ctx, target.slot_type) + } + return coerce_expr(checker, value, target.slot_type, span) +} + +// 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` +// desugars (in build_block) to `slot = x; break`, and the construct's value is a read +// of the slot after the loop. Reuses the ordinary `.For`/`.While` build via a peeled +// copy; no new HIR. The yielded type is the annotation when typed, else the first +// concrete yield's type (optional when any yield is `none`). +build_value_loop :: proc( + ctx: ^Build_Ctx, + body: ^[dynamic]hir.Stmt_Id, + loop_id: ast.Stmt_Id, + expected: types.Type, + span: source.Span, +) -> (value: hir.Expr_Id, value_type: types.Type) { + checker := ctx.checker + loop_stmt := checker.ast_module.statements[loop_id] + n := len(loop_stmt.body) + last_is_fallthrough := n > 0 && + checker.ast_module.statements[loop_stmt.body[n - 1]].kind == .Yield && + !symbol.is_valid(checker.ast_module.statements[loop_stmt.body[n - 1]].label) + if !symbol.is_valid(loop_stmt.label) { + id := source.add(checker.diagnostics, span, + "a value loop must label its body (e.g. 'blk:') so a 'yield :blk' can exit it") + ctx.problematic^ = true + return invalid_hir_expr(checker, span, id), types.INVALID + } + if !last_is_fallthrough { + id := source.add(checker.diagnostics, span, + "a value loop's body must end with a 'yield' for when the loop completes") + ctx.problematic^ = true + return invalid_hir_expr(checker, span, id), types.INVALID + } + fall_stmt := checker.ast_module.statements[loop_stmt.body[n - 1]] + + result_optional := loop_yields_none(checker, loop_stmt.body) + 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) + } + append(ctx.yield_targets, Yield_Target{ + label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional, + }) + + // Build the loop with the fall-through peeled off, reusing the normal For/While arm. + // The peeled body is a fresh copy so destroy_module won't double-free the original. + peeled := loop_stmt + peeled_body := make([]ast.Stmt_Id, n - 1, checker.ast_module.allocator) + copy(peeled_body, loop_stmt.body[:n - 1]) + peeled.body = peeled_body + peeled_id := ast.stmt_id(len(checker.ast_module.statements)) + append(&checker.ast_module.statements, peeled) + loop_block := build_block(ctx, []ast.Stmt_Id{peeled_id}) + + target := pop(ctx.yield_targets) + + // The fall-through value initializes the slot before the loop (loop captures are + // out of scope here), so the loop completing leaves it as the result. + fall_value := build_expr(checker, fall_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) + fall_value = resolve_loop_slot(ctx, &target, fall_value, checker.module.exprs[fall_value].type if fall_value != hir.INVALID_EXPR else types.INVALID, fall_stmt.span) + if target.slot == hir.INVALID_LOCAL || fall_value == hir.INVALID_EXPR { + for s in loop_block { + append(body, s) + } + delete(loop_block, checker.allocator) + id := source.add(checker.diagnostics, span, + "could not determine the value loop's yield type; annotate the binding") + 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 = fall_value, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + for s in loop_block { + append(body, s) + } + delete(loop_block, checker.allocator) + + value = slot_read(checker, target.slot, target.slot_type, span) + return value, target.slot_type +} + // Reports whether every control-flow path through `stmts` terminates (returns or traps), // so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an // `.If` terminates only when it has an `else` and both arms terminate. A literal @@ -5069,6 +5444,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { defers.allocator = checker.allocator loop_defer_starts: [dynamic]int loop_defer_starts.allocator = checker.allocator + yield_targets: [dynamic]Yield_Target + yield_targets.allocator = checker.allocator ctx := Build_Ctx{ checker = checker, pkg = function.pkg, @@ -5082,6 +5459,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { problematic = &problematic, defers = &defers, loop_defer_starts = &loop_defer_starts, + yield_targets = &yield_targets, } block := build_block(&ctx, function.body) returns := all_paths_return(&checker.module, block) @@ -5131,6 +5509,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { } delete(defers) delete(loop_defer_starts) + delete(yield_targets) delete(locals) } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 67c4e06..210f0be 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -49,6 +49,23 @@ previous :: proc(parser: ^Parser) -> token.Token { return parser.tokens.items[max(parser.cursor-1, 0)] } +peek :: proc(parser: ^Parser) -> token.Token { + return parser.tokens.items[min(parser.cursor+1, len(parser.tokens.items)-1)] +} + +// A loop body may be labeled `blk: { ... }` so a nested `yield :blk x` can exit +// it past an enclosing `if`. Consumes and returns the label when the next tokens +// are `Identifier Colon`; otherwise leaves the cursor untouched. +parse_optional_loop_label :: proc(parser: ^Parser) -> symbol.Id { + if current(parser).kind == .Identifier && peek(parser).kind == .Colon { + name := advance(parser) // the label name + advance(parser) // consume ':' + skip_newlines(parser) + return name.symbol + } + return symbol.INVALID +} + advance :: proc(parser: ^Parser) -> token.Token { result := current(parser) if result.kind != .Eof { @@ -992,11 +1009,23 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id { parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id { start := advance(parser) // consume 'yield' skip_newlines(parser) + // `yield :blk x` targets the loop labeled `blk`; a bare `yield x` targets + // the directly-enclosing value block / if branch. No expression starts with + // ':', so a leading colon is unambiguously a label. + 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 ':'") + } + } expr := parse_expression(parser) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ kind=.Yield, span=span_from(start.span, parser.module.exprs[expr].span), + label=label, expr=expr, diagnostic=source.INVALID_DIAGNOSTIC, }) @@ -1078,6 +1107,19 @@ starts_declared_type :: proc(parser: ^Parser) -> bool { return cursor < len(parser.tokens.items) && is_type_token(parser.tokens.items[cursor].kind) } +// A declaration/assignment RHS may be a value-producing control-flow construct: +// an `if`/`for`/`while` whose branches/iterations `yield`. Returns the parsed +// statement (to be carried as a one-element block-init `body`) and true when the +// current token opens one. +parse_value_control_flow :: proc(parser: ^Parser) -> (ast.Stmt_Id, bool) { + #partial switch current(parser).kind { + case .Keyword_If: return parse_if(parser), true + case .Keyword_For: return parse_for(parser), true + case .Keyword_While: return parse_while(parser), true + } + return ast.INVALID_STMT, false +} + parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { if current(parser).kind == .Keyword_Return { return parse_return(parser) @@ -1146,6 +1188,25 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { }) return id } + // A value-producing `if`/`for`/`while`: carried as a one-element block-init + // body, the same signal a value block uses (`expr` invalid). + if cf, is_cf := parse_value_control_flow(parser); is_cf { + body := make([]ast.Stmt_Id, 1, parser.module.allocator) + body[0] = cf + 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, + target=ast.INVALID_EXPR, + expr=ast.INVALID_EXPR, + body=body, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } expr := parse_expression(parser) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ @@ -1181,6 +1242,21 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { }) return id } + // A value-producing `if`/`for`/`while` assigned to a complex target. + if cf, is_cf := parse_value_control_flow(parser); is_cf { + body := make([]ast.Stmt_Id, 1, parser.module.allocator) + body[0] = cf + 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, + expr=ast.INVALID_EXPR, + body=body, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } value := parse_expression(parser) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ @@ -1494,6 +1570,7 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id { } } skip_newlines(parser) + label := parse_optional_loop_label(parser) body := parse_block(parser) id := ast.stmt_id(len(parser.module.statements)) @@ -1502,6 +1579,7 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id { span=span_from(start.span, previous(parser).span), name=item_name, index_name=index_name, + label=label, pointer_capture=pointer_capture, expr=iterable, body=body, @@ -1526,6 +1604,7 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id { update = parse_while_update(parser) skip_newlines(parser) } + label := parse_optional_loop_label(parser) body := parse_block(parser) id := ast.stmt_id(len(parser.module.statements)) @@ -1534,6 +1613,7 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id { span=span_from(start.span, previous(parser).span), expr=condition, body=body, + label=label, update=update, diagnostic=source.INVALID_DIAGNOSTIC, }) diff --git a/compiler_tests.odin b/compiler_tests.odin index 34d38b3..d206c51 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2131,8 +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) - // Untyped/typed value blocks, the value captured before block defers run, and - // reassignment from a value block together produce 42. + // Value blocks (untyped/typed, defer-spill, reassignment) plus value if-statements + // (untyped/typed/reassign/else-if/defer) and value loops (a labeled `for` search + // yielding `?usize` on both the found and not-found paths, and a labeled `while`) + // together produce 42. testing.expect_value(t, state.exit_code, 42) } @@ -2175,6 +2177,57 @@ yield_misuse_is_diagnosed :: proc(t: ^testing.T) { testing.expect(t, misplaced_yield) } +@(test) +yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { + // Value if/loop misuse (milestone 20.5): an `if` value without an `else`; a + // branch that does not end in `yield`; a value loop whose body lacks a trailing + // fall-through `yield`; and a `yield :label` with no matching value loop. + text := `main :: func() i32 { + noelse :: if (true) { + yield 1 + } + badbranch :: if (true) { + k :: 5 + } else { + yield 2 + } + noloopyield :: for 0..10 |i| blk: { + if (i == 0) yield :blk i + } + for 0..10 |j| stray: { + yield :stray j + } + return noelse + badbranch + noloopyield +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + no_else := false + branch_no_yield := false + loop_no_yield := false + stray_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 block must end with an explicit 'yield'") + 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'") + } + testing.expect(t, no_else) + testing.expect(t, branch_no_yield) + testing.expect(t, loop_no_yield) + testing.expect(t, stray_label) +} + @(test) foreign_function_links_from_c_source :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-source" diff --git a/examples/programs/yield/main.bro b/examples/programs/yield/main.bro index 12ed8b0..da97aa5 100644 --- a/examples/programs/yield/main.bro +++ b/examples/programs/yield/main.bro @@ -1,9 +1,11 @@ -# Milestone 20: `yield` and value blocks. +# Milestones 20 / 20.5: `yield`, value blocks, and value if/loops. # -# A `{ ... }` on the right of a declaration or assignment is a value block: its -# final `yield ` supplies the value (the block analogue of `return`). The -# yielded value is captured before the block's defers run. Each section returns a -# distinct code on failure; success falls through to 42. +# A `{ ... }`, an `if`, or a labeled `for`/`while` on the right of a declaration +# or assignment is a *value source*: `yield ` supplies the value (the block +# analogue of `return`). Every path must yield. Each section returns a distinct +# code on failure; success falls through to 42. + +# --- value blocks (milestone 20) --------------------------------------------- # Untyped `::`: the local's type is the yield's natural type. basic :: func() i32 { @@ -43,10 +45,109 @@ reassign :: func() i32 { return r } +# --- value if-statements (milestone 20.5) ------------------------------------ + +# Untyped `::` over an `else if` chain; the first branch fixes the type. +vif_untyped :: func(sel i32) i32 { + r :: if (sel == 0) { + yield 10 + } else if (sel == 1) { + yield 20 + } else { + yield 30 + } + return r +} + +# Typed `T =`: every branch coerces to the annotation. +vif_typed :: func(sel i32) i32 { + r i32 = if (sel == 0) { yield 100 } else { yield 200 } + return r +} + +# Assigned into an existing local. +vif_reassign :: func(sel i32) i32 { + r i32 = 0 + r = if (sel == 0) { yield 7 } else { yield 9 } + return r +} + +# A branch is a full value block: leading statements + a defer captured before +# the yield. +vif_defer :: func() i32 { + r :: if (true) { + n i32 = 5 + defer n = 999 + yield n + } else { + yield 0 + } + return r # 5 +} + +# --- value loops (milestone 20.5) -------------------------------------------- + +# Labeled `for` used as a value: `yield :blk i` exits early with a value, the +# trailing `yield none` supplies the value when the loop completes. The `{i, +# none}` yields resolve the result to an optional. +loop_search :: func() i32 { + # first i in 0..10 whose square exceeds 40 (6*6=36 no, 7*7=49 yes -> 7). + idx :: for 0..10 |i| blk: { + if (i * i > 40) yield :blk i + yield none + } + if idx |found| { + if (found == 7) return 0 + return 1 + } + return 2 +} + +# Same loop, but nothing matches -> the fall-through `yield none` is the result. +loop_none :: func() i32 { + idx :: for 0..10 |i| blk: { + if (i > 100) yield :blk i + yield none + } + if idx |found| { + _ = found + return 1 # should be unreachable: no match + } + return 0 +} + +# Labeled `while` value loop (label follows the `: update` clause). +loop_while :: func() i32 { + n i32 = 0 + found :: while n < 100 : n += 1 blk: { + if (n == 8) yield :blk n + yield none + } + if found |v| { + if (v == 8) return 0 + return 1 + } + return 2 +} + main :: func() i32 { if (basic() != 42) return 101 if (typed() != 100) return 102 if (spill() != 5) return 103 if (reassign() != 7) return 104 + + if (vif_untyped(0) != 10) return 105 + if (vif_untyped(1) != 20) return 106 + if (vif_untyped(2) != 30) return 107 + if (vif_typed(0) != 100) return 108 + if (vif_typed(1) != 200) return 109 + if (vif_reassign(0) != 7) return 110 + if (vif_reassign(9) != 9) return 111 + if (vif_defer() != 5) return 112 + + if (loop_search() != 0) return 113 + if (loop_none() != 0) return 114 + if (loop_while() != 0) return 115 + return 42 }