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