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
pull/15/head
韩天峰 2 months ago
parent d767104e1c
commit af3472bd9a
  1. 92
      docs/COMPILE_TIME_FUNCTIONS.md
  2. 1
      docs/INCOMPATIBLE_PHP_FEATURES.md
  3. 13
      docs/README.md
  4. 19
      phpunit/code/native-property-assign-op-int.php
  5. 16
      phpunit/src/NativePropertyTest.php
  6. 74
      src/Php/Parser/AssignOpTrait.php
  7. 43
      tests/aot/object_property/native-int-property-assign-op-var.phpt
  8. 68
      tests/aot/object_property/native-int-property-assign.phpt
  9. 28
      tests/aot/object_property/native-int-property-string-var.phpt

@ -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()` 在任意表达式位置行为一致。

@ -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 结果。
## 表达式与控制流

@ -34,6 +34,19 @@
---
### 1.2 [AOT 编译期函数与关键词方法](COMPILE_TIME_FUNCTIONS.md)
**必读指数**: ⭐⭐⭐⭐
内容概要:
- AOT 专用编译期函数清单
- `any()`、`refval()`、`objval()` 的语义和限制
- `toAny()`、`toRef()` 等关键词方法
- 当前实现风险和后续统一方向
**适合人群**: 所有使用者、贡献者、框架适配者
---
### 2. [快速入门指南](QUICKSTART.md)
**必读指数**: ⭐⭐⭐⭐⭐

@ -0,0 +1,19 @@
<?php
class NativePropertyAssignOpIntBox
{
public int $value = 1;
public function addThis(): int
{
$this->value += 2;
return $this->value;
}
}
function native_property_assign_op_int_object(): int
{
$box = new NativePropertyAssignOpIntBox();
$box->value += 2;
return $box->value;
}

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

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

@ -0,0 +1,43 @@
--TEST--
Native int property compound assignment from var
--FILE--
<?php
class NativeIntAssignOpBox
{
public int $value = 1;
public function add($delta): void
{
$this->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)

@ -0,0 +1,68 @@
--TEST--
Native int property assignment converts RHS without explicit cast
--FILE--
<?php
class GridSize
{
public int $size = 0;
}
class GridItem
{
public int $colStart = 0;
public int $colEnd = 0;
public int $rowStart = 0;
public int $rowEnd = 0;
public int $w = 0;
public int $h = 0;
public ?object $style = null;
public ?array $originalChildren = null;
}
class Cell
{
public ?object $style = null;
public ?array $children = null;
}
function main(): void
{
$idx = 5;
$numCols = 3;
$cols = [new GridSize(), new GridSize(), new GridSize()];
$rows = [new GridSize(), new GridSize()];
$cols[2]->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"
}

@ -0,0 +1,28 @@
--TEST--
Native int property assignment from string var uses setProperty fallback
--FILE--
<?php
class NativeIntStringVarBox
{
public int $value = 0;
}
function main(): void
{
$box = new NativeIntStringVarBox();
$numeric = "123";
$box->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"
Loading…
Cancel
Save