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
|
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 21–22)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ Stmt_Kind :: enum u8 {
|
|||||||
Continue,
|
Continue,
|
||||||
Block,
|
Block,
|
||||||
Defer,
|
Defer,
|
||||||
|
Yield,
|
||||||
}
|
}
|
||||||
|
|
||||||
Assignment_Op :: enum u8 {
|
Assignment_Op :: enum u8 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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