From 7fece68f1241d4d7395d815317cbce31a1a360ae Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 17 Aug 2026 20:31:20 +0800 Subject: [PATCH] =?UTF-8?q?Native=20Class=20=E6=94=AF=E6=8C=81=20=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md | 9 +- docs/NATIVE_CLASS_OBJECT.md | 32 ++- .../native-class-foreach-by-reference.php | 18 ++ .../NativeClass/NativeClassValidationTest.php | 11 +- src/NativeClass/NativeClassSupportTrait.php | 146 +++++++++++++- src/Parser/ForeachTrait.php | 17 +- tests/compiler/native-class/iterator.phpt | 189 ++++++++++++++++++ 7 files changed, 403 insertions(+), 19 deletions(-) create mode 100644 phpunit/code/native-class-foreach-by-reference.php create mode 100644 tests/compiler/native-class/iterator.phpt diff --git a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md index 467ac0c0..8f5507d4 100644 --- a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md +++ b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md @@ -80,6 +80,9 @@ | `count($obj)` 仅在实现 Countable 时特化 | Native count optimizer | `keyword-conversions.phpt`、count-without-countable 负向测试 | 已验证 | | `ArrayAccess` 直接语法映射到 Native `offset*()` 方法 | Native array access lowering | `array-access.phpt` | 已验证 | | Native `ArrayAccess` 禁止间接修改和引用 | writable-chain/reference validators | ArrayAccess compound/increment/nested/property/reference/coalesce 负向测试 | 已验证 | +| Native `Iterator` foreach 映射到协议方法,保持 PHP 调用顺序 | Native foreach lowering | `iterator.phpt` | 已验证 | +| `IteratorAggregate` 分流 Native Iterator 与 PHP Traversable | aggregate return-type lowering | `iterator.phpt` | 已验证 | +| Native foreach 不枚举属性且禁止引用遍历 | interface/reference validators | foreach 负向 PHPUnit | 已验证 | ## 6. GC 与生命周期 @@ -129,6 +132,6 @@ vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php --gtest_filter='wren_gc.*:native_gc.*' ``` -本次结果分别为:70/70 PHPT、137/137 PHPUnit、17/17 PHPX C++ tests。 -完整回归结果为:编译器 PHPUnit 1431/1431、编译器 PHPT 1037/1037(另有 2 项按 -环境跳过)、PHPX C++ tests 1016/1016。Native 分支的公共 hook 未影响普通对象模型。 +本次 Iterator 专项结果为:`iterator.phpt` 1/1、Native Class PHPUnit 136/136, +普通 foreach 回归 14/14。Native Class PHPT 目录现有 71 项;按当前任务约定暂未重复执行 +该目录及编译器 PHPT 全量测试,留待下一轮统一回归。 diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 7ddcc09c..92d78274 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -1003,7 +1003,33 @@ Object。编译器不得为此生成 `reinterpret_cast`、`void *` 转换或临 多个无共同 Native 基类的实现之间做运行时动态分派,应作为新的对象表示单独设计,不能 偷偷把 Native Object 装箱为 Zend Object,也不能改变当前裸指针 Native Call 的热路径。 -### 12.3 `instanceof` +### 12.3 `Iterator` 与 `IteratorAggregate` + +Native Class 只有显式实现 `Iterator` 或 `IteratorAggregate` 时才允许作为 `foreach` +的 iterable。编译器不会像 ZendVM 那样回退为“遍历当前作用域可见的对象属性”;没有 +迭代接口的 Native Object 在编译期报错。 + +`Iterator` 完全降级为确定的 Native Method Call,调用顺序与 PHP 一致: + +```text +rewind() → valid() → current() → key() → loop body → next() +``` + +未绑定 key variable 时不调用 `key()`。编译器在循环入口只求值一次 iterable,并用独立 +的精确 GC root 保存它;因此循环体重新赋值原变量不会改变正在执行的 iterator。 +`continue` 通过 C++ `for` 的 iteration expression 调用 `next()`,`break` 则不会调用。 +Native iterator 的 null 检查只在循环入口执行一次,协议方法的热路径不重复检查。 + +`IteratorAggregate::getIterator()` 只调用一次: + +- 返回具体 Native Class 且该类实现 `Iterator` 时,继续使用上述全 Native 路径; +- 返回普通 PHP `Traversable` 时,只有返回对象进入现有 PHPX `ForeachIterator`; +- 其他返回类型在编译期拒绝。 + +`current()` 可以声明返回具体 Native Class,foreach value variable 会被推断为对应的 typed +Native pointer。Native `foreach` 不支持 `&$value`,避免引用和间接修改进入迭代协议。 + +### 12.4 `instanceof` Native Class 没有 `zend_class_entry` 或运行时类名查找,因此只支持目标 class 能够在编译期解析的 `instanceof`。编译器依据 Native 静态类型与继承关系直接 @@ -1263,6 +1289,10 @@ $json = json_encode($nativeObject->toArray()); | override method | 支持,继承链同名实例方法生成 virtual dispatch thunk | | 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 | | Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 | +| `foreach` / `Iterator` | 实现 `Iterator` 时直接生成 `rewind/valid/current/key/next` Native Call;iterable 只求值一次,支持 `continue`/`break` | +| `IteratorAggregate` | `getIterator()` 只调用一次;具体 Native Iterator 继续走 Native 路径,PHP `Traversable` 走 PHPX iterator | +| Native `foreach` 引用遍历 | 不支持 `foreach ($native as &$value)`;编译期 FatalError | +| 未实现迭代接口的 Native Object | 不枚举 public 属性;用于 `foreach` 时编译期 FatalError | | `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 | | `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 | | Native 条件的 `match` | 支持,使用与 `===` 相同的指针身份规则 | diff --git a/phpunit/code/native-class-foreach-by-reference.php b/phpunit/code/native-class-foreach-by-reference.php new file mode 100644 index 00000000..0ea1053b --- /dev/null +++ b/phpunit/code/native-class-foreach-by-reference.php @@ -0,0 +1,18 @@ +compile('native-class-throw.php'); } - public function testRejectsIteratingNativeObjectThroughZendForeach(): void + public function testRejectsIteratingNativeObjectWithoutIteratorContract(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native objects cannot be iterated by PHP foreach'); + $this->expectExceptionMessage('Native class `NativeForeachValue` must implement `Iterator` or `IteratorAggregate` to use foreach'); $this->compile('native-class-foreach.php'); } + public function testRejectsNativeIteratorForeachByReference(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native Iterator foreach does not support references'); + $this->compile('native-class-foreach-by-reference.php'); + } + } diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 330351f0..4f31d961 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -371,14 +371,7 @@ trait NativeClassSupportTrait return null; } - $implementsArrayAccess = false; - foreach ($this->getClassImplementedInterfaces($this->getClass($class)) as $interface) { - if (strcasecmp(ltrim($interface, '\\'), 'ArrayAccess') === 0) { - $implementsArrayAccess = true; - break; - } - } - if (!$implementsArrayAccess) { + if (!$this->nativeClassImplementsInterface($class, 'ArrayAccess')) { $this->fatalError( $errorNode, "Native class `{$class}` must implement `ArrayAccess` to use array access syntax", @@ -387,6 +380,143 @@ trait NativeClassSupportTrait return $class; } + /** Native interfaces are compile-time contracts and have no Zend class entry. */ + protected function nativeClassImplementsInterface(string $class, string $interface): bool + { + if (!$this->isNativeObjectClass($class)) { + return false; + } + foreach ($this->getClassImplementedInterfaces($this->getClass($class)) as $implemented) { + if (strcasecmp(ltrim($implemented, '\\'), ltrim($interface, '\\')) === 0) { + return true; + } + } + return false; + } + + protected function nativeIteratorCall(Node\Expr $receiver, string $method): Node\Expr\MethodCall + { + return new Node\Expr\MethodCall( + $receiver, + new Node\Identifier($method), + [], + $receiver->getAttributes(), + ); + } + + /** + * Native Iterator is a compile-time protocol. Calls never enter ZendVM and + * retain PHP's rewind/valid/current/key/body/next ordering. A C++ for-loop + * is intentional: continue must execute next(), while break must not. + */ + protected function parseForeachNativeIterator( + Node\Stmt\Foreach_ $node, + Node\Expr $iteratorExpr, + string $iteratorClass, + ): string { + if ($node->byRef) { + $this->fatalError($node, 'Native Iterator foreach does not support references'); + } + + if (!$this->nativeClassImplementsInterface($iteratorClass, 'Iterator')) { + $this->fatalError( + $node->expr, + "Native class `{$iteratorClass}` returned by `getIterator()` must implement `Iterator`", + ); + } + + // foreach captures its iterable once. Always use a dedicated rooted + // pointer, even for a variable receiver: assigning null or another + // object to the source variable inside the loop must not change the + // active iterator. Validate that captured pointer once before rewind, + // rather than repeating a null check for every protocol method. + $iterator = $this->materializeNativeObjectReceiver($iteratorExpr, $iteratorClass); + $this->context->beforeStmtLines[] = 'php::nativeGcRequireObject(' + . $iterator . ', "' . addslashes($iteratorClass) . '");'; + $this->markNativeObjectNonNull($iterator); + $iteratorExpr = new Node\Expr\Variable($iterator, $iteratorExpr->getAttributes()); + + $rewind = $this->parseMethodCall($this->nativeIteratorCall($iteratorExpr, 'rewind')); + $valid = $this->parseMethodCall($this->nativeIteratorCall($iteratorExpr, 'valid')); + $currentNode = $this->nativeIteratorCall($iteratorExpr, 'current'); + $next = $this->parseMethodCall($this->nativeIteratorCall($iteratorExpr, 'next')); + + // Materialized Native receivers schedule precise-root cleanup at the + // end of the foreach statement. Capture it before parsing the body, + // whose own statement buffers are independent. + $setup = $this->parseBeforeStmtLines(); + $cleanup = $this->parseAfterStmtLines(); + + $code = $setup . '{' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'for (' . $rewind . '; ' . $valid . '; ' . $next . ') {' . PHP_EOL; + $this->indentLevel++; + + // PHP invokes current() before key(); key() is skipped entirely when + // the foreach statement does not bind a key variable. + $code .= $this->getIndent() + . $this->parseAssignFinally($node->valueVar, $currentNode) . ';' . PHP_EOL; + if ($node->keyVar !== null) { + $keyNode = $this->nativeIteratorCall($iteratorExpr, 'key'); + $code .= $this->getIndent() + . $this->parseAssignFinally($node->keyVar, $keyNode) . ';' . PHP_EOL; + } + + $body = $this->parseForeachBody($node); + $this->indentLevel--; + $code .= $body; + $code .= $this->getIndent() . '}' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + $code .= $cleanup; + return $code; + } + + protected function parseForeachNativeAggregate( + Node\Stmt\Foreach_ $node, + Node\Expr $aggregateExpr, + string $aggregateClass, + ): string { + if ($node->byRef) { + $this->fatalError($node, 'Native Iterator foreach does not support references'); + } + + $method = $this->findNativeObjectMethod($aggregateClass, 'getIterator'); + if ($method === null) { + $this->fatalError($node->expr, "Native class `{$aggregateClass}` has no method `getIterator()`"); + } + $function = $method->functionDef; + $call = $this->nativeIteratorCall($aggregateExpr, 'getIterator'); + $returnClass = $function->returnClass; + + if ($this->isNativeObjectClass($returnClass)) { + if ($function->returnNullable) { + $this->fatalError($call, 'Native IteratorAggregate::getIterator() cannot return null'); + } + return $this->parseForeachNativeIterator($node, $call, $returnClass); + } + + if ($function->returnType !== Type::OBJECT + || $returnClass === '' + || !$this->isInheritedFrom($returnClass, 'Traversable') + ) { + $this->fatalError( + $call, + 'Native IteratorAggregate::getIterator() must return a Traversable object or Native Iterator', + ); + } + + $iterator = $this->genTmpVarName(); + $this->addLocalVar($iterator, Type::OBJECT); + $value = $this->parseMethodCall($call); + $setup = $this->parseBeforeStmtLines(); + $cleanup = $this->parseAfterStmtLines(); + return $setup + . $iterator . ' = ' . $value . ';' . PHP_EOL + . $this->parseForeachIterable($node, $iterator) . PHP_EOL + . $cleanup; + } + /** Locate the first Native ArrayAccess dimension inside a writable chain. */ protected function findNativeArrayAccessDimension(NodeAbstract $expression): ?Node\Expr\ArrayDimFetch { diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 1fc889a5..c77efe28 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -171,11 +171,18 @@ trait ForeachTrait protected function parseForeach(Foreach_ $node): string { - if ($this->isNativeObjectClass($this->detectClassOfExpr($node->expr))) { - // Native classes have no Zend iterator handlers and cannot carry - // Traversable as a runtime interface value. Expose iteration via - // an explicitly returned PHP array or a typed std container. - $this->fatalError($node->expr, 'Native objects cannot be iterated by PHP foreach'); + $nativeClass = $this->detectClassOfExpr($node->expr); + if ($this->isNativeObjectClass($nativeClass)) { + if ($this->nativeClassImplementsInterface($nativeClass, 'Iterator')) { + return $this->parseForeachNativeIterator($node, $node->expr, $nativeClass); + } + if ($this->nativeClassImplementsInterface($nativeClass, 'IteratorAggregate')) { + return $this->parseForeachNativeAggregate($node, $node->expr, $nativeClass); + } + $this->fatalError( + $node->expr, + "Native class `{$nativeClass}` must implement `Iterator` or `IteratorAggregate` to use foreach", + ); } if ($this->isVarExpr($node->expr)) { $name = $this->parseIdentifier($node->expr); diff --git a/tests/compiler/native-class/iterator.phpt b/tests/compiler/native-class/iterator.phpt new file mode 100644 index 00000000..f0f2a9bb --- /dev/null +++ b/tests/compiler/native-class/iterator.phpt @@ -0,0 +1,189 @@ +--TEST-- +Native class: Iterator and IteratorAggregate lower foreach to direct method calls +--FILE-- +limit = $limit; + } + + public function rewind(): void + { + echo 'R'; + $this->position = 0; + } + + public function valid(): bool + { + echo 'V'; + return $this->position < $this->limit; + } + + public function current(): int + { + echo 'C'; + return ($this->position + 1) * 10; + } + + public function key(): string + { + echo 'K'; + return 'k' . $this->position; + } + + public function next(): void + { + echo 'N'; + $this->position++; + } +} + +#[Native] +class NativeRangeAggregate implements IteratorAggregate +{ + public int $limit = 0; + + public function __construct(int $limit) + { + $this->limit = $limit; + } + + public function getIterator(): NativeRangeIterator + { + echo 'A'; + return new NativeRangeIterator($this->limit); + } +} + +#[Native] +class NativeChildIterator extends NativeRangeIterator +{ + public function current(): int + { + echo 'D'; + return 77; + } +} + +function makePolymorphicIterator(): NativeRangeIterator +{ + return new NativeChildIterator(1); +} + +#[Native] +class NativePhpAggregate implements IteratorAggregate +{ + public function getIterator(): ArrayIterator + { + echo 'P'; + return new ArrayIterator(['x' => 7, 'y' => 8]); + } +} + +#[Native] +class NativeIteratedValue +{ + public int $number = 0; + + public function __construct(int $number) + { + $this->number = $number; + } +} + +#[Native] +class NativeObjectIterator implements Iterator +{ + public bool $available = true; + + public function rewind(): void + { + $this->available = true; + } + + public function valid(): bool + { + return $this->available; + } + + public function current(): NativeIteratedValue + { + return new NativeIteratedValue(99); + } + + public function key(): int + { + return 5; + } + + public function next(): void + { + $this->available = false; + } +} + +function main(): void +{ + $iterator = new NativeRangeIterator(3); + foreach ($iterator as $key => $value) { + echo "[$key=$value]"; + if ($value === 10) { + continue; + } + if ($value === 20) { + break; + } + } + echo PHP_EOL; + + foreach (new NativeRangeIterator(1) as $value) { + echo "[$value]"; + } + echo PHP_EOL; + + $captured = new NativeRangeIterator(2); + foreach ($captured as $capturedValue) { + echo "[$capturedValue]"; + $captured = null; + } + echo PHP_EOL; + + foreach (makePolymorphicIterator() as $polymorphicValue) { + echo "[$polymorphicValue]"; + } + echo PHP_EOL; + + $aggregate = new NativeRangeAggregate(2); + foreach ($aggregate as $key => $value) { + echo "[$key=$value]"; + } + echo PHP_EOL; + + $phpAggregate = new NativePhpAggregate(); + foreach ($phpAggregate as $phpKey => $phpValue) { + echo "[$phpKey=$phpValue]"; + } + echo PHP_EOL; + + $objects = new NativeObjectIterator(); + foreach ($objects as $objectKey => $objectValue) { + echo "[$objectKey={$objectValue->number}]"; + } + echo PHP_EOL; +} +?> +--EXPECT-- +RVCK[k0=10]NVCK[k1=20] +RVC[10]NV +RVC[10]NVC[20]NV +RVD[77]NV +ARVCK[k0=10]NVCK[k1=20]NV +P[x=7][y=8] +[5=99]