- 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