diff --git a/src/Php/Analysis/SsaBuilder.php b/src/Php/Analysis/SsaBuilder.php index 773b011a..ee7ef2a7 100644 --- a/src/Php/Analysis/SsaBuilder.php +++ b/src/Php/Analysis/SsaBuilder.php @@ -1466,7 +1466,9 @@ class SsaBuilder if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall || $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) { foreach ($node->args as $arg) { - $this->collectVarUses($arg->value, $vars, false); + if (isset($arg->value)) { + $this->collectVarUses($arg->value, $vars, false); + } } if ($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) { $this->collectVarUses($node->var, $vars, false); diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index eaea4727..125a3916 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -361,6 +361,12 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected array $classExtends = []; + /** + * Reverse class hierarchy: parent class (lowercase) => list of child classes (lowercase) + * @var array + */ + protected array $classSubClasses = []; + public function __construct(string $rootPath) { parent::__construct(); @@ -2051,7 +2057,10 @@ class CompilerBase extends \PhpAot\Core\Translator ) { return self::TYPE_DECIMAL; } - if ($argType === self::TYPE_BIGFLOAT && $name === 'abs') { + if ( + $argType === self::TYPE_BIGFLOAT + && in_array($name, ['abs', 'sqrt'], true) + ) { return self::TYPE_BIGFLOAT; } } @@ -5016,6 +5025,11 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isNativeType($toType) and $this->isNativeType($fromType)) { return true; } + // BigInt/BigFloat/Decimal 与原生类型之间可能发生隐式转换,允许重新赋值 + $bigTypes = [self::TYPE_BIGINT, self::TYPE_DECIMAL, self::TYPE_BIGFLOAT]; + if (in_array($toType, $bigTypes, true) or in_array($fromType, $bigTypes, true)) { + return true; + } $varName = 'variable'; if ($this->isVarExpr($left)) { $varName = '`$' . $this->parseIdentifier($left) . '`'; @@ -5057,6 +5071,75 @@ class CompilerBase extends \PhpAot\Core\Translator return isset($this->classMethodOverride[$fullMethodNameLower]) and $this->classMethodOverride[$fullMethodNameLower]; } + protected function hasSubClasses(string $classNameLower): bool + { + return !empty($this->classSubClasses[$classNameLower]); + } + + protected function isCurrentClassFinal(): bool + { + return $this->classDef && ($this->classDef->flags & Modifiers::FINAL) !== 0; + } + + protected function getMethodFlags(string $class, string $method): int + { + if (!$this->hasClass($class)) { + return 0; + } + $classDef = $this->getClass($class); + while (true) { + if ($classDef->hasMethod($method)) { + return $classDef->getMethod($method)->flags; + } + if (!$classDef->extends || !$this->hasClass($classDef->extends)) { + return 0; + } + $classDef = $this->getClass($classDef->extends); + } + } + + /** + * Determine whether a method call can be devirtualized to a direct native call. + * + * Returns true when the exact class is known at compile time: + * 1. $this->m() in a final class (no subclass possible) + * 2. $this->m() where m is final (can't be overridden) + * 3. $this->m() where m is private (not virtual) + * 4. $obj->m() where obj's class has no known subclasses + * 5. $obj->m() where obj is SSA-stable (single def, no escape) + */ + protected function canDevirtualize(string $object, string $class, string $method): bool + { + // Case 1: Calling on 'this_' in a final class + if ($object === 'this_' && $this->isCurrentClassFinal()) { + return true; + } + + // Case 2 & 3: Method is final or private + $flags = $this->getMethodFlags($class, $method); + if ($flags & (Modifiers::FINAL | Modifiers::PRIVATE)) { + return true; + } + + // Case 4: Typed object whose class has no known subclasses + if ($object !== 'this_' && $this->hasClass($class)) { + $classLower = strtolower($class); + if (!$this->hasSubClasses($classLower) && !$this->isInterface($class)) { + return true; + } + } + + // Case 5: SSA-stable object — compile-time proven exact type + if ($object !== 'this_' && isset($this->context->stableObjects[$object])) { + $stableClass = $this->context->stableObjects[$object]; + if ($this->hasClass($stableClass) && !$this->isAbstractClass($stableClass)) { + return true; + } + } + + return false; + } + protected function findNativeMethod(CallLike $expr, string $object, string $method): string|false { $classDef = null; @@ -5065,6 +5148,10 @@ class CompilerBase extends \PhpAot\Core\Translator $classDef = $this->classDef; } elseif (isset($this->context->objects[$object])) { $class = $this->context->objects[$object]; + // SSA-stable: use exact type from stableObjects (more specific than declared type) + if (isset($this->context->stableObjects[$object])) { + $class = $this->context->stableObjects[$object]; + } } else { return false; } @@ -5083,9 +5170,11 @@ class CompilerBase extends \PhpAot\Core\Translator $fullMethodName = $object . '::' . $method; } - // 存在子类同名方法,需要转为动态调用 + // 存在子类同名方法,尝试去虚化 if ($this->isOverrideMethod($fullMethodName)) { - return false; + if (!$this->canDevirtualize($object, $class, $method)) { + return false; + } } if ($nativeFunc) { $this->checkFunction($nativeFunc); @@ -5213,6 +5302,9 @@ class CompilerBase extends \PhpAot\Core\Translator if (isset($this->context->arguments[$name])) { continue; } + if (isset($this->context->globalVars[$name])) { + continue; + } $code .= $this->getIndent(); if ($type === self::TYPE_STD_ARRAY) { $info = $this->context->stdArrays[$name]; diff --git a/src/Php/Optimizer/FuncCallOptimizer.php b/src/Php/Optimizer/FuncCallOptimizer.php index e4232829..84ae9ae3 100644 --- a/src/Php/Optimizer/FuncCallOptimizer.php +++ b/src/Php/Optimizer/FuncCallOptimizer.php @@ -128,6 +128,9 @@ trait FuncCallOptimizer if ($type === self::TYPE_DECIMAL) { return 'php::Decimal::sqrt(' . $this->parseExpr($expr->args[0]->value) . ')'; } + if ($type === self::TYPE_BIGFLOAT) { + return 'php::BigFloat::sqrt(' . $this->parseExpr($expr->args[0]->value) . ')'; + } } if ($name === 'floor') { $type = $this->detectTypeOfExpr($expr->args[0]->value); diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php index 1342066a..ba8a6eb2 100644 --- a/src/Php/Optimizer/SsaTypeOptimizer.php +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -58,6 +58,8 @@ trait SsaTypeOptimizer $hasDanger = false; $narrowedType = null; + $nonNarrowableType = null; + foreach ($varList as $ssaVar) { if ($ssaVar->flags & SsaFlags::PHI) { $hasPhi = true; @@ -69,10 +71,22 @@ trait SsaTypeOptimizer } $defType = $this->detectSsaDefType($ssaVar); - if ($defType === null || !isset($narrowableTypes[$defType])) { + if ($defType === null) { $hasDanger = true; break; } + // Non-narrowable types (BigInt/BigFloat/Decimal/Stream/objects etc.) + // are not dangerous — they just can't be narrowed. Record the type + // so dependent SSA variables can resolve it later. + if (!isset($narrowableTypes[$defType])) { + if ($nonNarrowableType === null) { + $nonNarrowableType = $defType; + } elseif ($nonNarrowableType !== $defType) { + // Mixed non-narrowable types — can't determine a single type + $nonNarrowableType = self::TYPE_VAR; + } + continue; + } if ($narrowedType === null) { $narrowedType = $defType; @@ -82,7 +96,25 @@ trait SsaTypeOptimizer } } - if ($hasDanger || $narrowedType === null) { + if ($hasDanger) { + continue; + } + + // Mixed narrowable and non-narrowable types (e.g. $x = [1,2] then $x = 42) + // — can't safely narrow. + if ($narrowedType !== null && $nonNarrowableType !== null && $nonNarrowableType !== self::TYPE_VAR) { + continue; + } + + if ($narrowedType === null) { + // No narrowable type found, but register Big* types so dependent + // SSA variables can resolve them (these types have no extra metadata). + if ( + $nonNarrowableType !== null + && in_array($nonNarrowableType, [self::TYPE_BIGINT, self::TYPE_DECIMAL, self::TYPE_BIGFLOAT, self::TYPE_STREAM], true) + ) { + $this->context->localVars[$varName] = $nonNarrowableType; + } continue; } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index de1329cf..6a82dfec 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -430,6 +430,7 @@ class Preprocessor extends CompilerBase $this->fatalError($class, "Class {$fullClassName} cannot extend itself"); } $this->classExtends[$fullClassNameLower] = $parentClassLower; + $this->classSubClasses[$parentClassLower][] = $fullClassNameLower; if (!$this->isInternalClass($parentClassLower)) { $this->symbolCallInFile[$this->file][] = $parentClassLower; } diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index 89fc2d87..6e20f390 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -316,6 +316,7 @@ trait UniversalMethodCall 'neg' => ['handler' => 'cpp_fn', 'fn' => 'php::BigFloat::neg', 'return_type' => CompilerBase::TYPE_BIGFLOAT, 'min_args' => 0, 'max_args' => 0], 'cmp' => ['handler' => 'cpp_fn', 'fn' => 'php::BigFloat::cmp', 'return_type' => CompilerBase::TYPE_INT, 'min_args' => 1, 'max_args' => 1], 'abs' => ['handler' => 'cpp_fn', 'fn' => 'php::BigFloat::abs', 'return_type' => CompilerBase::TYPE_BIGFLOAT, 'min_args' => 0, 'max_args' => 0], + 'sqrt' => ['handler' => 'cpp_fn', 'fn' => 'php::BigFloat::sqrt', 'return_type' => CompilerBase::TYPE_BIGFLOAT, 'min_args' => 0, 'max_args' => 0], ], ]; diff --git a/tests/aot/devirtualize/final-class.phpt b/tests/aot/devirtualize/final-class.phpt new file mode 100644 index 00000000..d432284f --- /dev/null +++ b/tests/aot/devirtualize/final-class.phpt @@ -0,0 +1,31 @@ +--TEST-- +Devirtualize: final class $this->method() uses native call +--FILE-- +name(); + } +} + +function main() { + $sealed = new Sealed(); + var_dump($sealed->getName()); + var_dump($sealed->name()); +} + +?> +--EXPECT-- +string(6) "sealed" +string(6) "sealed" diff --git a/tests/aot/devirtualize/final-method.phpt b/tests/aot/devirtualize/final-method.phpt new file mode 100644 index 00000000..85775ebf --- /dev/null +++ b/tests/aot/devirtualize/final-method.phpt @@ -0,0 +1,35 @@ +--TEST-- +Devirtualize: final method uses native call +--FILE-- +type(); + } +} + +class Dog extends Animal { + public function name(): string { + return "dog"; + } +} + +function main() { + $animal = new Animal(); + var_dump($animal->getType()); + + $dog = new Dog(); + var_dump($dog->getType()); + var_dump($dog->name()); +} + +?> +--EXPECT-- +string(6) "animal" +string(6) "animal" +string(3) "dog" diff --git a/tests/aot/devirtualize/override-dynamic.phpt b/tests/aot/devirtualize/override-dynamic.phpt new file mode 100644 index 00000000..fd78c20f --- /dev/null +++ b/tests/aot/devirtualize/override-dynamic.phpt @@ -0,0 +1,35 @@ +--TEST-- +Devirtualize: overridden method stays dynamic (no false devirtualization) +--FILE-- +greet(); + } +} + +class Child_ extends Parent_ { + public function greet(): string { + return "child"; + } +} + +function main() { + $parent = new Parent_(); + var_dump($parent->sayHello()); + + $child = new Child_(); + var_dump($child->sayHello()); + var_dump($child->greet()); +} + +?> +--EXPECT-- +string(6) "parent" +string(5) "child" +string(5) "child" diff --git a/tests/aot/devirtualize/private-method.phpt b/tests/aot/devirtualize/private-method.phpt new file mode 100644 index 00000000..76b151c6 --- /dev/null +++ b/tests/aot/devirtualize/private-method.phpt @@ -0,0 +1,33 @@ +--TEST-- +Devirtualize: private method always uses native call (not virtual) +--FILE-- +value(); + } + + private function withArg(int $n): string { + return "num:" . $n; + } + + public function testArg(): string { + return $this->withArg(42); + } +} + +function main() { + $base = new Base(); + var_dump($base->getValue()); + var_dump($base->testArg()); +} + +?> +--EXPECT-- +string(12) "base-private" +string(6) "num:42"