[php-src] PHP-8.5: Fix GH-15375: nested "yield from" skips items after valid()/next()

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-12T06:30:05-04:00

Commit: https://github.com/php/php-src/commit/0ccff767634b4c453f2971d256de86cc433c37fe
Raw diff: https://github.com/php/php-src/commit/0ccff767634b4c453f2971d256de86cc433c37fe.diff

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

Changed paths:
  A  Zend/tests/generators/gh15375.phpt
  M  NEWS
  M  Zend/zend_generators.c


Diff:

diff --git a/NEWS b/NEWS
index 14149cee9c60..ffefe0d338ec 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 000000000000..48919083f227
--- /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 f7bcf01881dc..83815773b912 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;
 	}
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.