[php-src] Issue #23301: Nested "yield from" yields a value twice when the middle generator ends with "yield from []"

[email protected] (kojiromike)
Newsgroups php.bugs
Message-ID <Tys9bTLrAoR2twqFUsDVDJqtoosRBOyyG5HVWQ25wIM@main.internal.php.net>
Issue: https://github.com/php/php-src/issues/23301
Author: kojiromike

### Description

Commit 0ccff767634b4c453f2971d256de86cc433c37fe ("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
<?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:

```
string(4) "ABCC"
```

But I expected this output instead:

```
string(3) "ABC"
```

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 0ccff7676 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:

```c
	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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.