Generator frames and closure environments
Each generator producer/specialization has a concrete frame layout, resume
function, and immutable descriptor. A frame owns parameters and locals in typed
slots, plus slot metadata, continuation state, resume callback, and reentrancy guard.
Its internal resume ABI is (frame, out_slot) -> ready; yields transfer owned
values, completion clears slots, and destruction releases remaining captures.
Ordinary slots use 16 bytes; inline payloads add their complete storage. Standard sums reserve the largest possible payload, and frames can contain other frames. Constructors and returning functions use caller-owned output storage. Moving nested frames repairs addresses; cleanup completes before surrounding storage expires.
Layout caches are per concrete producer. Recursive inline layouts are rejected, nesting is limited to 64 levels, native offsets fit signed 32-bit sizes, and consumer specialization is limited to 256 per compilation. Unresolved factory returns are rechecked with inferred context so all paths agree on concrete storage.
Yield requires an empty residual operand stack. Lowering adds resume-dispatch
edges to continuation blocks; all locals are frame-backed, so SSA values need
not survive invocations. Dispatch currently uses a linear comparison chain.
There is no interpreter, native-stack suspension, or eager yield collection.
Iteration wraps resume results in inline Option values.
Closures
Capture environments are concrete inline layouts with nominal producer identities. Consumers specialize for their actual layout; matching signatures do not erase identity or introduce boxing. Owned factories return into caller storage. Nested environment moves repair addresses, including environments stored in generator frames or collection rows.
Reusable invocation borrows an environment; consuming invocation transfers it. Borrowed capture loans remain source-level facts through environment moves and generic consumers. Captures are released in reverse capture order; an environment whose captures need no cleanup, such as a capture-less error mapper, is not retained or released at all. Cached facts and visited-type traversal avoid repeatedly expanding shared capture/type graphs. Nesting is limited to 64 levels and storage must fit the native stack-layout limit. The callable contract defines permitted captures, storage, and invocation; the generator contract defines suspension and lifetime restrictions.
Arithmetic boundaries and callbacks
Every named function and closure has an independent declared return contract.
The frontend’s CheckedFunction operation gives arithmetic failures a shared
exit through that public Result; it does not wrap successful source returns.
An explicit checked: expression instead constructs its own local Result.
unchecked: lowers its lexical suite’s operators with trap guards, which
validate its no-failure precondition. Nested
function definitions start their own recoverable boundary in either mode.
See function arithmetic contracts.
There is no private callback error channel, CheckedUnwrap operation, or
checked-block lifetime restriction. Ordinary calls, generic callback consumers,
and substituted pipeline callbacks preserve the callback’s declared Result
and its normal ownership rules. Consumers must handle fallible callbacks
explicitly; a caller’s arithmetic mode never changes a callee’s ABI.
Serial integer pipeline sum() also has a public overflow Result, rather
than inheriting the consumer’s local mode.
Runtime key sorting uses one caller-owned first-error cell per invocation. A failed key evaluation saves its error, releases earlier keys, and returns an equal comparison. Later comparisons skip user callbacks. The sorter finishes its ownership-safe row moves, then its caller forwards the saved error. This preserves its bounded sorting complexity and allocation behavior without unwinding across Rust frames; a failure does not roll back row order.