- Implement FiberGenerator trait with yield/yield from parsing - Add generator detection and preparation logic in function compilation - Generate TypePHP\FiberGenerator objects that implement Iterator - Support yield expressions and statements with key-value pairs - Enable yield from delegation with array and traversable forwarding - Register fiber generator class entry during module initialization - Update runtime initialization to use typephp_runtime_init - Replace php_aot_ prefixed helpers with typephp_ prefixed versions - Add comprehensive generator test suite covering various scenarios - Document generator limitations and compatibility restrictions - Mark dynamic PHP foreach over native generators as XFAIL casepull/16/head
parent
3d8eb272a6
commit
2bad099e09
26 changed files with 521 additions and 42 deletions
@ -0,0 +1,21 @@ |
||||
# yield / generator 限制 |
||||
|
||||
TypePHP 的 generator 基于 PHP Fiber 运行。generator 函数或方法会返回 `TypePHP\FiberGenerator`,该对象实现 `Iterator`,但不是 PHP 内置 `Generator` 实例。 |
||||
|
||||
## 不支持 |
||||
|
||||
- 不支持声明返回类型为 `Generator`;请使用 `Iterator`、`Traversable`、`iterable`、`mixed`,或省略返回类型。 |
||||
- 不支持按引用返回的 generator,例如 `function &gen() { yield 1; }`。 |
||||
- 不支持 generator 参数按引用传递。 |
||||
- 不支持 generator 可变参数。 |
||||
- 不支持 by-reference yield 语义。 |
||||
- 不支持在动态 PHP 脚本中使用 `foreach` 直接遍历 TypePHP Native generator 返回的 `TypePHP\FiberGenerator`。 |
||||
- 不保证 `instanceof Generator`、`ReflectionGenerator`、`Generator` 内部实现细节与 Zend 原生 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 可能存在边界差异。 |
||||
@ -0,0 +1,189 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Generator; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr\Yield_; |
||||
use PhpParser\Node\Expr\YieldFrom; |
||||
use PhpParser\Node\Stmt\ClassMethod; |
||||
use PhpParser\Node\Stmt\Function_; |
||||
use TypePhp\Context\FunctionContext; |
||||
use TypePhp\Entity\FunctionDef; |
||||
|
||||
trait FiberGenerator |
||||
{ |
||||
protected function containsYield(Function_|ClassMethod $v): bool |
||||
{ |
||||
return $this->containsYieldInNodes($v->stmts ?? []); |
||||
} |
||||
|
||||
protected function containsYieldInNodes(array $nodes): bool |
||||
{ |
||||
foreach ($nodes as $node) { |
||||
if ($node instanceof Node && $this->containsYieldInNode($node)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function containsYieldInNode(Node $node): bool |
||||
{ |
||||
if ($node instanceof Yield_ || $node instanceof YieldFrom) { |
||||
return true; |
||||
} |
||||
if ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike) { |
||||
return false; |
||||
} |
||||
foreach ($node->getSubNodeNames() as $name) { |
||||
$subNode = $node->{$name}; |
||||
if ($subNode instanceof Node) { |
||||
if ($this->containsYieldInNode($subNode)) { |
||||
return true; |
||||
} |
||||
} elseif (is_array($subNode) && $this->containsYieldInNodes($subNode)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function prepareGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef): void |
||||
{ |
||||
if ($v->byRef) { |
||||
$this->fatalError($v, 'Generators returning by reference are not supported yet'); |
||||
} |
||||
foreach ($v->params as $param) { |
||||
if ($param->byRef || $param->variadic) { |
||||
$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'); |
||||
} |
||||
$functionDef->generator = true; |
||||
$functionDef->returnType = self::TYPE_VAR; |
||||
$functionDef->returnClass = ''; |
||||
$functionDef->returnTypeCheck = null; |
||||
$functionDef->returnTypeStr = ''; |
||||
$functionDef->returnTypeNode = null; |
||||
} |
||||
|
||||
protected function parseYieldExpr(Yield_ $expr): string |
||||
{ |
||||
if (!$this->inGeneratorBody) { |
||||
$this->fatalError($expr, 'The `Expr_Yield` is not supported outside generator functions'); |
||||
} |
||||
return 'typephp_fiber_suspend(' . $this->genYieldPayload($expr) . ', nullptr)'; |
||||
} |
||||
|
||||
protected function parseYieldStmt(Yield_ $expr): string |
||||
{ |
||||
$payload = $this->genYieldPayload($expr); |
||||
$closed = $this->genTmpVarName(); |
||||
$this->addLocalVar($closed, self::TYPE_BOOL); |
||||
return $closed . ' = false;' . PHP_EOL |
||||
. $this->getIndent() . $closed . ' = typephp_fiber_yield(' . $payload . ');' . PHP_EOL |
||||
. $this->getIndent() . 'if (' . $closed . ') {' . PHP_EOL |
||||
. $this->getIndent() . ' return ' . self::VALUE_NULL . ';' . PHP_EOL |
||||
. $this->getIndent() . '}'; |
||||
} |
||||
|
||||
protected function parseYieldFromStmt(YieldFrom $expr): string |
||||
{ |
||||
$closed = $this->genTmpVarName(); |
||||
$this->addLocalVar($closed, self::TYPE_BOOL); |
||||
return $closed . ' = false;' . PHP_EOL |
||||
. $this->getIndent() . 'typephp_fiber_yield_from(' . $this->parseExprAsValue($expr->expr) . ', &' . $closed . ');' . PHP_EOL |
||||
. $this->getIndent() . 'if (' . $closed . ') {' . PHP_EOL |
||||
. $this->getIndent() . ' return ' . self::VALUE_NULL . ';' . PHP_EOL |
||||
. $this->getIndent() . '}'; |
||||
} |
||||
|
||||
protected function genYieldPayload(Yield_ $expr): string |
||||
{ |
||||
$value = $expr->value ? $this->parseExprAsValue($expr->value) : self::VALUE_NULL; |
||||
if ($expr->key) { |
||||
$key = $this->parseExprAsValue($expr->key); |
||||
return 'php::Array(php::StdStrKeyMap{{"key", ' . $key . '}, {"value", ' . $value . '}, {"has_key", true}})'; |
||||
} |
||||
return 'php::Array(php::StdStrKeyMap{{"value", ' . $value . '}, {"has_key", false}})'; |
||||
} |
||||
|
||||
protected function parseYieldFromExpr(YieldFrom $expr): string |
||||
{ |
||||
if (!$this->inGeneratorBody) { |
||||
$this->fatalError($expr, 'The `Expr_YieldFrom` is not supported outside generator functions'); |
||||
} |
||||
return 'typephp_fiber_yield_from(' . $this->parseExprAsValue($expr->expr) . ', nullptr)'; |
||||
} |
||||
|
||||
protected function genFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string |
||||
{ |
||||
$functionDeclCode = self::TYPE_VAR . ' ' . self::PREFIX . $nativeName . '('; |
||||
if ($this->class) { |
||||
$functionDeclCode .= self::TYPE_OBJECT . ' &this_'; |
||||
if ($functionDef->params) { |
||||
$functionDeclCode .= ', '; |
||||
} |
||||
} |
||||
$functionDeclCode .= $functionDef->params . ')'; |
||||
|
||||
$uses = []; |
||||
foreach ($functionDef->argInfoList as $argInfo) { |
||||
$uses[] = $argInfo->name; |
||||
} |
||||
|
||||
$code = $functionDeclCode . ' {' . PHP_EOL; |
||||
$this->indentLevel++; |
||||
$closureVar = $this->genTmpVarName(); |
||||
$code .= $this->getIndent() . 'php::ClosureFn ' . $closureVar . ' = [](' |
||||
. 'INTERNAL_FUNCTION_PARAMETERS, ' |
||||
. self::TYPE_OBJECT . ' &this_, ' |
||||
. self::TYPE_ARGS . ' &vars_) -> ' . self::TYPE_VAR . ' {' . PHP_EOL; |
||||
|
||||
$outerContext = $this->context; |
||||
$outerIndent = $this->indentLevel; |
||||
$outerInGeneratorBody = $this->inGeneratorBody; |
||||
$this->context = new FunctionContext(); |
||||
$this->context->inClosure = true; |
||||
$this->inGeneratorBody = true; |
||||
$this->indentLevel++; |
||||
|
||||
foreach ($functionDef->argInfoList as $i => $argInfo) { |
||||
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $argInfo->name . ' = vars_.get(' . $i . ');' . PHP_EOL; |
||||
$this->addArgument($argInfo->name, self::TYPE_VAR); |
||||
} |
||||
if ($this->class) { |
||||
$this->addArgument('this_', self::TYPE_OBJECT); |
||||
} |
||||
|
||||
$body = ''; |
||||
if ($v->stmts) { |
||||
$body = $this->parseStmts($v->stmts); |
||||
} |
||||
$body .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL; |
||||
$code .= $this->genScopeVarDecl() . $body; |
||||
|
||||
$this->indentLevel = $outerIndent; |
||||
$this->inGeneratorBody = $outerInGeneratorBody; |
||||
$this->context = $outerContext; |
||||
|
||||
$code .= $this->getIndent() . '};' . PHP_EOL; |
||||
$args = $uses ? '{ ' . implode(', ', $uses) . ' }' : '{}'; |
||||
$closureExpr = $this->class |
||||
? 'php::newClosure(' . $closureVar . ', ' . $args . ', this_)' |
||||
: 'php::newClosure(' . $closureVar . ', ' . $args . ')'; |
||||
$code .= $this->getIndent() . 'return php::newObject(typephp_fiber_generator_ce, {' . $closureExpr . '});' . PHP_EOL; |
||||
$this->indentLevel--; |
||||
$code .= '}' . PHP_EOL; |
||||
|
||||
return $code; |
||||
} |
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
--TEST-- |
||||
generator functions via Fiber iterator |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function gen_values(int $start): iterable |
||||
{ |
||||
yield 'a' => $start; |
||||
yield 'b' => $start + 1; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$gen = gen_values(10); |
||||
var_dump($gen instanceof Iterator); |
||||
foreach ($gen as $key => $value) { |
||||
var_dump($key . ':' . $value); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
string(4) "a:10" |
||||
string(4) "b:11" |
||||
@ -0,0 +1,24 @@ |
||||
--TEST-- |
||||
dynamic PHP foreach consumes native TypePHP generator |
||||
--XFAIL-- |
||||
Dynamic PHP foreach over TypePHP FiberGenerator requires Zend iterator handler integration. |
||||
--FILE-- |
||||
<?php |
||||
function native_values(): iterable |
||||
{ |
||||
yield 'native-a' => 100; |
||||
yield 'native-b' => 200; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
require __DIR__ . '/dynamic-generator-interop.inc'; |
||||
|
||||
foreach (dynamic_collect_iterable(native_values()) as $line) { |
||||
echo $line, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
native-a:100 |
||||
native-b:200 |
||||
@ -0,0 +1,22 @@ |
||||
<?php |
||||
function dynamic_yield_values(): iterable |
||||
{ |
||||
yield 'dyn-a' => 11; |
||||
yield 'dyn-b' => 22; |
||||
} |
||||
|
||||
function dynamic_yield_from_values(): iterable |
||||
{ |
||||
yield 'dyn-start' => 0; |
||||
yield from dynamic_yield_values(); |
||||
yield 'dyn-end' => 33; |
||||
} |
||||
|
||||
function dynamic_collect_iterable(iterable $iterable): array |
||||
{ |
||||
$result = []; |
||||
foreach ($iterable as $key => $value) { |
||||
$result[] = $key . ':' . $value; |
||||
} |
||||
return $result; |
||||
} |
||||
@ -0,0 +1,30 @@ |
||||
--TEST-- |
||||
generator methods can access private properties |
||||
--FILE-- |
||||
<?php |
||||
class PrivateGeneratorBox implements IteratorAggregate |
||||
{ |
||||
private array $items; |
||||
|
||||
public function __construct(array $items) |
||||
{ |
||||
$this->items = $items; |
||||
} |
||||
|
||||
public function getIterator(): Traversable |
||||
{ |
||||
foreach ($this->items as $item) { |
||||
yield $item; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
foreach (new PrivateGeneratorBox([1]) as $value) { |
||||
echo $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECTF-- |
||||
1 |
||||
@ -0,0 +1,33 @@ |
||||
--TEST-- |
||||
generator methods via Fiber iterator |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class GeneratorMethodBox implements IteratorAggregate |
||||
{ |
||||
private array $items; |
||||
|
||||
public function __construct(array $items) |
||||
{ |
||||
$this->items = $items; |
||||
} |
||||
|
||||
public function getIterator(): Traversable |
||||
{ |
||||
foreach ($this->items as $key => $value) { |
||||
yield $key => strtoupper($value); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$box = new GeneratorMethodBox(['first' => 'alpha', 'second' => 'beta']); |
||||
foreach ($box as $key => $value) { |
||||
echo $key, '=', $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
first=ALPHA |
||||
second=BETA |
||||
@ -0,0 +1,26 @@ |
||||
--TEST-- |
||||
native foreach consumes dynamic PHP generator |
||||
--FILE-- |
||||
<?php |
||||
function main(): void |
||||
{ |
||||
require __DIR__ . '/dynamic-generator-interop.inc'; |
||||
|
||||
foreach (dynamic_yield_values() as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
|
||||
echo "-- yield from --\n"; |
||||
foreach (dynamic_yield_from_values() as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
dyn-a:11 |
||||
dyn-b:22 |
||||
-- yield from -- |
||||
dyn-start:0 |
||||
dyn-a:11 |
||||
dyn-b:22 |
||||
dyn-end:33 |
||||
@ -0,0 +1,30 @@ |
||||
--TEST-- |
||||
native generator and native foreach interoperate |
||||
--FILE-- |
||||
<?php |
||||
function native_child(): iterable |
||||
{ |
||||
yield 'child-a' => 1; |
||||
yield 'child-b' => 2; |
||||
return 3; |
||||
} |
||||
|
||||
function native_parent(): iterable |
||||
{ |
||||
yield 'parent-start' => 0; |
||||
$ret = yield from native_child(); |
||||
yield 'parent-ret' => $ret; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
foreach (native_parent() as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
parent-start:0 |
||||
child-a:1 |
||||
child-b:2 |
||||
parent-ret:3 |
||||
@ -0,0 +1,23 @@ |
||||
--TEST-- |
||||
yield from forwards array keys and values |
||||
--FILE-- |
||||
<?php |
||||
function gen_from_array(): iterable |
||||
{ |
||||
yield 'start' => 0; |
||||
yield from ['a' => 1, 'b' => 2]; |
||||
yield 'end' => 3; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
foreach (gen_from_array() as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
start:0 |
||||
a:1 |
||||
b:2 |
||||
end:3 |
||||
@ -0,0 +1,29 @@ |
||||
--TEST-- |
||||
yield from forwards delegated generator values and returns its result |
||||
--FILE-- |
||||
<?php |
||||
function child_gen(): iterable |
||||
{ |
||||
yield 'x' => 10; |
||||
yield 'y' => 20; |
||||
return 30; |
||||
} |
||||
|
||||
function parent_gen(): iterable |
||||
{ |
||||
$result = yield from child_gen(); |
||||
yield 'result' => $result; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$gen = parent_gen(); |
||||
foreach ($gen as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
x:10 |
||||
y:20 |
||||
result:30 |
||||
@ -0,0 +1,20 @@ |
||||
--TEST-- |
||||
yield expression receives send value |
||||
--FILE-- |
||||
<?php |
||||
function gen_send(): iterable |
||||
{ |
||||
$value = yield 1; |
||||
yield $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$gen = gen_send(); |
||||
var_dump($gen->current()); |
||||
var_dump($gen->send(42)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
int(42) |
||||
Loading…
Reference in new issue