- Implement anonymous and arrow generator closures with lazy execution - Add proper context preservation for closure generator compilation - Support yield evaluation order with key and value side effects - Enable yield from with iterator evaluation order matching PHP behavior - Add FiberGenerator as global type replacing TypePHP\FiberGenerator - Implement proper return value handling for closed and failed generators - Add validation for generator closure parameter restrictions - Support reentrant generator advance with error handling - Add comprehensive test coverage for generator edge casespull/20/head
parent
1403590a32
commit
bd802ba9c4
13 changed files with 522 additions and 21 deletions
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
closed generator releases captured values while preserving its return value |
||||
--FILE-- |
||||
<?php |
||||
class GeneratorRetentionState |
||||
{ |
||||
public static ?WeakReference $weak = null; |
||||
} |
||||
|
||||
function retaining_generator(object $captured): iterable |
||||
{ |
||||
yield 1; |
||||
return 2; |
||||
} |
||||
|
||||
function make_retaining_generator(): iterable |
||||
{ |
||||
$captured = new stdClass(); |
||||
GeneratorRetentionState::$weak = WeakReference::create($captured); |
||||
return retaining_generator($captured); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = make_retaining_generator(); |
||||
$generator->current(); |
||||
$generator->next(); |
||||
gc_collect_cycles(); |
||||
var_dump($generator->getReturn()); |
||||
var_dump(GeneratorRetentionState::$weak?->get() === null); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
bool(true) |
||||
@ -0,0 +1,53 @@ |
||||
--TEST-- |
||||
anonymous and arrow generator closures preserve lazy execution captures and return values |
||||
--FILE-- |
||||
<?php |
||||
class GeneratorClosureBox |
||||
{ |
||||
public function make(int $base): Closure |
||||
{ |
||||
return function (int $offset = 1) use ($base): iterable { |
||||
yield 'method' => $this->value + $base + $offset; |
||||
return 9; |
||||
}; |
||||
} |
||||
|
||||
public int $value = 10; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$state = 1; |
||||
$factory = function (int $add) use (&$state): iterable { |
||||
++$state; |
||||
$sent = yield 'closure' => $state + $add; |
||||
return $sent; |
||||
}; |
||||
|
||||
$generator = $factory(3); |
||||
var_dump($state); |
||||
var_dump($generator->key(), $generator->current()); |
||||
var_dump($state); |
||||
var_dump($generator->send(7)); |
||||
var_dump($generator->getReturn()); |
||||
|
||||
$arrow = fn (): iterable => yield 'arrow' => $state; |
||||
var_dump($arrow()->key(), $arrow()->current()); |
||||
|
||||
$method = (new GeneratorClosureBox())->make(2)(3); |
||||
var_dump($method->current()); |
||||
$method->next(); |
||||
var_dump($method->getReturn()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
string(7) "closure" |
||||
int(5) |
||||
int(2) |
||||
NULL |
||||
int(7) |
||||
string(5) "arrow" |
||||
int(2) |
||||
int(15) |
||||
int(9) |
||||
@ -0,0 +1,41 @@ |
||||
--TEST-- |
||||
failed generator releases captured values while retaining failed state |
||||
--FILE-- |
||||
<?php |
||||
class FailedGeneratorRetentionState |
||||
{ |
||||
public static ?WeakReference $weak = null; |
||||
} |
||||
|
||||
function failing_retaining_generator(object $captured): iterable |
||||
{ |
||||
if (false) { |
||||
yield 1; |
||||
} |
||||
throw new RuntimeException('failed'); |
||||
} |
||||
|
||||
function make_failing_retaining_generator(): iterable |
||||
{ |
||||
$captured = new stdClass(); |
||||
FailedGeneratorRetentionState::$weak = WeakReference::create($captured); |
||||
return failing_retaining_generator($captured); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = make_failing_retaining_generator(); |
||||
try { |
||||
$generator->current(); |
||||
} catch (Throwable $e) { |
||||
echo $e->getMessage(), "\n"; |
||||
} |
||||
gc_collect_cycles(); |
||||
var_dump($generator->valid()); |
||||
var_dump(FailedGeneratorRetentionState::$weak?->get() === null); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
failed |
||||
bool(false) |
||||
bool(true) |
||||
@ -0,0 +1,27 @@ |
||||
--TEST-- |
||||
FiberGenerator is registered globally and accepted as an exact generator return type |
||||
--FILE-- |
||||
<?php |
||||
function exact_fiber_generator(): \FiberGenerator |
||||
{ |
||||
yield 1; |
||||
return 2; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = exact_fiber_generator(); |
||||
var_dump(get_class($generator)); |
||||
var_dump($generator instanceof \FiberGenerator); |
||||
var_dump(class_exists('TypePHP\\FiberGenerator', false)); |
||||
var_dump($generator->current()); |
||||
$generator->next(); |
||||
var_dump($generator->getReturn()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(14) "FiberGenerator" |
||||
bool(true) |
||||
bool(false) |
||||
int(1) |
||||
int(2) |
||||
@ -0,0 +1,34 @@ |
||||
--TEST-- |
||||
FiberGenerator cannot be directly constructed cloned or serialized |
||||
--FILE-- |
||||
<?php |
||||
function guarded_generator(): iterable |
||||
{ |
||||
yield 1; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = guarded_generator(); |
||||
$generator->current(); |
||||
|
||||
foreach (['clone', 'serialize', 'construct'] as $operation) { |
||||
try { |
||||
if ($operation === 'clone') { |
||||
$copy = clone $generator; |
||||
} elseif ($operation === 'serialize') { |
||||
serialize($generator); |
||||
} else { |
||||
new FiberGenerator(null); |
||||
} |
||||
echo $operation, ":allowed\n"; |
||||
} catch (Throwable $e) { |
||||
echo $operation, ':', get_class($e), "\n"; |
||||
} |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
clone:Error |
||||
serialize:Exception |
||||
construct:Error |
||||
@ -0,0 +1,26 @@ |
||||
--TEST-- |
||||
throw on a new generator that returns before yielding rethrows the supplied exception |
||||
--FILE-- |
||||
<?php |
||||
function empty_before_throw(): iterable |
||||
{ |
||||
if (false) { |
||||
yield 1; |
||||
} |
||||
return 7; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = empty_before_throw(); |
||||
try { |
||||
$generator->throw(new RuntimeException('new-empty')); |
||||
} catch (Throwable $e) { |
||||
echo get_class($e), ':', $e->getMessage(), "\n"; |
||||
} |
||||
var_dump($generator->getReturn()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
RuntimeException:new-empty |
||||
int(7) |
||||
@ -0,0 +1,39 @@ |
||||
--TEST-- |
||||
reentrant generator advance throws without corrupting outer generator state |
||||
--FILE-- |
||||
<?php |
||||
class ReentrantGeneratorState |
||||
{ |
||||
public static mixed $generator = null; |
||||
} |
||||
|
||||
function reentrant_generator(): iterable |
||||
{ |
||||
yield 1; |
||||
try { |
||||
ReentrantGeneratorState::$generator->next(); |
||||
} catch (Throwable $e) { |
||||
echo get_class($e), ':', $e->getMessage(), "\n"; |
||||
} |
||||
yield 2; |
||||
return 3; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$generator = reentrant_generator(); |
||||
ReentrantGeneratorState::$generator = $generator; |
||||
var_dump($generator->current()); |
||||
$generator->next(); |
||||
var_dump($generator->current(), $generator->valid()); |
||||
$generator->next(); |
||||
var_dump($generator->valid(), $generator->getReturn()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
Error:Cannot resume an already running generator |
||||
int(2) |
||||
bool(true) |
||||
bool(false) |
||||
int(3) |
||||
@ -0,0 +1,42 @@ |
||||
--TEST-- |
||||
yield evaluates key and value side effects before suspension in PHP order |
||||
--FILE-- |
||||
<?php |
||||
class YieldOrderBox |
||||
{ |
||||
public int $value = 0; |
||||
} |
||||
|
||||
function mark(string $name): string |
||||
{ |
||||
echo $name, "\n"; |
||||
return $name; |
||||
} |
||||
|
||||
function ordered_yield(YieldOrderBox $box): iterable |
||||
{ |
||||
yield mark('key') => match (mark('selector')) { |
||||
'selector' => mark('value'), |
||||
}; |
||||
yield $box->value++; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$box = new YieldOrderBox(); |
||||
$generator = ordered_yield($box); |
||||
var_dump($generator->current()); |
||||
var_dump($box->value); |
||||
$generator->next(); |
||||
var_dump($generator->current()); |
||||
var_dump($box->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
key |
||||
selector |
||||
value |
||||
string(5) "value" |
||||
int(0) |
||||
int(0) |
||||
int(1) |
||||
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
yield from calls Iterator current before key |
||||
--FILE-- |
||||
<?php |
||||
class YieldFromOrderIterator implements Iterator |
||||
{ |
||||
private int $index = 0; |
||||
|
||||
public function rewind(): void { echo "rewind\n"; } |
||||
public function valid(): bool { echo "valid\n"; return $this->index < 1; } |
||||
public function current(): mixed { echo "current\n"; return 1; } |
||||
public function key(): mixed { echo "key\n"; return 0; } |
||||
public function next(): void { echo "next\n"; ++$this->index; } |
||||
} |
||||
|
||||
function ordered_yield_from(): iterable |
||||
{ |
||||
yield from new YieldFromOrderIterator(); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
foreach (ordered_yield_from() as $key => $value) { |
||||
echo "body\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
rewind |
||||
valid |
||||
current |
||||
key |
||||
body |
||||
next |
||||
valid |
||||
Loading…
Reference in new issue