Generators
A function containing yield declares Generator[T]. Ordinary call syntax
producer(arguments) constructs its owned frame; .new(arguments) is an
equivalent alias. Arguments evaluate first and copy or move into the frame.
Construction runs no body code and requires no heap allocation. Argument
expressions keep their own allocation and error contracts.
def numbers(stop: i64) -> Generator[i64]:
mut value = 0
while value < stop:
yield value
# value starts at zero and value < stop <= i64.MAX.
unchecked:
value = value + 1
def main() -> Result[(), IoError]:
for value in numbers(3):
print(value)?
Ok(())
0
1
2
Resumption executes until a statement-only yield value, bare return, or
fallthrough. Yield types are exact and owned payloads transfer to the caller.
Reference, generator, closure, and unit yields are rejected; a generator body
cannot return a value. An ordinary factory without yield may return another
frame by moving it. ? is unsupported in a generator body; match fallible results
explicitly. Recoverable arithmetic failures cannot propagate from a generator:
handle a
local checked: result, call a helper
that returns a Result and yield or match it, or use a small unchecked: suite
with a documented bound.
next(&mut g) -> Option[T] borrows a mutable generator owner or reborrows an
exclusive generator reference. next(g) is shorthand for named bindings and
exclusive references. Shared references, immutable owners, and mutable
temporaries cannot be advanced. The borrow lasts only through the call;
exhaustion is stable and produces Nothing on every later call. Resumption
and its Option wrapper add no allocation beyond body operations.
Loops, comprehensions, and iterable collection constructors consume frames.
break destroys a loop’s hidden frame without executing subsequent generator
statements. Assignment, arguments, and returns transfer ownership. Standard
Option/Result wrappers may own frames, including nested wrappers, and remain
affine. Frames cannot enter collections, class fields, tuples, user enum
payloads, or heap-stored closures. Printing, comparison, length, and membership
are undefined for frames.
Each producer and concrete specialization has a distinct frame identity.
Generator[T] describes the yield contract without erasing that identity.
Local annotations preserve the initializer’s producer, and parameters,
references, aliases, and standard sum wrappers specialize to actual frames.
A factory must resolve to one producer per specialization; equal yield types
do not allow different producers to join. Nothing or Err alone cannot
infer an absent producer. No hidden boxing makes frames interchangeable.
A suspended frame retains its live owned locals. Completion releases them;
dropping a suspended or never-started frame releases remaining locals and
captures without resuming the body or allocating. Short local borrows completed
within one resume are permitted, but generator parameters cannot be references
and no live loan may cross yield. yield inside with is unsupported.
There is no yield-from, send/throw, or async/await.
Fluent transformations
Generators expose the pipeline adapters and terminals:
numbers(100).filter(even).map(square).fold(0, add) consumes the generator
through a fused adapter loop. Source resumption retains its ordinary cost;
the fluent adapters do not each construct another suspended frame.
An owned pipeline can become a returnable resumable stream through explicit
.generator(). It transfers already prepared source and capture state into
one frame without advancing any item. Borrowed sources or callback captures
are ineligible, including when mapped items are owned. There is no implicit
conversion at a Generator[T] parameter or return annotation. The resulting
frame follows the specialization, suspension and cleanup rules above.
Generator expressions
(expression for name in source if condition) creates a concrete owned
Generator[T], with its yielded type inferred from expression. Multiple
for and if clauses have the same scopes and nesting limits as comprehensions.
The outer source evaluates once at creation; inner sources, filters, and the
element expression run only as the frame advances. A range outer source is
stored in the frame and advances as a counted loop. A sole call argument can
omit parentheses: list(n for n in range(4)). Arithmetic in an element or
filter follows the same generator restriction; a Result-returning helper may
produce Result items. With other arguments,
parenthesize the generator expression.
Inputs and referenced enclosing local values copy or move into the frame at
creation. Copyable captures are snapshots; later assignments do not change them.
Affine captures become unavailable to the caller. Captures are taken in order
of first use in the remaining clauses, then the yielded expression, after the
outer source. References cannot be captured, including a borrowed outer source;
use an owned source or copy(source)?. Adapter sources enumerate, zip, and
reversed evaluate their inputs at creation without allocating an adapter.
Construction and advancement add no heap allocation beyond the expressions they run. The same yield restrictions, propagation restrictions, destruction, and concrete-frame specialization rules apply as for generator functions. Dropping an unstarted frame releases its captures without evaluating filters or elements; short-circuit consumers destroy the remaining frame immediately.
Recursive inline layouts are rejected. Nested frame layouts are limited to 64 levels, native frame offsets to signed 32-bit sizes, a function’s native stack frame (which holds the inline frames it owns) to Cranelift’s 1 GiB, a compile-time error naming the function, and concrete consumer specializations to 16,384 per consumer and 65,536 per compilation, with the dependency depth limit of generic functions guarding expanding recursion (see specialization). These are implementation limits, not heap-backed fallback behavior. See generator implementation.