← All docs

Generators

Generator functions with yield, the built-in Iterator and Generator types, foreach over Iterator objects, and Generator::send for coroutine-style flow.

A generator is a function whose body uses the yield keyword. Calling a generator function returns a Generator object — a real PHP object that implements the built-in Iterator interface — instead of executing the body. Each call to Generator::next() (and the implicit calls inside a foreach) runs the body up to the next yield, hands the yielded value back, and suspends until the next call.

Quick example

<?php
function counter(int $from) {
    $i = $from;
    while ($i < $from + 3) {
        yield $i;
        $i++;
    }
}

foreach (counter(10) as $v) {
    echo $v;
    echo " ";
}
// Prints: 10 11 12

yield with explicit and auto keys

When yield is used without an explicit key, PHP assigns an auto-incrementing integer key starting at 0:

<?php
function gen() { yield "a"; yield "b"; yield "c"; }
foreach (gen() as $k => $v) {
    echo "$k=$v ";
}
// Prints: 0=a 1=b 2=c

Explicit keys are passed through =>. A generator tracks the largest integer key it has yielded so far (starting below 0), and every keyless yield emits the next one after it. So an explicit integer key greater than every integer key yielded so far pushes the counter, and the following keyless yields continue from key + 1:

<?php
function gen() {
    yield 5 => "five";    // explicit key 5 — counter moves to 6
    yield "six";          // auto-key 6
    yield "seven";        // auto-key 7
}

Explicit keys that are not integers (string, float, bool, null) are yielded unchanged — generators do not apply array key coercion — and leave the counter alone. An integer key at or below the largest one already used is also yielded unchanged without rewinding the counter:

<?php
function gen() {
    yield "header";       // auto-key 0
    yield "k" => 42;      // string key — counter unchanged
    yield 3.5 => "half";  // float key, stays a float — counter unchanged
    yield "footer";       // auto-key 1
    yield 10 => "ten";    // counter moves to 11
    yield 2 => "two";     // lower than 10 — counter unchanged
    yield "tail";         // auto-key 11
}

The counter is a signed 64-bit value that wraps exactly like PHP’s: a PHP_INT_MAX key followed by a keyless yield produces PHP_INT_MIN. Each generator instance owns its counter.

yield from

yield from <array_literal> expands at compile time to one yield per element. Useful for sandwiching a fixed sequence between dynamic yields:

<?php
function delegate() {
    yield 0;
    yield from [10, 20, 30];
    yield 99;
}
foreach (delegate() as $v) { echo $v . " "; }
// Prints: 0 10 20 30 99

yield from <generator_function(args)> delegates iteration to another generator at runtime. The outer generator forwards each value (and key) from the inner, then continues its own body once the inner is exhausted:

<?php
function inner() { yield 1; yield 2; yield 3; }
function outer() {
    yield 0;
    yield from inner();
    yield 99;
}
foreach (outer() as $v) { echo $v . " "; }
// Prints: 0 1 2 3 99

yield from forwards the delegate’s keys verbatim: it neither renumbers them nor advances the outer generator’s auto-key counter, so the outer and inner keys can collide, exactly as in PHP:

<?php
function inner() { yield 100 => "i1"; yield "i2"; }
function outer() { yield 3 => "o1"; yield from inner(); yield "o2"; }
foreach (outer() as $k => $v) { echo "$k=$v "; }
// Prints: 3=o1 100=i1 101=i2 4=o2

yield from <generator> is driven by the __rt_gen_delegate runtime helper, which runs on the outer generator’s coroutine stack: it advances the inner generator, re-yields each inner key/value through the outer suspend boundary, forwards sent values into the inner generator, and returns the inner generator’s getReturn(). yield from <array> is desugared into an iterator loop. Both delegation paths enter the suspend primitive through __rt_gen_suspend_delegated, the entry point that skips the auto-key bookkeeping. Invalid non-generator, non-iterable delegates are rejected at type-check time.

Like PHP, yield from also evaluates to the delegated generator’s terminal return value, so the outer generator can capture and yield or return it after delegation finishes:

<?php
function inner() {
    yield 1;
    return 42;
}

function outer() {
    $ret = yield from inner();
    yield $ret;
}

foreach (outer() as $v) { echo $v . " "; }
// Prints: 1 42

The delegated return value can also become the outer generator’s terminal return value directly:

