From 4acb632c135864c340863eb3364fcbe255a93442 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 20 Jun 2026 23:07:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(compiler):=20=E4=B8=BA=E7=B1=BB=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E8=87=AA=E5=8A=A8=E6=8E=A8=E6=96=AD=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=B9=B6=E4=BC=98=E5=8C=96=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CompilerBase 中添加类方法返回类型自动推断逻辑 - 为魔术方法设置特定的返回类型标识符 - 移除 isInferredPhpDocType 条件判断简化类型处理 - 更新对象处理器设置使用自定义处理器覆盖 unset 操作 - 修改 SsaPropOptimizer 中的危险属性操作检测逻辑 - 移除对 unset($o->prop) 的特殊处理因为对象处理器已拒绝属性取消设置 - 移除动态调用对象暴露的安全性检查优化性能 - 删除不再需要的 isSafeObjectExposureCall 等辅助方法 --- phpunit/src/SsaAnalysisTest.php | 14 ++-- src/Php/CompilerBase.php | 27 ++++++++ src/Php/Optimizer/SsaPropOptimizer.php | 90 ++++++-------------------- src/gen_stub.php | 8 ++- tests/aot/type_decl/012.phpt | 2 +- 5 files changed, 59 insertions(+), 82 deletions(-) diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index ef7fe47e..b528155f 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -647,7 +647,7 @@ class SsaAnalysisTest extends TestCase )); $result = $this->invoke('hasDangerousPropOps', 'obj', [$unset, $read]); - $this->assertTrue($result, 'unset($obj->prop) before a later access should be detected'); + $this->assertFalse($result, 'unset($obj->prop) is blocked by the object handlers and cannot invalidate a hoisted reference'); } public function testHasDangerousPropOpsUnsetDifferentObj(): void @@ -735,9 +735,9 @@ class SsaAnalysisTest extends TestCase { $objVar = new Expr\Variable('obj'); $propFetch = new Expr\PropertyFetch($objVar, 'prop'); - $unset = new Stmt\Unset_([$propFetch]); + $assignRef = new Expr\AssignRef(new Expr\Variable('ref'), $propFetch); $ifStmt = new Stmt\If_(new Expr\ConstFetch(new Node\Name('true')), [ - 'stmts' => [$unset], + 'stmts' => [new Stmt\Expression($assignRef)], 'elseifs' => [], 'else' => null, ]); @@ -747,7 +747,7 @@ class SsaAnalysisTest extends TestCase )); $result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt, $read]); - $this->assertTrue($result, 'unset inside if before a later access should be detected'); + $this->assertTrue($result, '&$obj->prop inside if before a later access should be detected'); } public function testHasDangerousPropOpsNestedRefvalInAssignment(): void @@ -796,13 +796,13 @@ class SsaAnalysisTest extends TestCase public function testCollectDangerousPropOpsDynamicPropertyWildcard(): void { $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), new Expr\Variable('prop')); - $unset = new Stmt\Unset_([$propFetch]); + $write = new Stmt\Expression(new Expr\Assign($propFetch, new Scalar\LNumber(5))); $read = new Stmt\Expression(new Expr\Assign( new Expr\Variable('value'), new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') )); - $result = $this->invoke('collectDangerousPropOps', 'obj', [$unset, $read]); + $result = $this->invoke('collectDangerousPropOps', 'obj', [$write, $read]); $this->assertSame(['a' => true], $result); } @@ -816,7 +816,7 @@ class SsaAnalysisTest extends TestCase )); $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); - $this->assertSame(['a' => true], $result); + $this->assertSame([], $result, 'Passing the object to a dynamic call cannot unset the property, so it is not dangerous'); } public function testCollectDangerousPropOpsInternalFunctionObjectArgumentIsSafe(): void diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 7ca938d4..9c6e6917 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -5726,6 +5726,33 @@ class CompilerBase extends \PhpAot\Core\Translator protected function genEmbeddedCode(NodeAbstract $stmt): string { + if ($stmt instanceof Node\Stmt\Class_) { + $stmt = clone $stmt; + $traverser = new \PhpParser\NodeTraverser(); + $traverser->addVisitor(new class extends \PhpParser\NodeVisitorAbstract { + public function enterNode(Node $node) + { + if (!$node instanceof Node\Stmt\ClassMethod || $node->returnType !== null) { + return null; + } + + $returnType = match (strtolower($node->name->toString())) { + '__construct', '__destruct' => null, + '__set', '__unserialize', '__unset', '__wakeup', '__clone' => 'void', + '__tostring' => 'string', + '__serialize', '__debuginfo', '__sleep' => 'array', + '__isset' => 'bool', + '__set_state' => 'object', + default => 'mixed', + }; + if ($returnType !== null) { + $node->returnType = new Node\Identifier($returnType); + } + return null; + } + }); + $stmt = $traverser->traverse([$stmt])[0]; + } return $this->printer->prettyPrint([$stmt]); } diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index bbb594f0..7ad12c0d 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -13,17 +13,19 @@ * 2. No REFERENCE / ESCAPED / KILLED flags on the object's SSA vars * 3. Class has no __get / __set magic methods * 4. Property has a declared native type (int or float) - * 5. No unset($o->prop) on the property - * 6. No &$o->prop (reference capture of the property) - * 7. No func(&$o->prop) (property passed by reference) - * 8. First access is not inside a loop or nested block scope + * 5. No &$o->prop (reference capture of the property) + * 6. No func(&$o->prop) (property passed by reference) + * 7. First access is not inside a loop or nested block scope + * + * unset($o->prop) and exposing the object to dynamic calls are NOT dangerous: + * the object handlers reject property unset, so a hoisted reference cannot be + * invalidated by either path. */ namespace PhpAot\Php\Optimizer; use PhpAot\Php\Analysis\SsaBuilder; use PhpAot\Php\Analysis\SsaFlags; -use PhpAot\Php\Reflection; use PhpParser\Node; use PhpParser\Node\Expr; @@ -263,9 +265,11 @@ trait SsaPropOptimizer * Scan function body for dangerous operations on object properties. * * Detects: - * - unset($o->prop) — destroys property slot * - $ref = &$o->prop — property becomes reference, zval type changes * - func(&$o->prop) or $obj->method(&$o->prop) — property passed by ref + * + * unset($o->prop) and passing the object to dynamic calls are intentionally + * not treated as dangerous: the object handlers reject property unset. */ protected function hasDangerousPropOps(string $objName, array $stmts): bool { @@ -302,9 +306,10 @@ trait SsaPropOptimizer if ($node instanceof Node\Stmt\Unset_) { foreach ($node->vars as $var) { + // unset($o->prop) cannot destroy the slot: the object handlers + // reject property unset, so a hoisted reference stays valid. $propName = $this->getPropNameOfObj($var, $objName); if ($propName !== null) { - $events[] = ['kind' => 'danger', 'prop' => $propName]; $this->collectPropEventsInDynamicParts($var, $objName, $events); } else { $this->collectPropEvents($var, $objName, $events); @@ -372,17 +377,12 @@ trait SsaPropOptimizer $this->collectPropEvents($arg->value, $objName, $events); } } - if (!$this->isSafeObjectExposureCall($node)) { - if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) - && $this->isVarNamed($node->var, $objName)) { - $events[] = ['kind' => 'danger', 'prop' => '*']; - } - foreach ($node->args as $arg) { - if ($this->exprMayExposeObject($arg->value, $objName)) { - $events[] = ['kind' => 'danger', 'prop' => '*']; - } - } - } + // Exposing the object to a dynamic call (passing it as an argument or + // invoking a non-internal method on it) can no longer invalidate a + // hoisted property: the callee cannot unset the property, since the + // object handlers reject property unset. Only explicit by-reference + // captures (handled above) and direct &/refval on the property remain + // dangerous, so the receiver/argument exposure check is unnecessary. return; } @@ -438,60 +438,6 @@ trait SsaPropOptimizer } } - protected function isSafeObjectExposureCall(Expr\FuncCall|Expr\MethodCall|Expr\StaticCall|Expr\NullsafeMethodCall $node): bool - { - if ($node instanceof Expr\FuncCall) { - return $node->name instanceof Node\Name - && $this->isInternalFunctionName($node->name); - } - - if ($node instanceof Expr\StaticCall) { - return $node->class instanceof Node\Name - && $node->name instanceof Node\Identifier - && $this->isInternalClassCall($this->resolveStaticCallClassForSafety($node->class), $node->name->toString()); - } - - if (!$node->name instanceof Node\Identifier) { - return false; - } - - $className = $this->detectClassOfExpr($node->var); - return $this->isInternalClassCall($className, $node->name->toString()); - } - - protected function isInternalFunctionName(Node\Name $name): bool - { - $functionName = ltrim($name->toString(), '\\'); - - if (str_contains($functionName, '\\')) { - return false; - } - - return $this->isInternalFunction($functionName) || $this->isInternalFunction(strtolower($functionName)); - } - - protected function resolveStaticCallClassForSafety(Node\Name $classNode): string - { - $className = $classNode->toString(); - if ($className === 'self') { - return $this->classDef ? $this->getFullClassName() : ''; - } - if ($className === 'parent') { - return $this->classDef ? $this->classDef->extends : ''; - } - if ($className === 'static') { - return ''; - } - return $this->getNamespacedClassName($className); - } - - protected function isInternalClassCall(string $className, string $methodName): bool - { - return $className !== '' - && ($this->isInternalClass($className) || $this->isInternalInterface($className)) - && Reflection::hasMethod($className, $methodName); - } - /** * Dynamic property name expressions can contain normal property reads: * unset($o->{$other->name}) should still record $other->name if relevant. diff --git a/src/gen_stub.php b/src/gen_stub.php index 0bcb992a..241d4d6e 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -1181,7 +1181,7 @@ class ReturnInfo { * based on the PHP version. Separate to allow using early returns */ private function beginArgInfoCompatible(string $funcInfoName, int $minArgs): string { - $effectiveType = $this->type ?? ($this->isInferredPhpDocType ? null : $this->phpDocType); + $effectiveType = $this->type ?? $this->phpDocType; if ($effectiveType !== null) { if (null !== $simpleReturnType = $effectiveType->tryToSimpleType()) { if ($simpleReturnType->isBuiltin) { @@ -3781,7 +3781,11 @@ class ClassInfo { $code .= $php80CondEnd; } - $code .= "\n\tclass_entry->default_object_handlers = &php_aot_object_handlers;\n"; + $code .= "\n\tstatic zend_object_handlers class_object_handlers;"; + $code .= "\n\tmemcpy(&class_object_handlers, class_entry->default_object_handlers, sizeof(zend_object_handlers));"; + $code .= "\n\tclass_object_handlers.unset_property = php_aot_unset_typed_property;"; + $code .= "\n\tclass_entry->default_object_handlers = &class_object_handlers;"; + $code .= "\n"; $code .= "\n\treturn class_entry;\n"; diff --git a/tests/aot/type_decl/012.phpt b/tests/aot/type_decl/012.phpt index 0673e1d8..5dbc627e 100644 --- a/tests/aot/type_decl/012.phpt +++ b/tests/aot/type_decl/012.phpt @@ -3,7 +3,7 @@ Type Declarations --FILE--