From 622159ae6482dd4ca1ab0cf1d651cf6a2ee551ee Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Sun, 16 Aug 2026 02:24:50 +0500 Subject: [PATCH 1/4] Fix GH-23301: nested "yield from" repeats a value after "yield from []" --- NEWS | 2 ++ Zend/tests/generators/gh23301.phpt | 29 +++++++++++++++++++++++++++++ Zend/zend_generators.c | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/generators/gh23301.phpt diff --git a/NEWS b/NEWS index ecb4d105e917..6f02804e00de 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 ends with "yield from []"). (Lazizbek Ergashev) - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/Zend/tests/generators/gh23301.phpt b/Zend/tests/generators/gh23301.phpt new file mode 100644 index 000000000000..dd71cf5f0ffc --- /dev/null +++ b/Zend/tests/generators/gh23301.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-23301 (Nested "yield from" yields a value twice when the middle generator ends with "yield from []") +--FILE-- + +--EXPECT-- +A +B +C diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c index 83815773b912..25669f8e54d5 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -890,7 +890,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */ /* yield from was used, try another resume. */ if (UNEXPECTED(generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM)) { - delegator = generator; + delegator = generator->node.parent ? generator : orig_generator; generator = zend_generator_get_current(orig_generator); goto try_again; } From a03afc659315220c80e1593dccd14af6bbb5173a Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 19 Aug 2026 17:08:24 +0500 Subject: [PATCH 2/4] Fix GH-23301: clear ZEND_GENERATOR_DO_INIT on the delegating generator --- NEWS | 2 +- .../backtrace_multi_yield_from.phpt | 4 +-- Zend/tests/generators/gh23301.phpt | 25 +++++++++++++++---- Zend/zend_generators.c | 6 ++++- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 6f02804e00de..ee3bebda09ee 100644 --- a/NEWS +++ b/NEWS @@ -6,7 +6,7 @@ PHP NEWS . 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 ends with "yield from []"). (Lazizbek Ergashev) + middle generator delegates again). (Lazizbek Ergashev) - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) 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/gh23301.phpt b/Zend/tests/generators/gh23301.phpt index dd71cf5f0ffc..02aaf2fe2022 100644 --- a/Zend/tests/generators/gh23301.phpt +++ b/Zend/tests/generators/gh23301.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-23301 (Nested "yield from" yields a value twice when the middle generator ends with "yield from []") +GH-23301 (Nested "yield from" yields a value twice when the middle generator delegates again) --FILE-- 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 25669f8e54d5..b9065b2f0a90 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -782,6 +782,10 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */ return; } + /* The flag applies to this resume only: if it stays set on a delegating + * generator other than orig_generator, it suppresses a later resume of it */ + delegator->flags &= ~ZEND_GENERATOR_DO_INIT; + if (EG(active_fiber)) { orig_generator->flags |= ZEND_GENERATOR_IN_FIBER; generator->flags |= ZEND_GENERATOR_IN_FIBER; @@ -890,7 +894,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */ /* yield from was used, try another resume. */ if (UNEXPECTED(generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM)) { - delegator = generator->node.parent ? generator : orig_generator; + delegator = generator; generator = zend_generator_get_current(orig_generator); goto try_again; } From e380159f11b13aac5913a4aed07c20e1144856ac Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 19 Aug 2026 19:06:27 +0500 Subject: [PATCH 3/4] Remove redundant ZEND_GENERATOR_DO_INIT clears in zend_generator_resume On entry delegator == orig_generator, so the guard block at the top of try_again always clears orig_generator's ZEND_GENERATOR_DO_INIT before anything advances: on the taken branch through orig_generator, on the other through delegator. The three later clears cannot observe the flag set, so drop them. --- Zend/zend_generators.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c index b9065b2f0a90..f17c5353cedb 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -823,7 +823,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; } @@ -886,7 +886,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; } @@ -904,7 +903,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; } /* }}} */ From b1f74a692ad2a588533dc778c06aa79f8c6094bf Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 19 Aug 2026 20:39:08 +0500 Subject: [PATCH 4/4] Clear ZEND_GENERATOR_DO_INIT on the delegator before advancing Check and clear the flag on the delegator in one place, then decide whether to advance. Reading a chain of "yield from" delegations primes every level but advances nothing, and a later next() on any level advances the shared generator. Before this, only the delegator sitting directly above an already primed Generator held its value back. An array delegate, an unprimed Generator, and any outer level all advanced instead. The shared primed case in gh15375.phpt asserted the old behaviour, so it is updated. Suggested by arnaud-lb, semantics confirmed by bwoebi. --- Zend/tests/generators/gh15375.phpt | 18 +++++++++--------- Zend/zend_generators.c | 14 ++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) 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/zend_generators.c b/Zend/zend_generators.c index f17c5353cedb..ef645f8f9a3a 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -776,16 +776,14 @@ 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; + } } - /* The flag applies to this resume only: if it stays set on a delegating - * generator other than orig_generator, it suppresses a later resume of it */ - delegator->flags &= ~ZEND_GENERATOR_DO_INIT; - if (EG(active_fiber)) { orig_generator->flags |= ZEND_GENERATOR_IN_FIBER; generator->flags |= ZEND_GENERATOR_IN_FIBER;