<?php
function outer() {
    return yield from inner();
}

Generator closures

Anonymous functions that contain yield also return Generator objects. Captured scalar locals are copied into the generator frame just like ordinary closure captures:

<?php
$start = 7;
$gen = function() use ($start) {
    yield $start;
    yield $start + 1;
};

foreach ($gen() as $v) { echo $v . " "; }
// Prints: 7 8

return value and Generator::getReturn()

A generator body may end with return <expr>; to stash a final value (distinct from yielded values) that the caller retrieves with Generator::getReturn() after iteration completes:

<?php
function gen() {
    yield 1;
    yield 2;
    return 42;
}

$g = gen();
foreach ($g as $v) { echo $v . " "; }
echo "ret=" . $g->getReturn();
// Prints: 1 2 ret=42

A bare return; (no value) terminates the generator without writing a return value; getReturn() then surfaces the slot’s initial null/0.

Generator::throw

$g->throw($exc) resumes the generator and raises $exc at the suspended yield, exactly like PHP. If the generator body has a surrounding try/catch, that handler runs and execution continues; the value of throw() is the next yielded value. If the exception is not caught inside the body, it propagates to the caller as if the generator had thrown it.

<?php
function gen() {
    try {
        yield 1;
    } catch (Exception $e) {
        echo "caught inside: " . $e->getMessage() . " ";
    }
    yield 2;
}

$g = gen();
echo $g->current() . " ";              // 1
echo $g->throw(new Exception("boom")); // caught inside: boom 2
// Prints: 1 caught inside: boom 2

When the body has no handler, the exception escapes to the caller’s nearest active handler:

<?php
function gen() {
    yield 1;
    yield 2;
}

try {
    $g = gen();
    $g->rewind();
    echo $g->current() . " ";   // 1
    $g->throw(new Exception("boom"));
    echo "unreachable";
} catch (Exception $e) {
    echo "caught: " . $e->getMessage();
}
// Prints: 1 caught: boom

Locals and control flow inside generator bodies

Generator bodies in elephc can contain ordinary local variables, arithmetic, and the usual control-flow constructs. Locals declared inside the generator survive across yield points because the body’s real stack frame is suspended and resumed in place on the coroutine stack.

<?php
function fib(int $count) {
    $a = 0;
    $b = 1;
    $i = 0;
    while ($i < $count) {
        yield $a;
        $c = $a + $b;
        $a = $b;
        $b = $c;
        $i++;
    }
}

foreach (fib(10) as $v) { echo $v . " "; }
// Prints: 0 1 1 2 3 5 8 13 21 34

Because the body is compiled like an ordinary function, every control-flow construct and operator the language supports is available inside a generator body — if/else/elseif, while, do-while, for, foreach, switch, break, continue, try/catch/finally, and arbitrary nesting — with the same semantics they have outside a generator.

Calling user functions from a generator body

Generator bodies use the ordinary EIR call path, so they can invoke user functions and methods with the same argument and return-value support as code outside a generator. The generator-specific seven-value limit applies only to the generator function’s own parameters and closure captures, which must fit the coroutine object’s start_args slots; it does not limit calls made by the body:

<?php
function helper(int $x): int { return $x * 2; }

function gen() {
    $i = 1;
    while ($i < 5) {
        yield helper($i) + 10;
        $i++;
    }
}

foreach (gen() as $v) { echo $v . " "; }
// Prints: 12 14 16 18

Generator::send for coroutine-style flow

yield is also an expression: assigning its result to a variable lets the caller pump values into the generator via Generator::send. The sent value becomes the result of the in-progress yield expression.

<?php
function echoer() {
    $a = yield 1;     // first yield: $a starts as null until send()
    $b = yield $a;    // yields whatever was sent in
    yield $b;
}

$g = echoer();
$g->rewind();              // runs to first yield → current() = 1
echo $g->current();        // 1
$g->send(100);             // resumes with $a = 100, runs to next yield
echo $g->current();        // 100
$g->send(200);             // resumes with $b = 200
echo $g->current();        // 200

If the generator is resumed via next() instead of send(), the in-progress yield expression evaluates to 0 for an int-typed LHS local. For Mixed-typed LHS locals (e.g. $x = yield $prompt; where $x was previously assigned a string or array), next() leaves the slot at its previous value while send($v) transfers the boxed Mixed pointer into the slot:

