From af3472bd9ae41faf7e32a4eea6b2045eeb8010f5 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 9 Jul 2026 11:37:30 +0800 Subject: [PATCH] feat(parser): add native property assignment and compound assignment support - Implement dynamic native property write detection logic - Add shouldUseDynamicNativePropertyWrite method for type checking - Support native property assignment with type conversion - Handle compound assignment operations for native properties - Add canUseNativePropertyAssignOp validation method - Create comprehensive documentation for compile-time functions - Add test cases for native int property assignments - Include unit tests for native property assignment operations - Update incompatible PHP features documentation - Add native property assignment to test suite --- docs/COMPILE_TIME_FUNCTIONS.md | 92 +++++++++++++++++++ docs/INCOMPATIBLE_PHP_FEATURES.md | 1 + docs/README.md | 13 +++ .../code/native-property-assign-op-int.php | 19 ++++ phpunit/src/NativePropertyTest.php | 16 ++++ src/Php/Parser/AssignOpTrait.php | 74 ++++++++++++++- .../native-int-property-assign-op-var.phpt | 43 +++++++++ .../native-int-property-assign.phpt | 68 ++++++++++++++ .../native-int-property-string-var.phpt | 28 ++++++ 9 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 docs/COMPILE_TIME_FUNCTIONS.md create mode 100644 phpunit/code/native-property-assign-op-int.php create mode 100644 tests/aot/object_property/native-int-property-assign-op-var.phpt create mode 100644 tests/aot/object_property/native-int-property-assign.phpt create mode 100644 tests/aot/object_property/native-int-property-string-var.phpt diff --git a/docs/COMPILE_TIME_FUNCTIONS.md b/docs/COMPILE_TIME_FUNCTIONS.md new file mode 100644 index 00000000..a8ad3d24 --- /dev/null +++ b/docs/COMPILE_TIME_FUNCTIONS.md @@ -0,0 +1,92 @@ +# AOT 编译期函数与关键词方法 + +本文档记录 AOT 编译器专有的编译期函数、关键词方法和相关构造入口。它们不是标准 PHP 语法的一部分,普通 PHP 运行时只能依赖 `src/polyfills.php` 提供的兼容占位。 + +## 核心编译期函数 + +当前核心全局编译期函数共 3 个。 + +| 名称 | 参数 | 作用 | 当前主要处理位置 | +| --- | --- | --- | --- | +| `any($value)` | 1 个 | 将表达式降级为 `mixed/any`,阻止继续按静态 native/object 类型处理。 | 赋值右值路径中特判。 | +| `refval($target)` | 1 个 | 显式把变量、数组元素或对象属性作为引用传给动态调用或无法静态识别引用参数的调用。 | 参数解析、动态调用、SSA/优化器引用逃逸分析。 | +| `objval($value, ClassName::class 或 'ClassName')` | 2 个 | 告诉编译器 `$value` 是指定类对象,并生成 `php::toObject(..., target_ce)` 运行时兜底检查。 | 函数调用解析、对象类型推导。 | + +约束: + +- `refval()` 只接受变量、数组元素或对象属性。 +- `objval()` 第二个参数必须是编译期可解析的类名字符串或 `ClassName::class`。 +- `any()` 语义上应是任意表达式位置可用的编译期标记;当前实现仍有路径差异,后续应统一到表达式解析入口,而不是只在部分赋值路径中处理。 + +## 关键词方法 + +当前内置关键词方法共 12 个。 + +| 名称 | 等价行为 | 说明 | +| --- | --- | --- | +| `toAny()` | `any($receiver)` | 返回接收者本身,但类型降级为 `mixed/any`。 | +| `toRef()` | `refval($receiver)` | 返回接收者引用;参数限制与 `refval()` 一致。 | +| `toObject()` | `php::toObject($receiver)` | 可带目标类参数,执行对象转换/检查。 | +| `toInt()` | `php::toInt($receiver)` | 转为 native int 表达式。 | +| `toFloat()` | `php::toFloat($receiver)` | 转为 native float 表达式。 | +| `toString()` | `php::toString($receiver)` | 转为字符串表达式。 | +| `toBool()` | `php::toBool($receiver)` | 转为 bool 表达式。 | +| `toArray()` | `php::toArray($receiver)` | 转为数组表达式。 | +| `toStream()` | `php::toStream($receiver)` | 转为 stream 表达式。 | +| `toBigInt()` | `php::BigInt::newInstance($receiver)` | 构造 BigInt。 | +| `toBigFloat()` | `php::BigFloat::newInstance($receiver)` | 构造 BigFloat。 | +| `toDecimal()` | `php::Decimal::newInstance($receiver)` | 构造 Decimal。 | + +约束: + +- `toAny()`、`toRef()` 不接受参数。 +- `toRef()` 只适用于可取引用的接收者。 +- 关键词方法优先于普通方法和 universal method 分派。 + +## `std::` 编译期构造入口 + +当前 `std::` 编译期构造入口共 10 个。 + +| 名称 | 作用 | 主要限制 | +| --- | --- | --- | +| `std::int($value)` | 显式创建 native int 表达式。 | 需要 1 个值参数。 | +| `std::float($value)` | 显式创建 native float 表达式。 | 需要 1 个值参数。 | +| `std::bool($value)` | 显式创建 native bool 表达式。 | 需要 1 个值参数。 | +| `std::bigInt($value)` | 构造 BigInt。 | 不允许从 float 变量隐式构造。 | +| `std::decimal($value)` | 构造 Decimal。 | float 变量需改用字符串或整型;float 字面量会按原始字面量处理。 | +| `std::bigFloat($value)` | 构造 BigFloat。 | 需要 1 个值参数。 | +| `std::array($type, $size[, ...$sizes])` | 构造固定大小 std array。 | 只能在变量首次赋值的顶层作用域使用。 | +| `std::vector($type[, $size])` | 构造 std vector。 | 只能在变量首次赋值的顶层作用域使用。 | +| `std::map($keyType, $valueType)` | 构造 std map。 | 只能在变量首次赋值的顶层作用域使用。 | +| `std::ordered_map($keyType, $valueType)` | 构造 std ordered map。 | 只能在变量首次赋值的顶层作用域使用。 | + +## Std 容器转换关键词方法 + +当前 Std 容器转换关键词方法共 4 个。 + +| 名称 | 作用 | 主要限制 | +| --- | --- | --- | +| `toStdArray(...)` | 将变量包装为 std array。 | 只能在变量首次赋值的顶层作用域使用。 | +| `toStdVector(...)` | 将变量包装为 std vector。 | 只能在变量首次赋值的顶层作用域使用。 | +| `toStdMap(...)` | 将变量包装为 std map。 | 只能在变量首次赋值的顶层作用域使用。 | +| `toStdOrderedMap(...)` | 将变量包装为 std ordered map。 | 只能在变量首次赋值的顶层作用域使用。 | + +## 不计入本文清单的机制 + +- `$array->any()` 是 universal method,映射到 PHP `array_any()`,不是 `any()` 编译期函数。 +- `native_types::type_*`、`complex_types::type_*` 是编译期类型描述常量,不是函数。 +- keyword extension method 是用户自定义扩展方法机制,不属于固定内置编译期函数清单。 + +## 当前实现风险 + +编译期函数应当在任意合法表达式位置可用,并且在所有路径上保持一致语义。当前代码中仍存在处理入口分散的问题: + +- `any()` 主要在赋值右值路径中被特殊识别,表达式参数、二元运算、返回值等位置可能走普通函数调用或依赖 polyfill。 +- `refval()` / `toRef()` 在参数解析和动态调用路径中特判较多,后续应统一为一个“引用包装表达式”解析入口。 +- `objval()` 当前通过函数调用解析和类型推导路径识别,整体较集中。 + +后续重构目标: + +- 建立统一的 `CompileTimeFunctionResolver` 或等价模块。 +- 在 `parseExpr()` / `detectTypeOfExpr()` / `detectClassOfExpr()` / 参数解析路径中复用同一份编译期函数元信息。 +- 保证 `any()`、`refval()`、`objval()` 在任意表达式位置行为一致。 diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 78432544..b6745ce0 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -44,6 +44,7 @@ - 禁止子类覆盖父类私有属性。 - `parent::method()` 的方法名必须是字面量。 - 通过变量持有的 clone 对象写入私有 typed property 时,可能无法完全复现 PHP 的私有属性访问语义。 +- 为避免 typed property 写入路径引入额外动态检查,native typed property 在右值类型不确定或与属性类型不一致时会退化为 `setProperty()`;部分标量赋值可能遵循 Zend 弱类型转换,而不是 AOT 默认 strict 语义。 - constructor property promotion 的运行时属性可用,但 `ReflectionProperty::isPromoted()` 目前不返回标准 PHP 结果。 ## 表达式与控制流 diff --git a/docs/README.md b/docs/README.md index 4b7125c9..85a3a240 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,19 @@ --- +### 1.2 [AOT 编译期函数与关键词方法](COMPILE_TIME_FUNCTIONS.md) +**必读指数**: ⭐⭐⭐⭐ + +内容概要: +- AOT 专用编译期函数清单 +- `any()`、`refval()`、`objval()` 的语义和限制 +- `toAny()`、`toRef()` 等关键词方法 +- 当前实现风险和后续统一方向 + +**适合人群**: 所有使用者、贡献者、框架适配者 + +--- + ### 2. [快速入门指南](QUICKSTART.md) **必读指数**: ⭐⭐⭐⭐⭐ diff --git a/phpunit/code/native-property-assign-op-int.php b/phpunit/code/native-property-assign-op-int.php new file mode 100644 index 00000000..c90c9f9a --- /dev/null +++ b/phpunit/code/native-property-assign-op-int.php @@ -0,0 +1,19 @@ +value += 2; + return $this->value; + } +} + +function native_property_assign_op_int_object(): int +{ + $box = new NativePropertyAssignOpIntBox(); + $box->value += 2; + return $box->value; +} diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index e1c8763f..8809f60c 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -42,6 +42,22 @@ class NativePropertyTest extends \BaseTest $this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count") = php::toInt(value)', $code); } + public function testNativeIntPropertyAssignOpUsesNativeReference(): void + { + try { + $outputFile = $this->compileNativeProperty('native-property-assign-op-int.php'); + } catch (TestError $e) { + $this->fail($e->getMessage()); + } + + $code = file_get_contents($outputFile); + $this->assertStringContainsString('php_aot_static_int_ref(this_.attr(', $code); + $this->assertStringContainsString('php_aot_static_int_ref(box.attr(', $code); + $this->assertSame(2, substr_count($code, 'php_aot_static_int_ref(')); + $this->assertStringNotContainsString('this_.attr(php_get_prop(0, _literal_strings[0], 0, _literal_strings[1]), true) +=', $code); + $this->assertStringNotContainsString('box.attr(php_get_prop(0, _literal_strings[0], 0, _literal_strings[1]), true) +=', $code); + } + public function testCannotAccessPrivateNativePropertyFromUnrelatedClass(): void { $this->exec('Cannot access private property `value` of class `NativePrivateOwner`', 'native-property-private-other-class.php'); diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 2f359686..e8ff7092 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -153,6 +153,10 @@ trait AssignOpTrait $type = self::TYPE_VAR; } + if ($propertyWriteTarget !== null && $this->shouldUseDynamicNativePropertyWrite($left, $type)) { + return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget); + } + if ($this->isVarExpr($left)) { $var = $this->parseWritableIdentifier($left); if ($var === 'this_') { @@ -304,6 +308,9 @@ trait AssignOpTrait } $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); + if ($propertyWriteTarget !== null && ($propertyDef = $this->getNativePropertyDef($left)) !== null) { + return $var . ' = ' . $this->convertExprFromType($propertyDef->type, $rightExpr); + } if ($finalVarType === self::TYPE_VAR) { return $var . ' = ' . $rightExpr; } else { @@ -311,6 +318,25 @@ trait AssignOpTrait } } + protected function shouldUseDynamicNativePropertyWrite(Expr $left, string $rightType): bool + { + if (!$this->isPropertyFetch($left)) { + return false; + } + + $def = $this->getNativePropertyDef($left); + if ($def === null) { + return false; + } + + if ($rightType === self::TYPE_VAR) { + return true; + } + + return in_array($def->type, [self::TYPE_INT, self::TYPE_FLOAT, self::TYPE_BOOL, self::TYPE_STR], true) + && $rightType !== $def->type; + } + protected function parseStdContainerCopyAssign(string $leftVar, Expr $right): ?string { $rightInfo = $this->getStdContainerExprInfo($right); @@ -346,11 +372,17 @@ trait AssignOpTrait protected function parseAssignOp(Expr\AssignOp $node, string $op): string { $this->assertNotNullsafeWriteContext($node->var); - $var = $this->parseWritableIdentifier($node->var); - $expr = $this->parseIdentifier($node->expr); $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); $this->guardLiteralDivisionByZero($node->expr, $op); + $nativePropertyAssignOp = $this->parseNativePropertyAssignOp($node, $op); + if ($nativePropertyAssignOp !== null) { + return $nativePropertyAssignOp; + } + + $var = $this->parseWritableIdentifier($node->var); + $expr = $this->parseIdentifier($node->expr); + if ($this->isVarExpr($node->var)) { if (!$this->hasVar($var)) { $this->fatalError($node->var, 'Cannot assign to undefined variable'); @@ -443,6 +475,44 @@ trait AssignOpTrait return $var . ' ' . $op . ' (' . $expr . ')'; } + protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op): ?string + { + if (!$this->isPropertyFetch($node->var)) { + return null; + } + + $def = $this->getNativePropertyDef($node->var); + if ($def === null) { + return null; + } + + $rightType = $this->detectTypeOfExpr($node->expr); + if (!$this->canUseNativePropertyAssignOp($def->type, $rightType, $op)) { + return null; + } + + $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'; + $var = $helper . '(' . $var . '.unwrap_ptr())'; + } + + return $var . ' ' . $op . ' (' . $this->convertExprFromType($def->type, $this->parseIdentifier($node->expr)) . ')'; + } + + protected function canUseNativePropertyAssignOp(string $propertyType, string $rightType, string $op): bool + { + if ($propertyType !== $rightType) { + return false; + } + + return match ($propertyType) { + self::TYPE_INT => in_array($op, ['+=', '-=', '*=', '%=', '<<=', '>>=', '&=', '|=', '^='], true), + self::TYPE_FLOAT => in_array($op, ['+=', '-=', '*=', '/='], true), + default => false, + }; + } + protected function parseBigAssignOp(Expr\AssignOp $node, string $var, string $type, string $expr, string $rightType, string $op): string { $binaryOp = $this->removeAssignOp($op); diff --git a/tests/aot/object_property/native-int-property-assign-op-var.phpt b/tests/aot/object_property/native-int-property-assign-op-var.phpt new file mode 100644 index 00000000..a2e34b34 --- /dev/null +++ b/tests/aot/object_property/native-int-property-assign-op-var.phpt @@ -0,0 +1,43 @@ +--TEST-- +Native int property compound assignment from var +--FILE-- +value += $delta; + } +} + +function main(): void +{ + $box = new NativeIntAssignOpBox(); + $delta = any(2); + $box->value += $delta; + var_dump($box->value); + + $text = any("3"); + $box->value += $text; + var_dump($box->value); + + $bad = any("abc"); + try { + $box->value += $bad; + } catch (TypeError $e) { + var_dump($e->getMessage()); + } + + $selfBox = new NativeIntAssignOpBox(); + $selfDelta = any(5); + $selfBox->add($selfDelta); + var_dump($selfBox->value); +} +?> +--EXPECT-- +int(3) +int(6) +string(39) "Unsupported operand types: int + string" +int(6) diff --git a/tests/aot/object_property/native-int-property-assign.phpt b/tests/aot/object_property/native-int-property-assign.phpt new file mode 100644 index 00000000..6c438bdf --- /dev/null +++ b/tests/aot/object_property/native-int-property-assign.phpt @@ -0,0 +1,68 @@ +--TEST-- +Native int property assignment converts RHS without explicit cast +--FILE-- +size = 120; + $rows[1]->size = 30; + + $cr = new Cell(); + $cr->style = new stdClass(); + $cr->children = ['a', 'b']; + + $gi = new GridItem(); + $gi->colStart = $idx % $numCols; + $gi->colEnd = $gi->colStart + 1; + $gi->rowStart = $idx / $numCols; + $gi->rowEnd = $gi->rowStart + 1; + $gi->w = $cols[$gi->colStart]->size; + $gi->h = $gi->rowStart < count($rows) ? $rows[$gi->rowStart]->size : 50; + $gi->style = $cr->style; + $gi->originalChildren = $cr->children; + + var_dump($gi->colStart, $gi->colEnd, $gi->rowStart, $gi->rowEnd, $gi->w, $gi->h); + var_dump($gi->style instanceof stdClass, $gi->originalChildren); +} +?> +--EXPECT-- +int(2) +int(3) +int(1) +int(2) +int(120) +int(30) +bool(true) +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} diff --git a/tests/aot/object_property/native-int-property-string-var.phpt b/tests/aot/object_property/native-int-property-string-var.phpt new file mode 100644 index 00000000..94d13a1b --- /dev/null +++ b/tests/aot/object_property/native-int-property-string-var.phpt @@ -0,0 +1,28 @@ +--TEST-- +Native int property assignment from string var uses setProperty fallback +--FILE-- +value = $numeric; + var_dump($box->value); + + $bad = "abc"; + try { + $box->value = $bad; + } catch (TypeError $e) { + var_dump($e->getMessage()); + } +} +?> +--EXPECT-- +int(123) +string(74) "Cannot assign string to property NativeIntStringVarBox::$value of type int"