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 operations
pull/16/head
韩天峰 2 months ago
parent d251331ae4
commit 7faec8f459
  1. 3
      docs/YIELD_GENERATOR.md
  2. 5
      src/CompilerBase.php
  3. 68
      src/Generator/FiberGenerator.php
  4. 5
      src/Preprocessor.php
  5. 24
      tests/aot/generator/closed-throw.phpt
  6. 22
      tests/aot/generator/constructor-property-promotion.phpt
  7. 25
      tests/aot/generator/destructor-finally.phpt
  8. 36
      tests/aot/generator/send-value-lifetime.phpt
  9. 71
      tests/aot/generator/state-machine.phpt
  10. 32
      tests/aot/generator/uncaught-exception.phpt
  11. 28
      tests/aot/generator/union-signatures.phpt
  12. 28
      tests/aot/generator/yield-automatic-keys.phpt
  13. 28
      tests/aot/generator/yield-from-iterator-aggregate-cycle.phpt
  14. 33
      tests/aot/generator/yield-from-throw.phpt

@ -4,7 +4,7 @@ TypePHP 的 generator 基于 PHP Fiber 运行。generator 函数或方法会返
## 不支持
- 不支持声明返回类型为 `Generator`;请使用 `Iterator`、`Traversable`、`iterable`、`mixed`,或省略返回类型。
- 不支持声明返回类型为 `Generator`;请使用 `Iterator`、`Traversable`、`iterable`、`object`、`mixed`,或省略返回类型。
- 不支持按引用返回的 generator,例如 `function &gen() { yield 1; }`
- 不支持 generator 参数按引用传递。
- 不支持 generator 可变参数。
@ -15,7 +15,6 @@ TypePHP 的 generator 基于 PHP Fiber 运行。generator 函数或方法会返
## 受限行为
- `yield from` 可以转发数组和 `Traversable` 的 key/value;委托对象是 generator 时可以读取其 return value。
- `yield from``send()`/`throw()` 委托透传仍属于受限场景,复杂协程式双向通信应避免依赖。
- TypePHP Native `foreach` 可以遍历动态 PHP 返回的 Zend 原生 generator;反向由 ZendVM `foreach` 驱动 TypePHP Native generator 暂不支持。
- generator 的执行依赖 Fiber;如果当前 PHP 运行环境禁用或缺失 Fiber,则无法运行。
- generator body 在 Fiber 内执行,析构、异常传播、force-close 与 Zend 原生 generator 可能存在边界差异。

@ -7205,7 +7205,10 @@ class CompilerBase implements PropertyAccessContext
$code .= $this->parseStmts($finally->stmts);
$code .= PHP_EOL;
}
$code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . 'php::throwException(php::Object(' . $exVar . '));' . PHP_EOL . $this->getIndent() . '}';
$rethrow = $this->inGeneratorBody
? 'typephp_fiber_rethrow(' . $exVar . ');'
: 'php::throwException(php::Object(' . $exVar . '));';
$code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . $rethrow . PHP_EOL . $this->getIndent() . '}';
return $code;
}

@ -13,6 +13,9 @@ use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\Expr\YieldFrom;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
use TypePhp\Context\FunctionContext;
use TypePhp\Entity\FunctionDef;
@ -64,8 +67,8 @@ trait FiberGenerator
$this->fatalError($param, 'Generators with by-reference or variadic parameters are not supported yet');
}
}
if ($functionDef->returnClass === 'Generator') {
$this->fatalError($v, 'Generator return type is not supported by TypePHP Fiber generators yet; use Iterator, Traversable, iterable, mixed, or omit the return type');
if (!$this->generatorReturnTypeAcceptsFiber($v->returnType)) {
$this->fatalError($v, 'Generator return type must accept TypePHP\\FiberGenerator; use Iterator, Traversable, iterable, object, mixed, or omit the return type');
}
$functionDef->generator = true;
$functionDef->returnType = self::TYPE_VAR;
@ -75,6 +78,42 @@ trait FiberGenerator
$functionDef->returnTypeNode = null;
}
protected function generatorReturnTypeAcceptsFiber(?Node $type): bool
{
if ($type === null) {
return true;
}
if ($type instanceof NullableType) {
return $this->generatorReturnTypeAcceptsFiber($type->type);
}
if ($type instanceof UnionType) {
foreach ($type->types as $member) {
if ($this->generatorReturnTypeAcceptsFiber($member)) {
return true;
}
}
return false;
}
if ($type instanceof IntersectionType) {
foreach ($type->types as $member) {
if (!$this->generatorReturnTypeAcceptsFiber($member)) {
return false;
}
}
return true;
}
$typeName = strtolower($this->parseIdentifier($type));
if (in_array($typeName, ['mixed', 'object', 'iterable'], true)) {
return true;
}
$class = '';
$this->parseTypeDecl($type, self::DECL_TYPE_OF_RETURN, $class);
$class = strtolower(ltrim($class, '\\'));
return in_array($class, ['iterator', 'traversable', 'typephp\\fibergenerator'], true);
}
protected function parseYieldExpr(Yield_ $expr): string
{
if (!$this->inGeneratorBody) {
@ -142,6 +181,17 @@ trait FiberGenerator
$code = $functionDeclCode . ' {' . PHP_EOL;
$this->indentLevel++;
foreach ($functionDef->argInfoList as $i => $argInfo) {
if (!empty($argInfo->typeCheck)) {
$code .= $this->genUnionParamCheck($argInfo, $i);
}
}
foreach ($functionDef->argInfoList as $argInfo) {
if ($argInfo->property) {
$code .= $this->getIndent() . $this->genPropertyPromotion($argInfo);
}
}
$closureVar = $this->genTmpVarName();
$code .= $this->getIndent() . 'php::ClosureFn ' . $closureVar . ' = []('
. 'INTERNAL_FUNCTION_PARAMETERS, '
@ -165,11 +215,21 @@ trait FiberGenerator
}
$body = '';
$this->indentLevel++;
if ($this->methodDef && $this->methodDef->hasDynamicCall) {
$body .= $this->genScopeSwitchCode();
}
if ($v->stmts) {
$body = $this->parseStmts($v->stmts);
$body .= $this->parseStmts($v->stmts);
}
$body .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL;
$code .= $this->genScopeVarDecl() . $body;
$this->indentLevel--;
$code .= $this->genScopeVarDecl();
$code .= $this->getIndent() . 'try {' . PHP_EOL;
$code .= $body;
$code .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL;
$code .= $this->getIndent() . ' return ' . self::VALUE_NULL . ';' . PHP_EOL;
$code .= $this->getIndent() . '}' . PHP_EOL;
$this->indentLevel = $outerIndent;
$this->inGeneratorBody = $outerInGeneratorBody;

@ -447,7 +447,10 @@ class Preprocessor extends CompilerBase
$this->prepareGeneratorFunction($v, $functionDef);
}
if ($v->returnType instanceof NullableType || $v->returnType instanceof UnionType || $v->returnType instanceof IntersectionType) {
if (!$functionDef->generator
&& ($v->returnType instanceof NullableType
|| $v->returnType instanceof UnionType
|| $v->returnType instanceof IntersectionType)) {
$typeInfo = $this->buildTypeCheckFromNode($v->returnType);
if (!empty($typeInfo['check'])) {
$functionDef->returnTypeCheck = $typeInfo['check'];

@ -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…
Cancel
Save