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;
 	}