Native Class 支持 迭代器

master
韩天峰 1 week ago
parent 464de23051
commit 7fece68f12
  1. 9
      docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md
  2. 32
      docs/NATIVE_CLASS_OBJECT.md
  3. 18
      phpunit/code/native-class-foreach-by-reference.php
  4. 11
      phpunit/src/NativeClass/NativeClassValidationTest.php
  5. 146
      src/NativeClass/NativeClassSupportTrait.php
  6. 17
      src/Parser/ForeachTrait.php
  7. 189
      tests/compiler/native-class/iterator.phpt

@ -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 全量测试,留待下一轮统一回归

@ -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` | 支持,使用与 `===` 相同的指针身份规则 |

@ -0,0 +1,18 @@
<?php
#[Native]
class NativeReferenceIterator implements Iterator
{
public function rewind(): void {}
public function valid(): bool { return false; }
public function current(): mixed { return null; }
public function key(): mixed { return null; }
public function next(): void {}
}
function main(): void
{
$iterator = new NativeReferenceIterator();
foreach ($iterator as &$value) {
}
}

@ -1007,11 +1007,18 @@ final class NativeClassValidationTest extends \BaseTest
$this->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');
}
}

@ -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
{

@ -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);

@ -0,0 +1,189 @@
--TEST--
Native class: Iterator and IteratorAggregate lower foreach to direct method calls
--FILE--
<?php
#[Native]
class NativeRangeIterator implements Iterator
{
public int $position = 0;
public int $limit = 0;
public function __construct(int $limit)
{
$this->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]
Loading…
Cancel
Save