diff --git a/NEWS b/NEWS index cdd6a48a4874..1cdc0375cb5a 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with INT_MAX year). (arshidkv12) +- Core: + . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or + next() call on the inner generator). (iliaal) + - DBA: . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz) 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-- +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; }