diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 9ec1a32e..78562758 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -239,12 +239,22 @@ - `findNativeStaticProperty()` 已通过 resolver 的 `resolveNativeStaticProperty()` 显式接口完成静态属性检查。 - nullsafe 属性链检查已通过 resolver 的 `resolveNullsafePropertyChain()` 显式接口完成类名推进和可见性检查。 - 旧的 `CompilerBase::findNativeProperty()` 泛型入口已移除,避免后续继续扩散带 `$static` 布尔参数的访问模式。 +- 旧的 `findNativeStaticProperty(..., &$class)` by-ref 协议已移除,静态属性读取改为 `StaticPropertyFetchTarget` 和 `StaticPropertyFetchResolution` 显式 DTO。 +- 实例属性读取的目标类解析已抽离为 `InstancePropertyFetchTarget`,`getPropertyIdentifier()` 不再混合目标解析、resolver 调用和动态 fallback 分支。 +- 原先散落在 AST attribute 上的 `nativeProperty`、`nativePropertyDef`、`nativeClassDef` 已合并为 `NativePropertyAccess` metadata,避免三者状态不一致。 +- `nativePropertyVar`、`nativePropertyValueSource`、`objectProps`、`staticPropRefs` 的直接读写已收敛到 helper 方法;业务路径不再通过字符串内容判断属性访问语义。 +- typed instance property hoist 和 typed static property ref 注册已提取为独立 helper,当前仍保持原有生成代码结构。 - `CompilerBase::isSameClassName()`、`isSameOrSubclassOf()`、`canAccessProtectedProperty()` 已委托 resolver,避免规则继续扩散。 - 已添加 `prepare/convert/idle` 编译阶段状态;`PropertyAccessResolver` 只能在 convert 阶段创建和使用,避免预处理阶段误用不完整的类表状态。 - `PropertyAccessResolver` 已改为依赖 `PropertyAccessContext` 只读接口,而不是完整依赖 `CompilerBase` 大类。 - 已建立 `PropertyAssignTypeInfo`,抽离 typed property 写入的纯 metadata 计算,包括固定类型属性判断、默认值、runtime typecheck 列表和类型字符串。 - 当前迁移保持生成代码不变,后续阶段再统一 read/write emitter。 +状态: + +- 阶段 1 已基本收尾。后续除非发现属性读取 resolver 绕过或行为回归,否则不再继续扩大阶段 1 范围。 +- 阶段 2 已开始;属性写入相关的 assignment、compound assignment、inc/dec、unset、refval 路径仍需继续统一。 + 验证: - `phpunit/src/NativePropertyTest.php` @@ -277,6 +287,15 @@ - nullsafe 写上下文错误测试。 - private/protected/static 属性错误测试。 +当前进展: + +- 阶段 2 已开始。 +- 已建立 `PropertyWriteTarget` 作为属性写入路径的最小目标 DTO。 +- 普通赋值和 `??=` 已接入 `preparePropertyWriteTarget()`,在写入前统一完成属性 target 准备,并通过 `assertCanAssignPropertyWrite()` 与 `wrapPropertyWriteTypeCheck()` 执行静态检查和 runtime typecheck 包装。 +- dynamic object property 的 `getProperty()` / `setProperty()` 生成已收敛到 `emitDynamicPropertyRead()` / `emitDynamicPropertyWrite()` helper;普通动态属性赋值、复合赋值、自增自减已复用该入口。 +- 复合赋值的动态属性路径已接入 `preparePropertyWriteTarget()`,先统一完成属性写入 target 准备和静态检查。 +- 当前步骤保持生成代码不变;后续继续收敛 dynamic/native property write emitter、compound assignment、inc/dec、unset 和 refval 路径。 + ### 阶段 3:类型系统模块化 目标: diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index add3b483..f7c5afcb 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -49,6 +49,7 @@ use PhpAot\Php\Resolver\NativePropertyAccess; use PhpAot\Php\Resolver\PropertyAccessResult; use PhpAot\Php\Resolver\PropertyAccessResolver; use PhpAot\Php\Resolver\PropertyAssignTypeInfo; +use PhpAot\Php\Resolver\PropertyWriteTarget; use PhpAot\Php\Resolver\StaticPropertyFetchResolution; use PhpAot\Php\Resolver\StaticPropertyFetchTarget; use PhpParser\Modifiers; @@ -2828,10 +2829,10 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $this->addLocalVar($tmpVar, self::TYPE_VAR); if ($isPre) { - $this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName}) {$op} 1; {$obj}.setProperty({$propName}, {$tmpVar});"; + $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . " {$op} 1; " . $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';'; } else { - $this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName});"; - $this->context->afterStmtLines[] = "{$obj}.setProperty({$propName}, {$tmpVar} {$op} 1);"; + $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . ';'; + $this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, "{$tmpVar} {$op} 1") . ';'; } return $tmpVar; @@ -4701,6 +4702,35 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $this->assertCanAssignObjectProperty($left, $right, 'static property'); } + protected function preparePropertyWriteTarget(NodeAbstract $left): ?PropertyWriteTarget + { + if ($left instanceof Expr\PropertyFetch) { + if ($this->isIdExpr($left->name)) { + $this->getPropertyIdentifier($left, $left->var, $left->name); + } + return new PropertyWriteTarget($left, 'object property'); + } + + if ($left instanceof Expr\StaticPropertyFetch) { + if ($this->isIdExpr($left->name)) { + $this->resolveNativeStaticPropertyFetch($left); + } + return new PropertyWriteTarget($left, 'static property'); + } + + return null; + } + + protected function assertCanAssignPropertyWrite(PropertyWriteTarget $target, Expr $right): void + { + $this->assertCanAssignObjectProperty($target->node, $right, $target->label); + } + + protected function wrapPropertyWriteTypeCheck(PropertyWriteTarget $target, Expr $right, string $rightExpr): string + { + return $this->wrapObjectPropertyAssignTypeCheck($target->node, $right, $rightExpr); + } + private function assertCanAssignObjectProperty(NodeAbstract $left, Expr $right, string $label): void { $def = $this->getNativePropertyDef($left); @@ -5442,6 +5472,16 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } } + protected function emitDynamicPropertyRead(string $object, string $property): string + { + return "{$object}.getProperty({$property})"; + } + + protected function emitDynamicPropertyWrite(string $object, string $property, string $value): string + { + return "{$object}.setProperty({$property}, {$value})"; + } + protected function getChainedFunc(string $op): string { return match ($op) { diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 7f1f8c9c..11d5331a 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -54,7 +54,7 @@ trait AssignOpTrait $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, self::TYPE_VAR); // Comma expression: store RHS → execute side effect → evaluate to stored value - return '((' . $tmp . ' = ' . $rightExpr . ', ' . "{$array}.setProperty({$propName}, {$tmp})" . '), ' . $tmp . ')'; + return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyWrite($array, $propName, $tmp) . '), ' . $tmp . ')'; } protected function parseRightAssociativeAssign(NodeAbstract $left, Expr\Assign $right): string @@ -152,6 +152,7 @@ trait AssignOpTrait $this->context->inAssignExpr = true; $var = $this->parseIdentifier($left); $this->context->inAssignExpr = $oriInAssignExpr; + $propertyWriteTarget = $this->preparePropertyWriteTarget($left); if ($var === 'this_') { $this->fatalError($left, 'Cannot re-assign $this'); } @@ -273,15 +274,13 @@ trait AssignOpTrait return $this->parseAssignArrayDim($left, $right); } - if ($this->isPropertyFetch($left)) { - $this->assertCanAssignObjectProp($left, $right); - } elseif ($this->isStaticPropertyFetch($left)) { - $this->assertCanAssignStaticProp($left, $right); + if ($propertyWriteTarget !== null) { + $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); } $rightExpr = $this->parseAssignRightExpr($right); - if ($this->isPropertyFetch($left) || $this->isStaticPropertyFetch($left)) { - $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr); + if ($propertyWriteTarget !== null) { + $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); } $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); @@ -332,6 +331,7 @@ trait AssignOpTrait $var = $this->parseIdentifier($node->var); $this->context->inAssignExpr = $oriInAssignExpr; $expr = $this->parseIdentifier($node->expr); + $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); $this->guardLiteralDivisionByZero($node->expr, $op); if ($this->isVarExpr($node->var)) { @@ -398,19 +398,23 @@ trait AssignOpTrait } if ($this->isPropertyFetch($node->var) and !$this->isNativePropertyAccess($node->var)) { + if ($propertyWriteTarget !== null) { + $this->assertCanAssignPropertyWrite($propertyWriteTarget, $node->expr); + } $obj = $this->parseIdentifier($node->var->var); $propName = $this->identifierToStr($node->var->name, literal: true); $binaryOp = $this->removeAssignOp($op); $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, self::TYPE_VAR); + $readProperty = $this->emitDynamicPropertyRead($obj, $propName); if ($this->isAssignOpConcat($op)) { - $this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$obj}.getProperty({$propName}), {$expr});"; + $this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$readProperty}, {$expr});"; } elseif ($this->isAssignOpPow($op)) { - $this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$obj}.getProperty({$propName}), {$expr});"; + $this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$readProperty}, {$expr});"; } else { - $this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName}) {$binaryOp} ({$expr});"; + $this->context->beforeStmtLines[] = "{$tmpVar} = {$readProperty} {$binaryOp} ({$expr});"; } - $this->context->afterStmtLines[] = "{$obj}.setProperty({$propName}, {$tmpVar});"; + $this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';'; return $tmpVar; } @@ -595,16 +599,15 @@ trait AssignOpTrait $this->context->inAssignExpr = true; $var = $this->parseIdentifier($expr->var); $this->context->inAssignExpr = $inAssignExpr; + $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); - if ($this->isPropertyFetch($expr->var)) { - $this->assertCanAssignObjectProp($expr->var, $expr->expr); - } elseif ($this->isStaticPropertyFetch($expr->var)) { - $this->assertCanAssignStaticProp($expr->var, $expr->expr); + if ($propertyWriteTarget !== null) { + $this->assertCanAssignPropertyWrite($propertyWriteTarget, $expr->expr); } $right = $this->parseExpr($expr->expr); - if ($this->isPropertyFetch($expr->var) || $this->isStaticPropertyFetch($expr->var)) { - $right = $this->wrapObjectPropertyAssignTypeCheck($expr->var, $expr->expr, $right); + if ($propertyWriteTarget !== null) { + $right = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $expr->expr, $right); } if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) { $this->errorUndefinedVariable($expr->expr); diff --git a/src/Php/Resolver/PropertyWriteTarget.php b/src/Php/Resolver/PropertyWriteTarget.php new file mode 100644 index 00000000..c8fdd323 --- /dev/null +++ b/src/Php/Resolver/PropertyWriteTarget.php @@ -0,0 +1,20 @@ +