feat(generator): enhance FiberGenerator implementation with improved error handling and type support
- Add proper exception rethrow mechanism using typephp_fiber_rethrow in compiler - Implement comprehensive generator return type validation supporting UnionType, IntersectionType, and nullable types - Add support for constructor property promotion in generator functions - Enhance generator destructor handling with proper finally block execution - Implement proper exception boundary crossing between Fiber and generator contexts - Add comprehensive test coverage for generator lifecycle, yielding, and error scenarios - Update documentation to reflect new supported return types including object and mixed - Fix parameter validation and type checking integration within generator functions - Add proper handling of yield from delegation with throw and return value preservation - Implement automatic key tracking for generator yield operations - Add validation for recursive IteratorAggregate cycles in yield from operationspull/16/head
parent
d251331ae4
commit
7faec8f459
14 changed files with 400 additions and 8 deletions
@ -0,0 +1,24 @@ |
|||||||
|
--TEST-- |
||||||
|
throw on a closed generator rethrows without leaking the exception |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function closed_generator(): iterable |
||||||
|
{ |
||||||
|
if (false) { |
||||||
|
yield 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = closed_generator(); |
||||||
|
$generator->valid(); |
||||||
|
try { |
||||||
|
$generator->throw(new RuntimeException('closed')); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), ': ', $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
RuntimeException: closed |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
--TEST-- |
||||||
|
constructor property promotion runs even when the constructor contains yield |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class PromotedGeneratorConstructor |
||||||
|
{ |
||||||
|
public function __construct(public int $value) |
||||||
|
{ |
||||||
|
if (false) { |
||||||
|
yield 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new PromotedGeneratorConstructor(42); |
||||||
|
var_dump($object->value); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(42) |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
--TEST-- |
||||||
|
suspended generator destruction closes its Fiber without leaking |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function generator_with_finally(): iterable |
||||||
|
{ |
||||||
|
try { |
||||||
|
yield 1; |
||||||
|
yield 2; |
||||||
|
} finally { |
||||||
|
echo "finally\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = generator_with_finally(); |
||||||
|
var_dump($generator->current()); |
||||||
|
unset($generator); |
||||||
|
gc_collect_cycles(); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
finally |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
--TEST-- |
||||||
|
generator send preserves refcounted values across a Fiber suspension |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function receive_values(): iterable |
||||||
|
{ |
||||||
|
$string = yield 'string'; |
||||||
|
var_dump($string); |
||||||
|
$array = yield 'array'; |
||||||
|
var_dump($array); |
||||||
|
$object = yield 'object'; |
||||||
|
var_dump($object->value); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = receive_values(); |
||||||
|
var_dump($generator->current()); |
||||||
|
var_dump($generator->send(str_repeat('x', 32))); |
||||||
|
var_dump($generator->send(['key' => str_repeat('y', 16)])); |
||||||
|
$object = new stdClass(); |
||||||
|
$object->value = 42; |
||||||
|
var_dump($generator->send($object)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "string" |
||||||
|
string(32) "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
||||||
|
string(5) "array" |
||||||
|
array(1) { |
||||||
|
["key"]=> |
||||||
|
string(16) "yyyyyyyyyyyyyyyy" |
||||||
|
} |
||||||
|
string(6) "object" |
||||||
|
int(42) |
||||||
|
NULL |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
--TEST-- |
||||||
|
generator lifecycle methods follow Zend Generator semantics |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function lifecycle_generator(): iterable |
||||||
|
{ |
||||||
|
try { |
||||||
|
yield 'first' => 1; |
||||||
|
yield 'second' => 2; |
||||||
|
} catch (Exception $e) { |
||||||
|
yield 'caught' => $e->getMessage(); |
||||||
|
} |
||||||
|
return 9; |
||||||
|
} |
||||||
|
|
||||||
|
function empty_generator(): iterable |
||||||
|
{ |
||||||
|
if (false) { |
||||||
|
yield 1; |
||||||
|
} |
||||||
|
return 7; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$next = lifecycle_generator(); |
||||||
|
$next->next(); |
||||||
|
var_dump($next->key(), $next->current()); |
||||||
|
try { |
||||||
|
$next->rewind(); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), ': ', $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
while ($next->valid()) { |
||||||
|
$next->next(); |
||||||
|
} |
||||||
|
|
||||||
|
$throw = lifecycle_generator(); |
||||||
|
var_dump($throw->throw(new Exception('injected'))); |
||||||
|
var_dump($throw->key()); |
||||||
|
|
||||||
|
while ($throw->valid()) { |
||||||
|
$throw->next(); |
||||||
|
} |
||||||
|
$throw->next(); |
||||||
|
var_dump($throw->send('ignored')); |
||||||
|
var_dump($throw->getReturn()); |
||||||
|
|
||||||
|
$empty = empty_generator(); |
||||||
|
var_dump($empty->send('ignored')); |
||||||
|
$empty->next(); |
||||||
|
var_dump($empty->getReturn()); |
||||||
|
|
||||||
|
try { |
||||||
|
$empty->throw(new RuntimeException('closed')); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), ': ', $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "second" |
||||||
|
int(2) |
||||||
|
Exception: Cannot rewind a generator that was already run |
||||||
|
string(8) "injected" |
||||||
|
string(6) "caught" |
||||||
|
NULL |
||||||
|
int(9) |
||||||
|
NULL |
||||||
|
int(7) |
||||||
|
RuntimeException: closed |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
--TEST-- |
||||||
|
uncaught generator exceptions cross the Fiber boundary safely |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function failing_generator(): iterable |
||||||
|
{ |
||||||
|
yield 1; |
||||||
|
throw new RuntimeException('generator failed'); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = failing_generator(); |
||||||
|
var_dump($generator->current()); |
||||||
|
try { |
||||||
|
$generator->next(); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), ': ', $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
var_dump($generator->valid()); |
||||||
|
try { |
||||||
|
$generator->getReturn(); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), ': ', $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
RuntimeException: generator failed |
||||||
|
bool(false) |
||||||
|
Exception: Cannot get return value of a generator that hasn't returned |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
--TEST-- |
||||||
|
generator union signatures validate parameters without checking the yielded return value |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function union_generator(int|string $value): Iterator|array |
||||||
|
{ |
||||||
|
yield $value; |
||||||
|
return 42; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = union_generator('valid'); |
||||||
|
var_dump($generator->current()); |
||||||
|
$generator->next(); |
||||||
|
var_dump($generator->getReturn()); |
||||||
|
|
||||||
|
try { |
||||||
|
union_generator([]); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(5) "valid" |
||||||
|
int(42) |
||||||
|
TypeError |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
--TEST-- |
||||||
|
generator automatic integer keys track the greatest integer key |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function mixed_keys(): iterable |
||||||
|
{ |
||||||
|
yield 'name' => 1; |
||||||
|
yield 2; |
||||||
|
yield 5 => 3; |
||||||
|
yield 4; |
||||||
|
yield -2 => 5; |
||||||
|
yield 6; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
foreach (mixed_keys() as $key => $value) { |
||||||
|
var_dump($key); |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(4) "name" |
||||||
|
int(0) |
||||||
|
int(5) |
||||||
|
int(6) |
||||||
|
int(-2) |
||||||
|
int(7) |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
--TEST-- |
||||||
|
yield from rejects an IteratorAggregate cycle |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class CyclicAggregate implements IteratorAggregate |
||||||
|
{ |
||||||
|
public function getIterator(): Traversable |
||||||
|
{ |
||||||
|
return $this; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function cyclic_yield_from(): iterable |
||||||
|
{ |
||||||
|
yield from new CyclicAggregate(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
try { |
||||||
|
cyclic_yield_from()->current(); |
||||||
|
} catch (Throwable $e) { |
||||||
|
echo get_class($e), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
Exception |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
--TEST-- |
||||||
|
yield from delegates throw and preserves the child return value |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function throwing_child(): iterable |
||||||
|
{ |
||||||
|
try { |
||||||
|
yield 'ready'; |
||||||
|
} catch (RuntimeException $e) { |
||||||
|
yield 'child:' . $e->getMessage(); |
||||||
|
} |
||||||
|
return 7; |
||||||
|
} |
||||||
|
|
||||||
|
function throwing_parent(): iterable |
||||||
|
{ |
||||||
|
$result = yield from throwing_child(); |
||||||
|
yield 'return:' . $result; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$generator = throwing_parent(); |
||||||
|
var_dump($generator->current()); |
||||||
|
var_dump($generator->throw(new RuntimeException('injected'))); |
||||||
|
$generator->next(); |
||||||
|
var_dump($generator->current()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(5) "ready" |
||||||
|
string(14) "child:injected" |
||||||
|
string(8) "return:7" |
||||||
Loading…
Reference in new issue