add yield

This commit is contained in:
2026-06-27 04:12:04 +02:00
parent b4ce2c2117
commit f610be1b59
8 changed files with 426 additions and 16 deletions
+79 -1
View File
@@ -305,7 +305,32 @@
a bare block is built and spliced inline, and a deferred statement is built once at the 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 `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 <expr>`, 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 2122)
20.5 `yield` from if-statements and loops (see below)
21. unions and tagged unions 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 ## A word on memory allocation
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY) (NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
+1
View File
@@ -133,6 +133,7 @@ Stmt_Kind :: enum u8 {
Continue, Continue,
Block, Block,
Defer, Defer,
Yield,
} }
Assignment_Op :: enum u8 { Assignment_Op :: enum u8 {
+178 -6
View File
@@ -724,11 +724,13 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
for statement_id in statements { for statement_id in statements {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
switch statement.kind { switch statement.kind {
case .Declaration, .Assignment, .Return, .Expression: case .Declaration, .Assignment, .Return, .Expression, .Yield:
mark_expr_imports_used(checker, statement.expr, file) mark_expr_imports_used(checker, statement.expr, file)
if statement.target != ast.INVALID_EXPR { if statement.target != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.target, file) 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: case .If:
mark_expr_imports_used(checker, statement.expr, file) mark_expr_imports_used(checker, statement.expr, file)
if statement.guard != ast.INVALID_EXPR { if statement.guard != ast.INVALID_EXPR {
@@ -1721,6 +1723,22 @@ infer_statements :: proc(
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind { #partial switch statement.kind {
case .Declaration: 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) declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
value_type := types.INVALID value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) { 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) record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
} }
case .Assignment: 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) 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`): // 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) // pushing through an arithmetic RHS would feed the target's (often provisional)
@@ -3996,6 +4020,7 @@ build_block :: proc(
ctx: ^Build_Ctx, ctx: ^Build_Ctx,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
duplicate_scope_start := -1, duplicate_scope_start := -1,
close := true,
) -> []hir.Stmt_Id { ) -> []hir.Stmt_Id {
checker := ctx.checker checker := ctx.checker
body: [dynamic]hir.Stmt_Id body: [dynamic]hir.Stmt_Id
@@ -4007,6 +4032,43 @@ build_block :: proc(
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
switch statement.kind { switch statement.kind {
case .Declaration: 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) 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 // Adopt the type inference resolved for this local when the declaration has no
// concrete annotation and inference carried useful numeric context: constraints, // 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) 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 { } else {
value = build_expr( value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -4204,7 +4270,12 @@ build_block :: proc(
continue continue
} }
if statement.name == checker.sink_symbol { 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) { if types.is_void(checker.module.exprs[value].type) {
id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'") id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'")
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
@@ -4243,11 +4314,16 @@ build_block :: proc(
ctx.problematic^ = true ctx.problematic^ = true
continue continue
} }
value := build_expr( 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, checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
local.type, ctx.pkg, ctx.file, local.type, ctx.pkg, ctx.file,
) )
value = coerce_expr(checker, value, local.type, statement.span) value = coerce_expr(checker, value, local.type, statement.span)
}
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .Assignment, span = statement.span, expr = value, local = local.id, kind = .Assignment, span = statement.span, expr = value, local = local.id,
@@ -4663,6 +4739,21 @@ build_block :: proc(
append(&body, stmt) append(&body, stmt)
} }
delete(block, checker.allocator) 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: case .Defer:
deferred := checker.ast_module.statements[statement.update] deferred := checker.ast_module.statements[statement.update]
if deferred.kind == .Return || deferred.kind == .Break || if deferred.kind == .Return || deferred.kind == .Break ||
@@ -4701,20 +4792,101 @@ build_block :: proc(
} }
// Normal fall-through exit: run this block's own deferred statements, unless // Normal fall-through exit: run this block's own deferred statements, unless
// every path already exited early (return/break/continue) that would only // every path already exited early (return/break/continue) that would only
// emit unreachable duplicates. // 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[:]) { if !all_paths_exit(&checker.module, body[:]) {
flush_defers(ctx, &body, defer_start) flush_defers(ctx, &body, defer_start)
} }
// Free this block's deferred-statement entry slices (their stmt ids were already // Free this block's deferred-statement entry slices (their stmt ids were
// replayed at every path that can leave this block) and pop the frame. // already replayed at every path that can leave this block) and pop the frame.
for i := defer_start; i < len(ctx.defers^); i += 1 { for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator) delete(ctx.defers^[i], checker.allocator)
} }
resize(ctx.defers, defer_start) resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start) resize(ctx.locals, scope_start)
}
return body[:] 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 value, value_type
}
// Reports whether every control-flow path through `stmts` terminates (returns or traps), // 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 // 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 // `.If` terminates only when it has an `else` and both arms terminate. A literal
+1
View File
@@ -35,6 +35,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "break": return .Keyword_Break case "break": return .Keyword_Break
case "continue": return .Keyword_Continue case "continue": return .Keyword_Continue
case "defer": return .Keyword_Defer case "defer": return .Keyword_Defer
case "yield": return .Keyword_Yield
case "else": return .Keyword_Else case "else": return .Keyword_Else
case "true": return .Keyword_True case "true": return .Keyword_True
case "false": return .Keyword_False case "false": return .Keyword_False
+56 -2
View File
@@ -986,6 +986,23 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
return id return id
} }
// `yield <expr>` 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 // `break` / `continue` carry no value and target the innermost loop; the
// checker rejects them outside a loop. // checker rejects them outside a loop.
parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id { 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 { if current(parser).kind == .Keyword_Defer {
return parse_defer(parser) 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). // A leading `{` opens a bare block scope (struct literals are postfix only).
if current(parser).kind == .Left_Brace { if current(parser).kind == .Left_Brace {
return parse_block_statement(parser) 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 { if operator.kind == .Colon_Colon || operator.kind == .Equal {
advance(parser) advance(parser)
skip_newlines(parser) skip_newlines(parser)
expr := parse_expression(parser)
kind := ast.Stmt_Kind.Assignment kind := ast.Stmt_Kind.Assignment
immutable := false immutable := false
if operator.kind == .Colon_Colon || had_type { if operator.kind == .Colon_Colon || had_type {
kind = .Declaration kind = .Declaration
immutable = operator.kind == .Colon_Colon 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)) id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=kind, kind=kind,
@@ -1127,6 +1166,21 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
expr := parse_expression(parser) expr := parse_expression(parser)
if _, ok := allow(parser, .Equal); ok { if _, ok := allow(parser, .Equal); ok {
skip_newlines(parser) 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) value := parse_expression(parser)
id := ast.stmt_id(len(parser.module.statements)) id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
@@ -1375,7 +1429,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
statement := &parser.module.statements[update] statement := &parser.module.statements[update]
switch statement.kind { switch statement.kind {
case .Assignment, .Expression: 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( diagnostic := source.add(
parser.diagnostics, parser.diagnostics,
statement.span, statement.span,
+1
View File
@@ -70,6 +70,7 @@ Kind :: enum u8 {
Keyword_Break, Keyword_Break,
Keyword_Continue, Keyword_Continue,
Keyword_Defer, Keyword_Defer,
Keyword_Yield,
Keyword_Else, Keyword_Else,
Keyword_True, Keyword_True,
Keyword_False, Keyword_False,
+51
View File
@@ -2124,6 +2124,57 @@ bad_return_in_defer :: func() void {
testing.expect(t, return_in_defer) 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) @(test)
foreign_function_links_from_c_source :: proc(t: ^testing.T) { foreign_function_links_from_c_source :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-source" output := "/tmp/brolang-test-foreign-source"
+52
View File
@@ -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 <expr>` 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
}