Commit 0ccff767634 for php.net
commit 0ccff767634b4c453f2971d256de86cc433c37fe
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Jul 8 07:52:37 2026 -0400
Fix GH-15375: nested "yield from" skips items after valid()/next()
The DO_INIT guard in zend_generator_resume() that avoids re-advancing an
already-primed delegate read the flag on orig_generator, but in a nested
"yield from" chain the fresh delegation is established by a middle generator
rather than the outermost consumer. Track the delegating generator through
the resume loop and check its DO_INIT, so a primed inner generator reached
through two "yield from" levels yields its current value once instead of
skipping it.
Fixes GH-15375
Closes GH-22640
diff --git a/NEWS b/NEWS
index 14149cee9c6..ffefe0d338e 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,9 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.4.26
+- Core:
+ . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
+ next() call on the inner generator). (iliaal)
27 Aug 2026, PHP 8.4.25
diff --git a/Zend/tests/generators/gh15375.phpt b/Zend/tests/generators/gh15375.phpt
new file mode 100644
index 00000000000..48919083f22
--- /dev/null
+++ b/Zend/tests/generators/gh15375.phpt
@@ -0,0 +1,89 @@
+--TEST--
+GH-15375 (Nested "yield from" skips items after valid()/next() on the inner generator)
+--FILE--
+<?php
+
+function arrayProvider() {
+ yield ['one', 'two', 'three'];
+ yield ['four', 'five', 'six'];
+ yield ['seven', 'eight', 'nine'];
+}
+
+function iterateValues(array $array) {
+ foreach ($array as $value) {
+ yield $value;
+ }
+}
+
+function withValid() {
+ foreach (arrayProvider() as $array) {
+ $iterator = iterateValues($array);
+ if ($iterator->valid()) {
+ yield from $iterator;
+ }
+ }
+}
+
+function withNext() {
+ foreach (arrayProvider() as $array) {
+ $iterator = iterateValues($array);
+ $iterator->next();
+ yield from $iterator;
+ }
+}
+
+function outer(Generator $inner) {
+ yield from $inner;
+}
+
+echo "valid():\n";
+foreach (outer(withValid()) as $s) {
+ echo $s, "\n";
+}
+
+echo "next():\n";
+foreach (outer(withNext()) as $s) {
+ echo $s, "\n";
+}
+
+// A shared, pre-primed generator consumed through two nested "yield from"
+// levels must still present its current value once to each consumer (the fix
+// must not over-clear the middle level's first-touch).
+echo "shared primed:\n";
+function counter() {
+ yield 1;
+ yield 2;
+}
+$gen1 = counter();
+$gen1->valid();
+$gen2 = outer($gen1);
+$gen3 = outer($gen2);
+echo "gen3 current: ", $gen3->current(), "\n";
+$gen2->next();
+echo "gen2 current: ", $gen2->current(), "\n";
+$gen2->next();
+echo "gen2 current: ", $gen2->current(), "\n";
+
+?>
+--EXPECT--
+valid():
+one
+two
+three
+four
+five
+six
+seven
+eight
+nine
+next():
+two
+three
+five
+six
+eight
+nine
+shared primed:
+gen3 current: 1
+gen2 current: 1
+gen2 current: 2
diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c
index f7bcf01881d..83815773b91 100644
--- a/Zend/zend_generators.c
+++ b/Zend/zend_generators.c
@@ -768,13 +768,15 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
return;
}
+ zend_generator *delegator = orig_generator;
+
try_again:
if (generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING) {
zend_throw_error(NULL, "Cannot resume an already running generator");
return;
}
- if (UNEXPECTED((orig_generator->flags & ZEND_GENERATOR_DO_INIT) != 0 && !Z_ISUNDEF(generator->value))) {
+ if (UNEXPECTED((delegator->flags & ZEND_GENERATOR_DO_INIT) != 0 && !Z_ISUNDEF(generator->value))) {
/* We must not advance Generator if we yield from a Generator being currently run */
orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
return;
@@ -881,12 +883,19 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
generator = zend_generator_get_current(orig_generator);
zend_generator_throw_exception(generator, NULL);
orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
+ delegator = orig_generator;
goto try_again;
}
}
/* yield from was used, try another resume. */
- if (UNEXPECTED((generator != orig_generator && !Z_ISUNDEF(generator->retval)) || (generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM))) {
+ 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;
+ }
+ if (UNEXPECTED(generator != orig_generator && !Z_ISUNDEF(generator->retval))) {
+ delegator = orig_generator;
generator = zend_generator_get_current(orig_generator);
goto try_again;
}