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
+1
View File
@@ -133,6 +133,7 @@ Stmt_Kind :: enum u8 {
Continue,
Block,
Defer,
Yield,
}
Assignment_Op :: enum u8 {
+185 -13
View File
@@ -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),
+1
View File
@@ -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
+56 -2
View File
@@ -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,
+1
View File
@@ -70,6 +70,7 @@ Kind :: enum u8 {
Keyword_Break,
Keyword_Continue,
Keyword_Defer,
Keyword_Yield,
Keyword_Else,
Keyword_True,
Keyword_False,