add yield
This commit is contained in:
@@ -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 <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 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)
|
||||
|
||||
@@ -133,6 +133,7 @@ Stmt_Kind :: enum u8 {
|
||||
Continue,
|
||||
Block,
|
||||
Defer,
|
||||
Yield,
|
||||
}
|
||||
|
||||
Assignment_Op :: enum u8 {
|
||||
|
||||
+185
-13
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -986,6 +986,23 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_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
|
||||
// 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,
|
||||
|
||||
@@ -70,6 +70,7 @@ Kind :: enum u8 {
|
||||
Keyword_Break,
|
||||
Keyword_Continue,
|
||||
Keyword_Defer,
|
||||
Keyword_Yield,
|
||||
Keyword_Else,
|
||||
Keyword_True,
|
||||
Keyword_False,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user