[php-src] PHP-8.4: Fix GH-23301: nested "yield from" repeats a value after "yield from []" (#23302)

Lazizbek Ergashev via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Lazizbek Ergashev (lazerg)
Committer: GitHub (web-flow)
Pusher: arnaud-lb
Date: 2026-08-20T10:28:54+02:00

Commit: https://github.com/php/php-src/commit/7b34e84bdb45e3856c5a47ae975ac901eeabf250
Raw diff: https://github.com/php/php-src/commit/7b34e84bdb45e3856c5a47ae975ac901eeabf250.diff

Fix GH-23301: nested "yield from" repeats a value after "yield from []" (#23302)

The ZEND_GENERATOR_DO_INIT flag is added to a delegator to indicate that its next resume shouldn't advance an already initialized delegatee. The flag must be cleared on that next resume, but this was done on the wrong generator in some cases.

Fixes GH-23301.

Co-Authored-By: Arnaud Le Blanc <[email protected]>

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


Diff:

diff --git a/NEWS b/NEWS
index e273a86dfca6..a672360f0aef 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,8 @@ PHP                                                                        NEWS
 - Core:
   . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
     next() call on the inner generator). (iliaal)
+  . Fixed bug GH-23301 (Nested "yield from" yields a value twice when the
+    middle generator delegates again). (Lazizbek Ergashev)
 
 - DOM:
   . Fixed a use-after-free when cloning a DOMNameSpaceNode after
diff --git a/Zend/tests/generators/backtrace_multi_yield_from.phpt b/Zend/tests/generators/backtrace_multi_yield_from.phpt
index 4fbaa2f05bb4..2cb898b3aac2 100644
--- a/Zend/tests/generators/backtrace_multi_yield_from.phpt
+++ b/Zend/tests/generators/backtrace_multi_yield_from.phpt
@@ -25,8 +25,8 @@ var_dump($gen2->current());
 ?>
 --EXPECTF--
 int(1)
-int(1)
 #0 %s(10): gen()
 #1 [internal function]: from(Object(Generator))
-#2 %s(19): Generator->next()
+#2 %s(17): Generator->next()
 int(2)
+NULL
diff --git a/Zend/tests/generators/gh15375.phpt b/Zend/tests/generators/gh15375.phpt
index 48919083f227..f0a2fe56c16c 100644
--- a/Zend/tests/generators/gh15375.phpt
+++ b/Zend/tests/generators/gh15375.phpt
@@ -46,9 +46,9 @@ 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).
+// Reading a pre-primed generator through two nested "yield from" levels primes
+// the whole chain but advances nothing. A later next() on a middle level then
+// advances the shared generator, like any other next() would.
 echo "shared primed:\n";
 function counter() {
     yield 1;
@@ -58,11 +58,11 @@ $gen1 = counter();
 $gen1->valid();
 $gen2 = outer($gen1);
 $gen3 = outer($gen2);
-echo "gen3 current: ", $gen3->current(), "\n";
+var_dump($gen3->current());
 $gen2->next();
-echo "gen2 current: ", $gen2->current(), "\n";
+var_dump($gen2->current());
 $gen2->next();
-echo "gen2 current: ", $gen2->current(), "\n";
+var_dump($gen2->current());
 
 ?>
 --EXPECT--
@@ -84,6 +84,6 @@ six
 eight
 nine
 shared primed:
-gen3 current: 1
-gen2 current: 1
-gen2 current: 2
+int(1)
+int(2)
+NULL
diff --git a/Zend/tests/generators/gh23301.phpt b/Zend/tests/generators/gh23301.phpt
new file mode 100644
index 000000000000..02aaf2fe2022
--- /dev/null
+++ b/Zend/tests/generators/gh23301.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-23301 (Nested "yield from" yields a value twice when the middle generator delegates again)
+--FILE--
+<?php
+
+function inner() {
+    yield "B";
+}
+
+function middle() {
+    yield "A";
+    yield from inner();
+    yield "C";
+    yield from ["D"];
+}
+
+function delegate($gen) {
+    yield from $gen;
+}
+
+foreach (delegate(middle()) as $value) {
+    echo $value, "\n";
+}
+
+function tail($inner) {
+    yield from $inner;
+    yield from ["E"];
+}
+
+$middle = tail(inner());
+$outer = delegate($middle);
+
+var_dump($outer->current());
+$middle->next();
+var_dump($middle->current());
+
+?>
+--EXPECT--
+A
+B
+C
+D
+string(1) "B"
+string(1) "E"
diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c
index 83815773b912..ef645f8f9a3a 100644
--- a/Zend/zend_generators.c
+++ b/Zend/zend_generators.c
@@ -776,10 +776,12 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
 		return;
 	}
 
-	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;
+	if (UNEXPECTED((delegator->flags & ZEND_GENERATOR_DO_INIT) != 0)) {
+		delegator->flags &= ~ZEND_GENERATOR_DO_INIT;
+		if (UNEXPECTED(!Z_ISUNDEF(generator->value))) {
+			/* We must not advance an already initialized delegate on first resumption */
+			return;
+		}
 	}
 
 	if (EG(active_fiber)) {
@@ -819,7 +821,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
 			EG(current_execute_data) = original_execute_data;
 			EG(jit_trace_num) = original_jit_trace_num;
 
-			orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER);
+			orig_generator->flags &= ~ZEND_GENERATOR_IN_FIBER;
 			generator->flags &= ~(ZEND_GENERATOR_CURRENTLY_RUNNING | ZEND_GENERATOR_IN_FIBER);
 			return;
 		}
@@ -882,7 +884,6 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
 		} else {
 			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;
 		}
@@ -900,7 +901,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
 		goto try_again;
 	}
 
-	orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER);
+	orig_generator->flags &= ~ZEND_GENERATOR_IN_FIBER;
 }
 /* }}} */
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.