Skip to content

Commit 0ccff76

Browse files
committed
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
1 parent 257999b commit 0ccff76

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.26
44

5+
- Core:
6+
. Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
7+
next() call on the inner generator). (iliaal)
58

69
27 Aug 2026, PHP 8.4.25
710

Zend/tests/generators/gh15375.phpt

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
--TEST--
2+
GH-15375 (Nested "yield from" skips items after valid()/next() on the inner generator)
3+
--FILE--
4+
<?php
5+
6+
function arrayProvider() {
7+
yield ['one', 'two', 'three'];
8+
yield ['four', 'five', 'six'];
9+
yield ['seven', 'eight', 'nine'];
10+
}
11+
12+
function iterateValues(array $array) {
13+
foreach ($array as $value) {
14+
yield $value;
15+
}
16+
}
17+
18+
function withValid() {
19+
foreach (arrayProvider() as $array) {
20+
$iterator = iterateValues($array);
21+
if ($iterator->valid()) {
22+
yield from $iterator;
23+
}
24+
}
25+
}
26+
27+
function withNext() {
28+
foreach (arrayProvider() as $array) {
29+
$iterator = iterateValues($array);
30+
$iterator->next();
31+
yield from $iterator;
32+
}
33+
}
34+
35+
function outer(Generator $inner) {
36+
yield from $inner;
37+
}
38+
39+
echo "valid():\n";
40+
foreach (outer(withValid()) as $s) {
41+
echo $s, "\n";
42+
}
43+
44+
echo "next():\n";
45+
foreach (outer(withNext()) as $s) {
46+
echo $s, "\n";
47+
}
48+
49+
// A shared, pre-primed generator consumed through two nested "yield from"
50+
// levels must still present its current value once to each consumer (the fix
51+
// must not over-clear the middle level's first-touch).
52+
echo "shared primed:\n";
53+
function counter() {
54+
yield 1;
55+
yield 2;
56+
}
57+
$gen1 = counter();
58+
$gen1->valid();
59+
$gen2 = outer($gen1);
60+
$gen3 = outer($gen2);
61+
echo "gen3 current: ", $gen3->current(), "\n";
62+
$gen2->next();
63+
echo "gen2 current: ", $gen2->current(), "\n";
64+
$gen2->next();
65+
echo "gen2 current: ", $gen2->current(), "\n";
66+
67+
?>
68+
--EXPECT--
69+
valid():
70+
one
71+
two
72+
three
73+
four
74+
five
75+
six
76+
seven
77+
eight
78+
nine
79+
next():
80+
two
81+
three
82+
five
83+
six
84+
eight
85+
nine
86+
shared primed:
87+
gen3 current: 1
88+
gen2 current: 1
89+
gen2 current: 2

Zend/zend_generators.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,13 +768,15 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
768768
return;
769769
}
770770

771+
zend_generator *delegator = orig_generator;
772+
771773
try_again:
772774
if (generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING) {
773775
zend_throw_error(NULL, "Cannot resume an already running generator");
774776
return;
775777
}
776778

777-
if (UNEXPECTED((orig_generator->flags & ZEND_GENERATOR_DO_INIT) != 0 && !Z_ISUNDEF(generator->value))) {
779+
if (UNEXPECTED((delegator->flags & ZEND_GENERATOR_DO_INIT) != 0 && !Z_ISUNDEF(generator->value))) {
778780
/* We must not advance Generator if we yield from a Generator being currently run */
779781
orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
780782
return;
@@ -881,12 +883,19 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
881883
generator = zend_generator_get_current(orig_generator);
882884
zend_generator_throw_exception(generator, NULL);
883885
orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
886+
delegator = orig_generator;
884887
goto try_again;
885888
}
886889
}
887890

888891
/* yield from was used, try another resume. */
889-
if (UNEXPECTED((generator != orig_generator && !Z_ISUNDEF(generator->retval)) || (generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM))) {
892+
if (UNEXPECTED(generator->execute_data && generator->execute_data->opline->opcode == ZEND_YIELD_FROM)) {
893+
delegator = generator;
894+
generator = zend_generator_get_current(orig_generator);
895+
goto try_again;
896+
}
897+
if (UNEXPECTED(generator != orig_generator && !Z_ISUNDEF(generator->retval))) {
898+
delegator = orig_generator;
890899
generator = zend_generator_get_current(orig_generator);
891900
goto try_again;
892901
}

0 commit comments

Comments
 (0)