From f610be1b59cfecc08241a34f044801d1f3ff8f33 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 27 Jun 2026 04:12:04 +0200 Subject: [PATCH] add `yield` --- TODO.md | 80 ++++++++++++- compiler/ast/ast.odin | 1 + compiler/checker/checker.odin | 198 +++++++++++++++++++++++++++++-- compiler/lexer/lexer.odin | 1 + compiler/parser/parser.odin | 58 ++++++++- compiler/token/token.odin | 1 + compiler_tests.odin | 51 ++++++++ examples/programs/yield/main.bro | 52 ++++++++ 8 files changed, 426 insertions(+), 16 deletions(-) create mode 100644 examples/programs/yield/main.bro diff --git a/TODO.md b/TODO.md index 35a3852..9386a38 100644 --- a/TODO.md +++ b/TODO.md @@ -305,7 +305,32 @@ a bare block is built and spliced inline, and a deferred statement is built once at the `defer` site and its hir replayed at each exit, so lowering/codegen are unchanged -20. add `yield` statement (see below) +20. add `yield` statement (implemented; first pass — value blocks only; see below) + - a `{ ... }` on the right of a declaration or assignment is a *value block*: its final + statement must be `yield `, which supplies the block's value (the block analogue + of `return`). Supported: `x :: { ...; yield v }` (untyped — the local takes the yield's + natural type), `x T = { ... }` (coerces to `T`), and `target = { ... }` (coerces to the + target's type, including complex targets like `a[i] = { ... }`) + - the yielded value is captured *before* the block's defers run (a defer that mutates a + block local can't change what is yielded), reusing the `return` spill-to-temp pattern + - `yield` is valid *only* as the final statement of a value block. A `yield` nested in an + `if`/loop/inner block, or in a non-value block, is rejected ("'yield' is only valid as + the final statement of a value block"); a value block not ending in `yield` is rejected + too. This no-early-exit restriction keeps it a lexer/parser/checker-only change with no + HIR/lowering touch (like milestones 18/19) + - new `Keyword_Yield` token + `.Yield` AST stmt (reuses `expr`); a block-initialized + `Declaration`/`Assignment` reuses the existing `body` field with `expr` invalid. The + checker's `build_value_block` builds the leading statements (via `build_block` with a + new `close=false` flag that keeps the scope open), evaluates the final yield, spills and + flushes the block's defers, then feeds the value into an ordinary `Declaration`/ + `Assignment`. HIR never holds a `.Yield` (final yield → `Declaration`/`Assignment`, + misplaced yield → `Trap`), so lowering/codegen are unchanged + - deferred to a later milestone (needs labeled blocks + rules for whether an `if`/loop + always produces a value, e.g. optionals): yield from inside `if`/loops, labeled blocks + (`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) 21. unions and tagged unions @@ -546,6 +571,59 @@ data []u8 = read(path) catch |e| { } ``` +### Yielding from if-statements and loops + +Yielding from if-statements is possible with the constraint that all branches must resolve to the same yield type. + +``` +# yielding to a constant +result :: if a { + yield 1 +} else if b { + yield 2 +} else { + yield 3 +} + +# yielding to a variable +result int = if a { + yield 1 +} else if b { + yield 2 +} else { + yield 3 +} + +# ILLEGAL: branches with different yield types +result :: if a { + yield 1 +} else { + yield Color{ r = 255, g = 0, b = 0 } +} +``` + +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 + + # 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). +} + +# 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 +} + +# BAD: likewise for loops +for 0..10 |i| blk: { # bad, no name binds returned value + if is_active(some_entity, i) yield :blk i +} +``` + ## A word on memory allocation (NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index f0c0423..d04800c 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -133,6 +133,7 @@ Stmt_Kind :: enum u8 { Continue, Block, Defer, + Yield, } Assignment_Op :: enum u8 { diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 874f10e..e359f39 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -724,11 +724,13 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi for statement_id in statements { statement := checker.ast_module.statements[statement_id] switch statement.kind { - case .Declaration, .Assignment, .Return, .Expression: + case .Declaration, .Assignment, .Return, .Expression, .Yield: mark_expr_imports_used(checker, statement.expr, file) if statement.target != ast.INVALID_EXPR { mark_expr_imports_used(checker, statement.target, file) } + // A value-block declaration/assignment carries its block in `body`. + mark_block_imports_used(checker, statement.body, file) case .If: mark_expr_imports_used(checker, statement.expr, file) if statement.guard != ast.INVALID_EXPR { @@ -1721,6 +1723,22 @@ infer_statements :: proc( statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { case .Declaration: + if statement.expr == ast.INVALID_EXPR { + // Value block (`x :: { ... yield v }` / `x T = { ... }`): register the + // binding (its declared type when annotated, else left open) and walk + // the block body. The build pass resolves the yielded value's type + // independently — value blocks don't join the demand fixpoint. + declared_block := type_from_syntax(statement.type) + block_type := declared_block if is_runtime_type(checker, declared_block) else types.INVALID + local := Infer_Local{ + name=statement.name, type=block_type, declared=declared_block, + statement=statement_id, mutable=!statement.immutable, + } + append(locals, local) + record_infer_local_type(local, local_types) + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) + continue + } declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) value_type := types.INVALID if !is_undefined_expr(checker, statement.expr) { @@ -1770,6 +1788,12 @@ infer_statements :: proc( record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file) } case .Assignment: + if statement.expr == ast.INVALID_EXPR { + // Value block assigned to a target: walk the block body; the build + // pass handles the target coercion. + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) + continue + } value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) // Only push the target's type back onto a bare-name RHS (e.g. `x += speed`): // pushing through an arithmetic RHS would feed the target's (often provisional) @@ -3996,6 +4020,7 @@ build_block :: proc( ctx: ^Build_Ctx, statements: []ast.Stmt_Id, duplicate_scope_start := -1, + close := true, ) -> []hir.Stmt_Id { checker := ctx.checker body: [dynamic]hir.Stmt_Id @@ -4007,6 +4032,43 @@ build_block :: proc( statement := checker.ast_module.statements[statement_id] switch statement.kind { case .Declaration: + // A value block (`x :: { ... yield v }` / `x T = { ... }`): the parser + // leaves `expr` invalid and stashes the block in `body`. Build it, then + // declare the local from the yielded value (its type for an untyped `::`). + if statement.expr == ast.INVALID_EXPR { + expected := types.INVALID + typed := is_runtime_type(checker, type_from_syntax(statement.type)) + if typed { + expected = type_from_syntax(statement.type) + } + value, value_type := build_value_block(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, + "duplicate local '%s'", symbol_text(checker, statement.name), + ) + 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 + } + local_id := hir.local_id(len(ctx.hir_locals^)) + append(ctx.hir_locals, hir.Local{ + name = statement.name, type = value_type, mutable = !statement.immutable, + }) + append(ctx.locals, Build_Local{ + name = statement.name, type = value_type, mutable = !statement.immutable, id = local_id, + }) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Declaration, span = statement.span, local = local_id, expr = value, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + continue + } declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) // Adopt the type inference resolved for this local when the declaration has no // concrete annotation and inference carried useful numeric context: constraints, @@ -4187,6 +4249,10 @@ build_block :: proc( value = coerce_expr(checker, value, target_type, statement.span) } } + } 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) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, @@ -4204,7 +4270,12 @@ build_block :: proc( continue } if statement.name == checker.sink_symbol { - value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) + value: hir.Expr_Id + if statement.expr == ast.INVALID_EXPR { + value, _ = build_value_block(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) + } if types.is_void(checker.module.exprs[value].type) { id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'") append(&body, hir.stmt_id(len(checker.module.statements))) @@ -4243,11 +4314,16 @@ build_block :: proc( ctx.problematic^ = true continue } - value := build_expr( - checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, - local.type, ctx.pkg, ctx.file, - ) - value = coerce_expr(checker, value, local.type, statement.span) + value: hir.Expr_Id + if statement.expr == ast.INVALID_EXPR { + value, _ = build_value_block(ctx, &body, statement.body, local.type, statement.span) + } else { + value = build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + local.type, ctx.pkg, ctx.file, + ) + value = coerce_expr(checker, value, local.type, statement.span) + } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Assignment, span = statement.span, expr = value, local = local.id, @@ -4663,6 +4739,21 @@ build_block :: proc( append(&body, stmt) } 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. + id := source.add( + checker.diagnostics, statement.span, + "'yield' is only valid as the final statement of a value block", + ) + 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 case .Defer: deferred := checker.ast_module.statements[statement.update] if deferred.kind == .Return || deferred.kind == .Break || @@ -4701,18 +4792,99 @@ build_block :: proc( } // Normal fall-through exit: run this block's own deferred statements, unless // every path already exited early (return/break/continue) — that would only - // emit unreachable duplicates. - if !all_paths_exit(&checker.module, body[:]) { - flush_defers(ctx, &body, defer_start) + // emit unreachable duplicates. A value block (`close=false`) skips this so its + // caller can capture the yielded value before flushing the block's defers. + if close { + if !all_paths_exit(&checker.module, body[:]) { + flush_defers(ctx, &body, defer_start) + } + // Free this block's deferred-statement entry slices (their stmt ids were + // already replayed at every path that can leave this block) and pop the frame. + 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) } - // Free this block's deferred-statement entry slices (their stmt ids were already - // replayed at every path that can leave this block) and pop the frame. + return body[:] +} + +// build_value_block builds a `{ ... yield v }` value block whose final statement +// must be a `yield`: it builds the leading statements inline (their own scope and +// defers), evaluates the yield expression in that scope, then — capturing the value +// first, like a function return — runs the block's defers and closes the scope. The +// resulting `value`/`value_type` are spliced into the enclosing declaration or +// assignment. `expected` is the binding's type (INVALID for an untyped `::`, where +// the yield's natural type is taken). Statements are appended to `body`. +build_value_block :: 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 + n := len(body_stmts) + if n == 0 || checker.ast_module.statements[body_stmts[n - 1]].kind != .Yield { + // Build whatever is there so inner errors (and misplaced yields) surface, then + // report the missing trailing yield. + inner := build_block(ctx, body_stmts) + for s in inner { + append(body, s) + } + delete(inner, checker.allocator) + id := source.add(checker.diagnostics, span, "a value block must end with an explicit 'yield'") + ctx.problematic^ = true + return invalid_hir_expr(checker, span, id), types.INVALID + } + scope_start := len(ctx.locals^) + defer_start := len(ctx.defers^) + // Leading statements keep the scope open (close=false) so the yield can still see + // the block's locals; any nested `yield` hits the erroring `.Yield` switch case. + leading := build_block(ctx, body_stmts[:n - 1], close = false) + for s in leading { + append(body, s) + } + delete(leading, checker.allocator) + + yield_stmt := checker.ast_module.statements[body_stmts[n - 1]] + value = build_expr( + checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + expected, ctx.pkg, ctx.file, + ) + value_type = checker.module.exprs[value].type + if is_runtime_type(checker, expected) { + value = coerce_expr(checker, value, expected, yield_stmt.span) + value_type = checker.module.exprs[value].type + } + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + + // Run the block's deferred statements before the value escapes, but capture the + // value first (spill to a temp) so a defer can't change what is yielded — the same + // rule as `return`. + if len(ctx.defers^) > defer_start { + if checker.module.exprs[value].kind != .Invalid { + tmp := hir.local_id(len(ctx.hir_locals^)) + append(ctx.hir_locals, hir.Local{name = checker.sink_symbol, type = value_type, mutable = false}) + append(body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Declaration, span = yield_stmt.span, local = tmp, expr = value, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + value = hir.expr_id(len(checker.module.exprs)) + append(&checker.module.exprs, hir.Expr{ + kind = .Local, span = yield_stmt.span, type = value_type, target = hir.local_ref(tmp), + }) + } + flush_defers(ctx, body, defer_start) + } + // Close the scope (build_block left it open for us). 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 body[:] + return value, value_type } // Reports whether every control-flow path through `stmts` terminates (returns or traps), diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index ebe3fa4..8d99479 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -35,6 +35,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "break": return .Keyword_Break case "continue": return .Keyword_Continue case "defer": return .Keyword_Defer + case "yield": return .Keyword_Yield case "else": return .Keyword_Else case "true": return .Keyword_True case "false": return .Keyword_False diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 2286b1f..67c4e06 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -986,6 +986,23 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id { return id } +// `yield ` supplies the value of the enclosing value block. The checker +// only accepts it as the final statement of a value block (a `{ ... }` on the +// right of a declaration/assignment); it is the block analogue of `return`. +parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id { + start := advance(parser) // consume 'yield' + skip_newlines(parser) + 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), + expr=expr, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id +} + // `break` / `continue` carry no value and target the innermost loop; the // checker rejects them outside a loop. parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id { @@ -1083,6 +1100,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { if current(parser).kind == .Keyword_Defer { return parse_defer(parser) } + if current(parser).kind == .Keyword_Yield { + return parse_yield(parser) + } // A leading `{` opens a bare block scope (struct literals are postfix only). if current(parser).kind == .Left_Brace { return parse_block_statement(parser) @@ -1101,13 +1121,32 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { if operator.kind == .Colon_Colon || operator.kind == .Equal { advance(parser) skip_newlines(parser) - expr := parse_expression(parser) kind := ast.Stmt_Kind.Assignment immutable := false if operator.kind == .Colon_Colon || had_type { kind = .Declaration immutable = operator.kind == .Colon_Colon } + // 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 { + brace := current(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, brace.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{ kind=kind, @@ -1127,6 +1166,21 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { expr := parse_expression(parser) if _, ok := allow(parser, .Equal); ok { skip_newlines(parser) + // A value block assigned to a complex target (`a[i] = { ... }`, `p.f = { ... }`). + if current(parser).kind == .Left_Brace { + brace := current(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, brace.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{ @@ -1375,7 +1429,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id { statement := &parser.module.statements[update] switch statement.kind { case .Assignment, .Expression: - case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue, .Block, .Defer: + case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue, .Block, .Defer, .Yield: diagnostic := source.add( parser.diagnostics, statement.span, diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 3b296e8..7d30e08 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -70,6 +70,7 @@ Kind :: enum u8 { Keyword_Break, Keyword_Continue, Keyword_Defer, + Keyword_Yield, Keyword_Else, Keyword_True, Keyword_False, diff --git a/compiler_tests.odin b/compiler_tests.odin index 87e6724..34d38b3 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2124,6 +2124,57 @@ bad_return_in_defer :: func() void { testing.expect(t, return_in_defer) } +@(test) +yield_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-yield" + defer _ = os.remove(output) + 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. + testing.expect_value(t, state.exit_code, 42) +} + +@(test) +yield_misuse_is_diagnosed :: proc(t: ^testing.T) { + // A value block that does not end in `yield`, and a `yield` nested inside an + // `if` within a value block (only the final statement may yield). + text := `main :: func() i32 { + missing :: { + k :: 5 + } + nested :: { + if (true) { + yield 1 + } + yield 2 + } + return missing + nested +} +` + 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) + + missing_yield := false + misplaced_yield := false + for diagnostic in diagnostics.items { + missing_yield = missing_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'") + misplaced_yield = misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement of a value block") + } + testing.expect(t, missing_yield) + testing.expect(t, misplaced_yield) +} + @(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 new file mode 100644 index 0000000..12ed8b0 --- /dev/null +++ b/examples/programs/yield/main.bro @@ -0,0 +1,52 @@ +# Milestone 20: `yield` and value blocks. +# +# 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. + +# Untyped `::`: the local's type is the yield's natural type. +basic :: func() i32 { + x :: { + a :: 20 + b :: 22 + yield a + b + } + return x +} + +# Typed `T =`: the yield coerces to the annotation. +typed :: func() i64 { + x i64 = { + yield 100 + } + return x +} + +# The yielded value is captured before defers run: the defer mutates a block +# local, but the captured value is unchanged. +spill :: func() i32 { + v :: { + n i32 = 5 + defer n = 999 + yield n + } + return v # 5, not 999 +} + +# Reassignment into an existing mutable local. +reassign :: func() i32 { + r i32 = 0 + r = { + yield 7 + } + return r +} + +main :: func() i32 { + if (basic() != 42) return 101 + if (typed() != 100) return 102 + if (spill() != 5) return 103 + if (reassign() != 7) return 104 + return 42 +}