diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 1163e256..cfe3872d 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -13,13 +13,13 @@ ## 声明与类型 -- 不支持 `yield` / `yield from`。 - 不支持可变变量 `$$var`。 - 不支持 PHP 8.4 property hooks。 - 不支持闭包或箭头函数按引用返回。 - `__construct()` 不允许返回值。 - 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。 - 不支持引用可变参数 `&...$args`。 +- 不支持在动态 PHP 脚本中使用 `foreach` 直接遍历 TypePHP Native generator 返回的 `TypePHP\FiberGenerator`。 - 联合类型、交叉类型、`nullable` 类型在静态编译阶段按 `mixed/any` 处理,只保留运行时 type check。 - 局部变量类型一旦被静态推断为具体 native 类型,不支持在同一作用域内重新赋值为不兼容类型。 - attribute 参数不支持数组值和 `new` 表达式。 diff --git a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md index 6b3f9a4a..ce451495 100644 --- a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md +++ b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md @@ -77,13 +77,13 @@ These items should be documented with the exact boundary. | Feature | Classification | Implementation Direction | |---|---|---| -| `yield` / `yield from` | Pending | Lower generator functions to state machines and provide a Generator runtime object. | | Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. | | PHP 8.4 property hooks | Pending | Add parser and AST support, then lower property read/write paths to hook calls. | | Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. | | Closure and arrow function by-reference parameters | Pending | Closure arginfo must preserve by-reference parameters and call lowering must pass reference slots. | | By-reference variadic parameters (`&...$args`) | Pending | Variadic storage must preserve references instead of copying values. | | By-reference parameters with default values | Pending | Need PHP-compatible handling for omitted arguments using temporary default values while still binding references for passed arguments. | +| Dynamic PHP `foreach` over TypePHP Native generator | Pending / Complex | Requires Zend iterator handler integration or a userland wrapper so ZendVM can drive `TypePHP\FiberGenerator` exactly like native `Generator`. | | Reference assignment from complex static property expressions | Pending | Static property reference targets need complete lowering and lifetime handling. | | Dynamic calls automatically converting by-reference arguments | Pending | Runtime callable metadata or reflection can identify by-reference parameters and build reference arguments dynamically. | | Calls with unpack plus trailing named arguments staying native | Pending | Normalize and reorder call arguments in IR before native-call selection. | diff --git a/docs/YIELD_GENERATOR.md b/docs/YIELD_GENERATOR.md new file mode 100644 index 00000000..50cfced3 --- /dev/null +++ b/docs/YIELD_GENERATOR.md @@ -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 可能存在边界差异。 diff --git a/examples/lib-demo/cpp-src/exports.cc b/examples/lib-demo/cpp-src/exports.cc index 0c0f6a96..d356705b 100644 --- a/examples/lib-demo/cpp-src/exports.cc +++ b/examples/lib-demo/cpp-src/exports.cc @@ -2,14 +2,14 @@ #include "../include/typephp_lib_demo.h" #include -extern "C" int php_aot_runtime_init(int argc, char **argv); +extern "C" int typephp_runtime_init(int argc, char **argv); extern php::Int php_demo_add(php::Int a, php::Int b); extern "C" TYPEPHP_LIB_DEMO_API int typephp_lib_demo_add(int a, int b) { char app_name[] = "typephp_lib_demo"; char *argv[] = {app_name, nullptr}; - if (php_aot_runtime_init(1, argv) != 0) { + if (typephp_runtime_init(1, argv) != 0) { return 0; } return static_cast(php_demo_add(a, b)); diff --git a/examples/minecraft-godot/cpp-src/typephp_world_api.cc b/examples/minecraft-godot/cpp-src/typephp_world_api.cc index c130bade..138332f1 100644 --- a/examples/minecraft-godot/cpp-src/typephp_world_api.cc +++ b/examples/minecraft-godot/cpp-src/typephp_world_api.cc @@ -18,8 +18,8 @@ enum DemoBlockType { static constexpr int DEMO_WATER_LEVEL_C = 4; -extern "C" int php_aot_runtime_init(int argc, char **argv); -extern "C" void php_aot_runtime_shutdown(); +extern "C" int typephp_runtime_init(int argc, char **argv); +extern "C" void typephp_runtime_shutdown(); static bool g_typephp_world_initialized = false; @@ -31,7 +31,7 @@ static int typephp_world_ensure_runtime() char app_name[] = "typephp_world"; char *argv[] = {app_name, nullptr}; - if (php_aot_runtime_init(1, argv) != 0) { + if (typephp_runtime_init(1, argv) != 0) { return 0; } @@ -50,7 +50,7 @@ TYPEPHP_WORLD_API void typephp_world_shutdown() return; } - php_aot_runtime_shutdown(); + typephp_runtime_shutdown(); g_typephp_world_initialized = false; } diff --git a/examples/ocean-godot/cpp-src/typephp_ocean_api.cc b/examples/ocean-godot/cpp-src/typephp_ocean_api.cc index e1313ba0..8ea06061 100644 --- a/examples/ocean-godot/cpp-src/typephp_ocean_api.cc +++ b/examples/ocean-godot/cpp-src/typephp_ocean_api.cc @@ -6,8 +6,8 @@ #define TYPEPHP_OCEAN_API extern "C" __attribute__((visibility("default"))) #endif -extern "C" int php_aot_runtime_init(int argc, char **argv); -extern "C" void php_aot_runtime_shutdown(); +extern "C" int typephp_runtime_init(int argc, char **argv); +extern "C" void typephp_runtime_shutdown(); static bool g_typephp_ocean_initialized = false; @@ -19,7 +19,7 @@ static int typephp_ocean_ensure_runtime() char app_name[] = "typephp_ocean"; char *argv[] = {app_name, nullptr}; - if (php_aot_runtime_init(1, argv) != 0) { + if (typephp_runtime_init(1, argv) != 0) { return 0; } @@ -37,7 +37,7 @@ TYPEPHP_OCEAN_API void typephp_ocean_shutdown() if (!g_typephp_ocean_initialized) { return; } - php_aot_runtime_shutdown(); + typephp_runtime_shutdown(); g_typephp_ocean_initialized = false; } diff --git a/examples/win32-hello/CXX_STD_CONFIG_GUIDE.md b/examples/win32-hello/CXX_STD_CONFIG_GUIDE.md index 106818e2..7754bcc8 100644 --- a/examples/win32-hello/CXX_STD_CONFIG_GUIDE.md +++ b/examples/win32-hello/CXX_STD_CONFIG_GUIDE.md @@ -284,7 +284,7 @@ php bin/compiler.php project.yml --verbose 应该看到类似这样的输出: ``` -Compiling main.cc... +Compiling typephp_main.cc... g++ -std=c++17 -O0 -Wall ... ``` diff --git a/src/CompilerBase.php b/src/CompilerBase.php index f0cdf3c6..ef3394e2 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -25,6 +25,7 @@ use TypePhp\Exception\Skip; use TypePhp\Exception\TestError; use TypePhp\Generator\AnonClassGenerator; use TypePhp\Generator\ClosureGenerator; +use TypePhp\Generator\FiberGenerator; use TypePhp\Generator\PlaceHolderGenerator; use TypePhp\Generator\PropertyPromotion; use TypePhp\Generator\Utils; @@ -77,6 +78,7 @@ class CompilerBase implements PropertyAccessContext use FuncCallOptimizer; use AnonClassGenerator; use ClosureGenerator; + use FiberGenerator; use PlaceHolderGenerator; use PropertyPromotion; use MagicMethodDetector; @@ -244,7 +246,8 @@ class CompilerBase implements PropertyAccessContext 'phpx_big_int.h', 'phpx_big_float.h', 'phpx_decimal.h', - 'php_aot_helper.h', + 'typephp_helper.h', + 'typephp_fiber_generator.h', 'phpx_std.h', ]; protected array $localHeaders = []; @@ -352,6 +355,7 @@ class CompilerBase implements PropertyAccessContext protected ?ClassDef $classDef = null; protected ?MethodDef $methodDef = null; protected ?InterfaceDef $interfaceDef = null; + protected bool $inGeneratorBody = false; protected FunctionContext $context; protected array $superGlobalVars = [ '_GET' => self::TYPE_ARRAY, @@ -789,9 +793,9 @@ class CompilerBase implements PropertyAccessContext case 'Expr_Exit': return $this->parseExit($expr); case 'Expr_Yield': + return $this->parseYieldExpr($expr); case 'Expr_YieldFrom': - $this->fatalError($expr, 'The `' . $type . '` is not supported'); - break; + return $this->parseYieldFromExpr($expr); default: abort($expr); break; @@ -1599,7 +1603,13 @@ class CompilerBase implements PropertyAccessContext $lines[] = $this->getComment($v, $class); switch ($class) { case 'Stmt_Expression': - $result = $this->parseExpr($v->expr) . ';'; + if ($this->inGeneratorBody && $v->expr instanceof Expr\Yield_) { + $result = $this->parseYieldStmt($v->expr); + } elseif ($this->inGeneratorBody && $v->expr instanceof Expr\YieldFrom) { + $result = $this->parseYieldFromStmt($v->expr); + } else { + $result = $this->parseExpr($v->expr) . ';'; + } break; case 'Stmt_Echo': $result = $this->parseEcho($v); @@ -7013,7 +7023,7 @@ class CompilerBase implements PropertyAccessContext $this->registerStaticPropertyRef($refVar, $class, $nativeProp, $info); if ($info['kind'] === 'zval') { - $helper = $def->type === self::TYPE_FLOAT ? 'php_aot_static_float_ref' : 'php_aot_static_int_ref'; + $helper = $def->type === self::TYPE_FLOAT ? 'typephp_static_float_ref' : 'typephp_static_int_ref'; return $helper . '(' . $refVar . ')'; } diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 5e9c14d2..0a5a2b98 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -27,6 +27,7 @@ class FunctionDef public bool $stub = false; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; + public bool $generator = false; /** * @var string 必须是带有命名空间的完整类名 diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php new file mode 100644 index 00000000..681f1566 --- /dev/null +++ b/src/Generator/FiberGenerator.php @@ -0,0 +1,189 @@ +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; + } +} diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 7869a053..2e518347 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -500,7 +500,7 @@ trait AssignOpTrait $var = $this->parseWritableIdentifier($node->var); if (!$this->isNativePropertyTypedValue($node->var)) { - $helper = $def->type === self::TYPE_FLOAT ? 'php_aot_static_float_ref' : 'php_aot_static_int_ref'; + $helper = $def->type === self::TYPE_FLOAT ? 'typephp_static_float_ref' : 'typephp_static_int_ref'; $var = $helper . '(' . $var . '.unwrap_ptr())'; } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 0f7e0f27..3c85f738 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -443,6 +443,9 @@ class Preprocessor extends CompilerBase $functionDef->stub = $this->stubFile; $functionDef->returnTypeUndeclared = $v->returnType === null; $functionDef->returnsByRef = $v->byRef; + if ($this->containsYield($v)) { + $this->prepareGeneratorFunction($v, $functionDef); + } if ($v->returnType instanceof NullableType || $v->returnType instanceof UnionType || $v->returnType instanceof IntersectionType) { $typeInfo = $this->buildTypeCheckFromNode($v->returnType); diff --git a/src/Translator.php b/src/Translator.php index af16c2ad..60da2d78 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1015,6 +1015,7 @@ CODE; $code .= 'PHP_MINIT_FUNCTION(' . $this->getModuleName() . ') {' . PHP_EOL; $code .= 'zend_try {' . PHP_EOL; $code .= '// class/interface class entries' . PHP_EOL; + $code .= 'typephp_register_fiber_generator_class();' . PHP_EOL; $code .= $this->genClassPropertyInit() . PHP_EOL; $code .= '// register symbols' . PHP_EOL; @@ -1344,9 +1345,11 @@ CODE; { $job = $this->maxJob; + $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_fiber_generator.cc'; + // embed 需要 main 函数,以及 cli 的内置函数定义 if ($this->isBuildModeEmbed()) { - $sourceFiles[] = $this->getPhpxDir() . '/src/misc/main.cc'; + $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc'; } if ($this->isBuildModeBin()) { @@ -1640,7 +1643,7 @@ CODE; $userDefines = $this->userDefines; if ($this->isBuildModeLib()) { - $userDefines[] = 'PHPX_NO_MAIN=1'; + $userDefines[] = 'TYPEPHP_NO_MAIN=1'; } return [ @@ -1822,6 +1825,7 @@ CODE; public function genFunctionDeclaration(string $file): void { $code = '#include ' . PHP_EOL; + $code .= '#include ' . PHP_EOL; // 函数的默认值可能会使用字符串字面量,需要提前声明 if ($this->literalStrings) { @@ -3414,6 +3418,12 @@ CODE; } } + if ($this->functionDef->generator) { + $code = $this->genFiberGeneratorFunction($v, $this->functionDef, $name); + $this->resetFunction(); + return $code; + } + // Build SSA/e-SSA analysis for this function if ($v->stmts) { $oriLocalVars = $this->context->localVars; diff --git a/src/gen_stub.php b/src/gen_stub.php index fab12470..0c0a1dcd 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -3796,7 +3796,7 @@ class ClassInfo { $code .= "\n\tstatic zend_object_handlers class_object_handlers;"; $code .= "\n\tmemcpy(&class_object_handlers, class_entry->default_object_handlers, sizeof(zend_object_handlers));"; - $code .= "\n\tclass_object_handlers.unset_property = php_aot_unset_typed_property;"; + $code .= "\n\tclass_object_handlers.unset_property = typephp_unset_typed_property;"; $code .= "\n\tclass_entry->default_object_handlers = &class_object_handlers;"; $code .= "\n"; diff --git a/tests/aot/generator/basic-yield-fallback.phpt b/tests/aot/generator/basic-yield-fallback.phpt new file mode 100644 index 00000000..6390c23f --- /dev/null +++ b/tests/aot/generator/basic-yield-fallback.phpt @@ -0,0 +1,24 @@ +--TEST-- +generator functions via Fiber iterator +--FILE-- + $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" diff --git a/tests/aot/generator/dynamic-foreach-native-generator.phpt b/tests/aot/generator/dynamic-foreach-native-generator.phpt new file mode 100644 index 00000000..9aaf1449 --- /dev/null +++ b/tests/aot/generator/dynamic-foreach-native-generator.phpt @@ -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-- + 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 diff --git a/tests/aot/generator/dynamic-generator-interop.inc b/tests/aot/generator/dynamic-generator-interop.inc new file mode 100644 index 00000000..71d64725 --- /dev/null +++ b/tests/aot/generator/dynamic-generator-interop.inc @@ -0,0 +1,22 @@ + 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; +} diff --git a/tests/aot/generator/generators.phpt b/tests/aot/generator/generators.phpt index 3544449b..8a5f87c4 100644 --- a/tests/aot/generator/generators.phpt +++ b/tests/aot/generator/generators.phpt @@ -1,9 +1,5 @@ --TEST-- Generators - Yield keyword and generator functions ---SKIPIF-- - --FILE-- 3; } -// Test generator sending values -function echo_generator() { - while (true) { - $value = yield; - echo "Received: " . $value . "\n"; - } -} - // Test infinite generator function infinite_sequence() { $i = 1; @@ -88,5 +76,5 @@ int(4) int(5) bool(true) bool(true) -int(1) -int(2) +int(10) +int(11) diff --git a/tests/aot/generator/method-private-property.phpt b/tests/aot/generator/method-private-property.phpt new file mode 100644 index 00000000..5437a93b --- /dev/null +++ b/tests/aot/generator/method-private-property.phpt @@ -0,0 +1,30 @@ +--TEST-- +generator methods can access private properties +--FILE-- +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 diff --git a/tests/aot/generator/method-yield-fallback.phpt b/tests/aot/generator/method-yield-fallback.phpt new file mode 100644 index 00000000..3d02301b --- /dev/null +++ b/tests/aot/generator/method-yield-fallback.phpt @@ -0,0 +1,33 @@ +--TEST-- +generator methods via Fiber iterator +--FILE-- +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 diff --git a/tests/aot/generator/native-foreach-dynamic-generator.phpt b/tests/aot/generator/native-foreach-dynamic-generator.phpt new file mode 100644 index 00000000..d2fd4a99 --- /dev/null +++ b/tests/aot/generator/native-foreach-dynamic-generator.phpt @@ -0,0 +1,26 @@ +--TEST-- +native foreach consumes dynamic PHP generator +--FILE-- + $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 diff --git a/tests/aot/generator/native-generator-foreach-combined.phpt b/tests/aot/generator/native-generator-foreach-combined.phpt new file mode 100644 index 00000000..3693fdb1 --- /dev/null +++ b/tests/aot/generator/native-generator-foreach-combined.phpt @@ -0,0 +1,30 @@ +--TEST-- +native generator and native foreach interoperate +--FILE-- + 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 diff --git a/tests/aot/generator/yield-from-array.phpt b/tests/aot/generator/yield-from-array.phpt new file mode 100644 index 00000000..a426f889 --- /dev/null +++ b/tests/aot/generator/yield-from-array.phpt @@ -0,0 +1,23 @@ +--TEST-- +yield from forwards array keys and values +--FILE-- + 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 diff --git a/tests/aot/generator/yield-from-generator-return.phpt b/tests/aot/generator/yield-from-generator-return.phpt new file mode 100644 index 00000000..fca8d3c3 --- /dev/null +++ b/tests/aot/generator/yield-from-generator-return.phpt @@ -0,0 +1,29 @@ +--TEST-- +yield from forwards delegated generator values and returns its result +--FILE-- + 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 diff --git a/tests/aot/generator/yield-send.phpt b/tests/aot/generator/yield-send.phpt new file mode 100644 index 00000000..9074234a --- /dev/null +++ b/tests/aot/generator/yield-send.phpt @@ -0,0 +1,20 @@ +--TEST-- +yield expression receives send value +--FILE-- +current()); + var_dump($gen->send(42)); +} +?> +--EXPECT-- +int(1) +int(42) diff --git a/tests/aot/loop/iterators.phpt b/tests/aot/loop/iterators.phpt index ba0eca45..6a164d09 100644 --- a/tests/aot/loop/iterators.phpt +++ b/tests/aot/loop/iterators.phpt @@ -1,9 +1,5 @@ --TEST-- Iterable and Iterator - Custom iteration with Traversable ---SKIPIF-- - --FILE--