- 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