<?php
function chat() {
    $x = "init";              // $x is Mixed-typed
    $x = yield "first";        // $x ← whatever was sent (Mixed)
    yield $x;
    $x = yield "second";
    yield $x;
}

$g = chat();
$g->rewind();
echo $g->current() . " ";     // "first"
$g->send("alpha");
echo $g->current() . " ";     // "alpha" — string round-tripped
$g->send("beta");
echo $g->current() . " ";     // "second"
$g->send("gamma");
echo $g->current();           // "gamma"

foreach over arbitrary Iterator and IteratorAggregate objects

foreach accepts any object that implements the built-in Iterator interface (current, key, next, valid, rewind) or IteratorAggregate (getIterator(): Traversable). Generators are one such producer; user classes can implement either protocol:

<?php
class Range implements Iterator {
    private int $current;
    private int $end;
    public function __construct(int $start, int $end) {
        $this->current = $start;
        $this->end = $end;
    }
    public function rewind(): void {}
    public function valid(): bool { return $this->current < $this->end; }
    public function current(): mixed { return $this->current; }
    public function key(): mixed { return $this->current; }
    public function next(): void { $this->current = $this->current + 1; }
}

foreach (new Range(0, 5) as $i) { echo $i; }
// Prints: 01234

The loop calls rewind() once, then on each iteration: valid() to test continuation, current() and key() to bind the loop variables, and next() after the body. Method dispatch goes through the regular vtable.

When foreach is used on an IteratorAggregate, the codegen calls getIterator() once before the loop and uses the returned object’s class for the per-iteration dispatches:

<?php
class AggregateRange implements IteratorAggregate {
    public function getIterator(): Range { return new Range(0, 3); }
}

foreach (new AggregateRange() as $v) { echo $v; }
// Prints: 012

Control flow inside generator bodies

A generator body runs on its own coroutine stack and is compiled by the same backend as an ordinary function, so it supports the full language: arbitrary control flow (if/while/for/foreach/switch), try/catch/finally around yield, method calls, return values, and locals of any type — all preserved across yield points because the body’s real stack frame is suspended and resumed in place.

<?php
function items() {
    foreach (["a" => 1, "b" => 2] as $k => $v) {
        try {
            yield $k => $v;
        } catch (Exception $e) {
            echo "handled: " . $e->getMessage() . " ";
        }
    }
}

How it works at runtime

A Generator is a stackful coroutine that reuses the Fiber runtime: the object reuses the Fiber memory layout (its own mmap’d stack plus the exception-handler and transfer/pending fields) and a small block of generator-specific fields (last_key, last_value, return_value, auto_key). Each generator function f produces three target-specific symbols:

  • _fn_<f> — the constructor at the public symbol. It allocates the Generator coroutine object (__rt_fiber_construct), boxes the call arguments and closure captures into the object’s start_args slots, and returns the object. The body does not run yet — it starts lazily on the first accessor. The start_args storage has room for 7 boxed values, so a generator may declare at most 7 parameters (counting closure captures); a generator with more is rejected at compile time.
  • _fn_<f>__genbody — the body, compiled by the normal backend as a Mixed-returning function. Its return value is what getReturn() surfaces.
  • _fn_<f>__gencb — the coroutine entry wrapper. The fiber entry trampoline runs it on the coroutine stack; it unboxes start_args back into the body’s parameter registers, runs the body, and parks the body’s return value in the return_value slot.

Each yield lowers to __rt_gen_suspend(key, value), which applies the auto-key bookkeeping (a NULL key consumes the counter; an explicit integer key above the largest one used pushes it to key + 1), records the boxed key/value into the generator’s last_key/last_value slots, and then suspends the coroutine. Because the suspend boundary re-raises a scheduled exception inside the coroutine’s own stack, Generator::throw() lands in a try/catch inside the body. yield from <generator> is driven by __rt_gen_delegate, which forwards sent values into the inner generator and returns its getReturn(); yield from <array> is desugared into an iterator loop that re-yields each entry. Both enter the suspend primitive at __rt_gen_suspend_delegated, which stores the forwarded key without touching the counter.

The synthetic Generator class has no PHP body — its method dispatch is intercepted in the codegen and routed directly to the __rt_gen_* runtime helpers (current, key, valid, next, send, rewind, throw, getReturn), which drive the coroutine through the shared Fiber primitives.