Description
Commit 0ccff76 ("Fix GH-15375: nested yield from skips items after valid()/next()", merged to PHP-8.4/PHP-8.5/master on 2026-08-12) introduces a regression: in a three-level yield from delegation chain, a value is yielded twice when the middle generator ends with yield from [] (delegation to a non-generator empty iterable).
This is not a synthetic pattern. Twig compiles every template's doDisplay() to end with yield from [];, and rendering any template containing a block/include goes through exactly this three-level chain, so every affected template duplicates all output emitted after the block.
The following code:
<?php
function inner(): iterable
{
yield "B";
}
function mid(): iterable
{
yield "A";
yield from inner();
yield "C";
yield from [];
}
function top(): iterable
{
yield from mid();
}
$out = '';
foreach (top() as $chunk) {
$out .= $chunk;
}
var_dump($out);
Resulted in this output:
But I expected this output instead:
The duplicated value is whatever the middle generator yielded last before the trailing yield from []; with two tail yields (yield "C"; yield "D"; yield from [];) the result is ABCDD.
Narrowing:
| Shape |
Result |
3 levels, middle ends yield from [] |
BROKEN |
| 3 levels, no trailing empty delegate |
OK |
3 levels, middle ends yield from <empty generator> |
OK |
| 3 levels, inner is an array literal rather than a generator |
OK |
| 2 levels (no outer delegation) |
OK |
So it requires: an outer generator delegating to a middle generator, the middle delegating to an inner generator, and the middle then delegating to an empty non-generator iterable.
Bisected by reverting only the Zend/zend_generators.c hunks of 0ccff76 on top of master 5332ab0c291361abe3d12db21dd55e5dc7baa829 — that alone restores correct output, and also restores a full Twig render-test suite (22 tests) that fails 15 cases without it.
The relevant change is in zend_generator_resume(), where the DO_INIT re-advance guard now reads the flag from the delegating generator:
if (UNEXPECTED(generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM)) {
delegator = generator;
generator = zend_generator_get_current(orig_generator);
goto try_again;
}
When the middle generator sits on a ZEND_YIELD_FROM opline for an empty array, delegator becomes the middle generator, whose DO_INIT is clear, so the guard that previously stopped the re-advance (via orig_generator) no longer fires and the already-yielded value is presented again.
PHP Version
PHP 8.6.0-dev (built from master 5332ab0c291361abe3d12db21dd55e5dc7baa829)
Also present on the PHP-8.4 and PHP-8.5 branches, since the fix was merged to all three — i.e. this is heading for 8.4.26 and 8.5.11.
Operating System
Debian bookworm (also reproduced on GitHub Actions ubuntu-24.04 with the shivammathur/setup-php 8.6 nightly)
Description
Commit 0ccff76 ("Fix GH-15375: nested
yield fromskips items after valid()/next()", merged to PHP-8.4/PHP-8.5/master on 2026-08-12) introduces a regression: in a three-levelyield fromdelegation chain, a value is yielded twice when the middle generator ends withyield from [](delegation to a non-generator empty iterable).This is not a synthetic pattern. Twig compiles every template's
doDisplay()to end withyield from [];, and rendering any template containing a block/include goes through exactly this three-level chain, so every affected template duplicates all output emitted after the block.The following code:
Resulted in this output:
But I expected this output instead:
The duplicated value is whatever the middle generator yielded last before the trailing
yield from []; with two tail yields (yield "C"; yield "D"; yield from [];) the result isABCDD.Narrowing:
yield from []yield from <empty generator>So it requires: an outer generator delegating to a middle generator, the middle delegating to an inner generator, and the middle then delegating to an empty non-generator iterable.
Bisected by reverting only the
Zend/zend_generators.chunks of 0ccff76 on top of master5332ab0c291361abe3d12db21dd55e5dc7baa829— that alone restores correct output, and also restores a full Twig render-test suite (22 tests) that fails 15 cases without it.The relevant change is in
zend_generator_resume(), where theDO_INITre-advance guard now reads the flag from the delegating generator:When the middle generator sits on a
ZEND_YIELD_FROMopline for an empty array,delegatorbecomes the middle generator, whoseDO_INITis clear, so the guard that previously stopped the re-advance (viaorig_generator) no longer fires and the already-yielded value is presented again.PHP Version
Also present on the PHP-8.4 and PHP-8.5 branches, since the fix was merged to all three — i.e. this is heading for 8.4.26 and 8.5.11.
Operating System
Debian bookworm (also reproduced on GitHub Actions ubuntu-24.04 with the shivammathur/setup-php 8.6 nightly)