diff --git a/.gitignore b/.gitignore index 5281261d..b748d1ad 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ *.exe *.obj *.pdb -*.class \ No newline at end of file +*.class +/swoole_compiler +/tpc \ No newline at end of file diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 78562758..7ba7a70a 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -294,7 +294,11 @@ - 普通赋值和 `??=` 已接入 `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 路径。 +- `PropertyWriteTarget` 已开始携带安全动态属性写入目标的 object/property 表达式;普通动态属性赋值、复合赋值、自增自减已优先通过 target 级 read/write helper 发射代码。 +- 动态属性 `unset`、属性数组维度写入、引用参数/refval/引用赋值中的安全对象属性引用路径已开始复用 target 级 unset/ref helper。 +- 对象属性引用表达式的 target/ref 生成已收敛到 `emitDynamicPropertyFetchRef()`;未使用的旧静态属性赋值入口已删除,静态属性赋值继续走统一 assignment target 路径。 +- 为避免改变复杂表达式求值顺序,当前仅对对象部分为变量的动态属性写入填充 target object/property 字段,复杂对象表达式仍保留旧路径。 +- 当前步骤对有效代码保持生成逻辑兼容,但会让更多属性写入路径进入统一静态检查;后续继续收敛 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 6660e877..c216b715 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -2893,11 +2893,22 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return null; } - $obj = $this->parseIdentifier($var->var); - $propName = $this->identifierToStr($var->name, literal: true); + $target = $this->preparePropertyWriteTarget($var); $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, self::TYPE_VAR); + if ($target !== null && $target->isDynamicObjectProperty()) { + if ($isPre) { + $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyTargetRead($target) . " {$op} 1; " . $this->emitDynamicPropertyTargetWrite($target, $tmpVar) . ';'; + } else { + $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyTargetRead($target) . ';'; + $this->context->afterStmtLines[] = $this->emitDynamicPropertyTargetWrite($target, "{$tmpVar} {$op} 1") . ';'; + } + return $tmpVar; + } + + $obj = $this->parseIdentifier($var->var); + $propName = $this->identifierToStr($var->name, literal: true); if ($isPre) { $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . " {$op} 1; " . $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';'; } else { @@ -3674,14 +3685,14 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $this->fatalError($arg, 'Undefined variable `$' . $name . '`'); } } elseif ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) { - $obj = $this->parseIdentifier($arg->value->var); - if (!$this->hasVar($obj)) { - $this->fatalError($arg, 'Undefined variable `$' . $obj . '`'); - } if ($byRef) { - $this->addPositionalCallArg($obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')', $arrayArgsVar, $list_args); + $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args); continue; } + $objectExpr = $this->parseIdentifier($arg->value->var); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`'); + } } elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) { $array = $this->parseIdentifier($arg->value->var); if ($array === 'GLOBALS') { @@ -3815,11 +3826,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } if ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) { - $obj = $this->parseIdentifier($arg->value->var); - if (!$this->hasVar($obj)) { - $this->fatalError($arg, 'Undefined variable `$' . $obj . '`'); - } - return $obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')'; + return $this->emitDynamicPropertyFetchRef($arg->value, $arg); } if ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) { @@ -3856,11 +3863,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont protected function expandRefvalExpr(NodeAbstract $inner, Node\Arg $arg): ?string { if ($this->isPropertyFetch($inner) and $this->isVarExpr($inner->var)) { - $obj = $this->parseIdentifier($inner->var); - if (!$this->hasVar($obj)) { - $this->fatalError($arg, 'Undefined variable `$' . $obj . '`'); - } - return $obj . '.attrRef(' . $this->identifierToStr($inner->name) . ')'; + return $this->emitDynamicPropertyFetchRef($inner, $arg); } if ($this->isArrayDimFetch($inner) and $this->isVarExpr($inner->var)) { $array = $this->parseIdentifier($inner->var); @@ -4762,10 +4765,16 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont protected function preparePropertyWriteTarget(NodeAbstract $left): ?PropertyWriteTarget { if ($left instanceof Expr\PropertyFetch) { + $objectExpr = null; + $propertyExpr = null; + if (!$this->isNativePropertyAccess($left) && $this->isVarExpr($left->var)) { + $objectExpr = $this->parseIdentifier($left->var); + $propertyExpr = $this->identifierToStr($left->name, literal: true); + } if ($this->isIdExpr($left->name)) { $this->getPropertyIdentifier($left, $left->var, $left->name); } - return new PropertyWriteTarget($left, 'object property'); + return new PropertyWriteTarget($left, 'object property', $objectExpr, $propertyExpr); } if ($left instanceof Expr\StaticPropertyFetch) { @@ -4927,7 +4936,10 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $lines[] = $array . '.offsetUnset(' . $dim . ');'; } } elseif ($this->isPropertyFetch($var)) { - $object = $this->parseIdentifier($var->var); + $propertyWriteTarget = $this->preparePropertyWriteTarget($var); + $object = $propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty() + ? $propertyWriteTarget->objectExpr + : $this->parseIdentifier($var->var); $restoreDefault = null; if ($this->isIdExpr($var->name)) { $propertyId = $this->getPropertyIdentifier($var, $var->var, $var->name); @@ -4950,7 +4962,11 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } } if ($restoreDefault === null) { - $lines[] = $object . '.unsetProperty(' . $this->identifierToStr($var->name, literal: true) . ');'; + if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) { + $lines[] = $this->emitDynamicPropertyTargetUnset($propertyWriteTarget) . ';'; + } else { + $lines[] = $object . '.unsetProperty(' . $this->identifierToStr($var->name, literal: true) . ');'; + } } } elseif ($this->isStaticPropertyFetch($var)) { $this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name)); @@ -5557,6 +5573,57 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return "{$object}.setProperty({$property}, {$value})"; } + protected function emitDynamicPropertyTargetRead(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyRead($target->objectExpr, $target->propertyExpr); + } + + protected function emitDynamicPropertyTargetWrite(PropertyWriteTarget $target, string $value): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyWrite($target->objectExpr, $target->propertyExpr, $value); + } + + protected function emitDynamicPropertyTargetUnset(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $target->objectExpr . '.unsetProperty(' . $target->propertyExpr . ')'; + } + + protected function emitDynamicPropertyTargetRef(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $target->objectExpr . '.attrRef(' . $target->propertyExpr . ')'; + } + + protected function assertDynamicPropertyTarget(PropertyWriteTarget $target): void + { + if (!$target->isDynamicObjectProperty()) { + $this->fatalError($target->node, 'Internal error: property write target is not a dynamic object property'); + } + } + + protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string + { + $target = $this->preparePropertyWriteTarget($expr); + $objectExpr = $target !== null && $target->isDynamicObjectProperty() + ? $target->objectExpr + : $this->parseIdentifier($expr->var); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`'); + } + if ($target !== null && $target->isDynamicObjectProperty()) { + return $this->emitDynamicPropertyTargetRef($target); + } + + return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; + } + protected function getChainedFunc(string $op): string { return match ($op) { diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 11d5331a..021d655e 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -8,7 +8,7 @@ namespace PhpAot\Php\Parser; -use PhpAot\Php\Symbol; +use PhpAot\Php\Resolver\PropertyWriteTarget; use PhpParser\Node; use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr; @@ -44,16 +44,28 @@ trait AssignOpTrait return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')'; } - protected function parseAssignPropertyFetch(NodeAbstract $left, NodeAbstract $right): string + protected function parseAssignPropertyFetch(NodeAbstract $left, NodeAbstract $right, ?PropertyWriteTarget $target = null): string { - $array = $this->parseIdentifier($left->var); - $propName = $this->identifierToStr($left->name, literal: true); + if ($target !== null) { + $this->assertCanAssignPropertyWrite($target, $right); + } + $rightExpr = $this->trimBrackets($this->parseExpr($right)); - $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr); + if ($target !== null) { + $rightExpr = $this->wrapPropertyWriteTypeCheck($target, $right, $rightExpr); + } else { + $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr); + } $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, self::TYPE_VAR); // Comma expression: store RHS → execute side effect → evaluate to stored value + if ($target !== null && $target->isDynamicObjectProperty()) { + return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyTargetWrite($target, $tmp) . '), ' . $tmp . ')'; + } + + $array = $this->parseIdentifier($left->var); + $propName = $this->identifierToStr($left->name, literal: true); return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyWrite($array, $propName, $tmp) . '), ' . $tmp . ')'; } @@ -82,18 +94,6 @@ trait AssignOpTrait return implode(";\n" . $this->getIndent(), $list); } - protected function parseAssignStaticProperty($left, $right): string - { - $value = $this->trimBrackets($this->parseExpr($right)); - $native = $this->parseNativeStaticPropertyFetch($left); - if ($native !== null) { - return $native . ' = ' . $value; - } - $class = $this->identifierToStr($left->class); - $propName = $this->identifierToStr($left->name); - return Symbol::setStaticProperty() . "({$class}, {$propName}, {$value})"; - } - protected function parseAssign(Expr\Assign $v): string { $left = $v->var; @@ -262,7 +262,7 @@ trait AssignOpTrait } } } elseif ($this->isPropertyFetch($left) and !$this->isNativePropertyAccess($left)) { - return $this->parseAssignPropertyFetch($left, $right); + return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget); } elseif ($this->isArrayDimFetch($left) and $this->isVarExpr($left->var)) { $tmp = $this->parseIdentifier($left->var); if ($this->getVarType($tmp) === self::TYPE_STR and $left->dim === null) { @@ -401,12 +401,16 @@ trait AssignOpTrait 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 ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) { + $readProperty = $this->emitDynamicPropertyTargetRead($propertyWriteTarget); + } else { + $obj = $this->parseIdentifier($node->var->var); + $propName = $this->identifierToStr($node->var->name, literal: true); + $readProperty = $this->emitDynamicPropertyRead($obj, $propName); + } if ($this->isAssignOpConcat($op)) { $this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$readProperty}, {$expr});"; } elseif ($this->isAssignOpPow($op)) { @@ -414,7 +418,11 @@ trait AssignOpTrait } else { $this->context->beforeStmtLines[] = "{$tmpVar} = {$readProperty} {$binaryOp} ({$expr});"; } - $this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';'; + if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) { + $this->context->afterStmtLines[] = $this->emitDynamicPropertyTargetWrite($propertyWriteTarget, $tmpVar) . ';'; + } else { + $this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';'; + } return $tmpVar; } @@ -554,9 +562,14 @@ trait AssignOpTrait $rightExpr = $tmpVar . ' = ' . $this->parseIdentifier($expr->expr) . '.toReference()'; } elseif ($this->isPropertyFetch($expr->expr)) { $left = $this->parseIdentifier($expr->var); - $object = $this->parseExpr($expr->expr->var); - $prop = $this->identifierToStr($expr->expr->name); - $rightExpr = $tmpVar . ' = ' . $object . '.attrRef(' . $prop . ')'; + $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->expr); + if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) { + $rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyTargetRef($propertyWriteTarget); + } else { + $object = $this->parseExpr($expr->expr->var); + $prop = $this->identifierToStr($expr->expr->name); + $rightExpr = $tmpVar . ' = ' . $object . '.attrRef(' . $prop . ')'; + } } elseif ($this->isArrayDimFetch($expr->expr)) { $left = $this->parseIdentifier($expr->var); $array = $this->parseIdentifier($expr->expr->var); @@ -574,8 +587,14 @@ trait AssignOpTrait protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string { - $obj = $this->parseIdentifier($left->var->var); - $propName = $this->identifierToStr($left->var->name); + $propertyWriteTarget = $this->preparePropertyWriteTarget($left->var); + if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) { + $obj = $propertyWriteTarget->objectExpr; + $propName = $propertyWriteTarget->propertyExpr; + } else { + $obj = $this->parseIdentifier($left->var->var); + $propName = $this->identifierToStr($left->var->name); + } $code = ''; $value = $this->trimBrackets($this->parseExpr($right)); diff --git a/src/Php/Resolver/PropertyWriteTarget.php b/src/Php/Resolver/PropertyWriteTarget.php index c8fdd323..4bb8fbb3 100644 --- a/src/Php/Resolver/PropertyWriteTarget.php +++ b/src/Php/Resolver/PropertyWriteTarget.php @@ -15,6 +15,13 @@ final readonly class PropertyWriteTarget public function __construct( public NodeAbstract $node, public string $label, + public ?string $objectExpr = null, + public ?string $propertyExpr = null, ) { } + + public function isDynamicObjectProperty(): bool + { + return $this->objectExpr !== null && $this->propertyExpr !== null; + } }