devirtualize statically exact magic calls

master
韩天峰 1 day ago
parent 34e229e30b
commit 35f6110842
  1. 29
      phpunit/code/magic-call-codegen.php
  2. 42
      phpunit/src/MagicCallCodegenTest.php
  3. 10
      src/Context/FunctionContext.php
  4. 28
      src/Optimizer/SsaPropOptimizer.php
  5. 61
      src/Parser/MethodCallTrait.php
  6. 69
      tests/compiler/dynamic_call/call-magic-direct-safe.phpt

@ -0,0 +1,29 @@
<?php
class ExactMagicHandler
{
public function __call(string $name, array $arguments): mixed
{
return [$name, $arguments];
}
}
class RuntimeMagicMethod extends ExactMagicHandler
{
public function missing(): string
{
return 'real';
}
}
function exactMagicCall(): mixed
{
$handler = new ExactMagicHandler();
return $handler->missing(1, named: 2);
}
function runtimeMagicCall(ExactMagicHandler $handler): mixed
{
return $handler->missing();
}

@ -0,0 +1,42 @@
<?php
use TypePhp\CompilerTest;
final class MagicCallCodegenTest extends \BaseTest
{
public function testOnlyExactReceiverBypassesZendMagicCallTrampoline(): void
{
$code = $this->compileFixture();
$exactBody = $this->functionBody($code, 'php_exactmagiccall');
self::assertStringContainsString('php_exactmagichandler____call(', $exactBody);
self::assertStringNotContainsString('.call(', $exactBody);
$runtimeBody = $this->functionBody($code, 'php_runtimemagiccall');
self::assertStringContainsString('.call(', $runtimeBody);
self::assertStringNotContainsString('php_exactmagichandler____call(', $runtimeBody);
}
private function compileFixture(): string
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/magic-call-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
return $code;
}
private function functionBody(string $code, string $function): string
{
$matched = preg_match('/php::Var ' . preg_quote($function, '/') . '\\([^)]*\\) \\{(?<body>.*?)\\n\\}/s', $code, $match);
self::assertSame(1, $matched, "generated body of {$function}() not found");
return $match['body'];
}
}

@ -18,6 +18,15 @@ class FunctionContext
/** Map of SSA-stable object variable name => class name (SsaPropOptimizer). */
public array $stableObjects = [];
/**
* SSA-stable variables whose sole definition is a concrete `new ClassName()`.
* Unlike a declared/returned object type, these entries prove the exact
* runtime class and may be used for conservative method devirtualization.
*
* @var array<string, string>
*/
public array $exactObjects = [];
/** Map of hoisted property refs: objName => [propName => true] (SsaPropOptimizer). */
public array $hoistedProps = [];
@ -126,6 +135,7 @@ class FunctionContext
$this->objectProps = [];
$this->ssaBuilder = null;
$this->stableObjects = [];
$this->exactObjects = [];
$this->hoistedProps = [];
$this->unsafeObjectProps = [];
$this->staticPropRefs = [];

@ -61,7 +61,35 @@ trait SsaPropOptimizer
}
$this->context->stableObjects[$objName] = $className;
if ($this->isExactNewObjectDefinition($ssa, $objName)) {
$this->context->exactObjects[$objName] = $className;
}
}
}
/**
* A declared return type is only an upper bound and may contain a subclass.
* Only a sole, concrete `new` definition proves the runtime class exactly.
*/
protected function isExactNewObjectDefinition(SsaBuilder $ssa, string $objName): bool
{
foreach ($ssa->ssaVars as $ssaVar) {
if ($ssaVar->origName !== $objName || $ssaVar->flags & SsaFlags::PHI) {
continue;
}
$definition = $ssaVar->definition;
if (!$definition instanceof Node\Stmt\Expression
|| !$definition->expr instanceof Expr\Assign
|| !$definition->expr->expr instanceof Expr\New_
|| !$definition->expr->expr->class instanceof Node\Name) {
return false;
}
return strtolower($definition->expr->expr->class->toString()) !== 'static';
}
return false;
}
/**

@ -283,6 +283,59 @@ trait MethodCallTrait
return false;
}
/**
* Directly invoke a TypePHP-compiled __call() only when the receiver's
* exact runtime class is statically proven. A declared class is not enough:
* a subclass may provide the requested real method instead of invoking the
* parent's __call().
*/
protected function parseDirectNativeMagicCall(
Expr\MethodCall $expr,
string $object,
string $class,
string $method,
): ?string {
if ($class === '' || !$this->hasClass($class)) {
return null;
}
$exactClass = null;
if ($object === 'this_' && $this->isCurrentClassFinal()) {
$exactClass = $this->getFullClassName();
} elseif (isset($this->context->exactObjects[$object])) {
$exactClass = $this->context->exactObjects[$object];
} elseif ($this->isFinalClass($class)) {
$exactClass = $class;
}
if ($exactClass === null || strcasecmp(ltrim($exactClass, '\\'), ltrim($class, '\\')) !== 0) {
return null;
}
$nativeFunc = $this->getNativeMethod($expr, $exactClass, '__call', false);
if ($nativeFunc === false || !$this->hasFunction($nativeFunc)) {
return null;
}
$this->checkFunction($nativeFunc);
if ($this->getVarType($object) !== Type::OBJECT) {
$tmpObject = $this->genTmpVarName();
$this->context->beforeStmtLines[] = Type::OBJECT . ' ' . $tmpObject . ' = ' . $object . ';';
$object = $tmpObject;
}
// __call receives one PHP array containing positional and named
// arguments. Reuse the dynamic call argument builder so evaluation
// order, unpacking and named keys remain identical to the Zend path.
$arguments = $this->parseCallArgs(
$expr->args,
separateNamedArgs: false,
forceArrayArgs: true,
);
return self::PREFIX . $nativeFunc . '(' . $object . ', ' . $method . ', ' . $arguments . ')';
}
protected function parseNativeMethodCall(string $object, string $nativeFunc, array $args): string
{
if ($this->getVarType($object) != Type::OBJECT) {
@ -695,6 +748,14 @@ trait MethodCallTrait
if ($extension !== null) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension);
}
try {
$directMagicCall = $this->parseDirectNativeMagicCall($expr, $object, $class, $method);
if ($directMagicCall !== null) {
return $directMagicCall;
}
} catch (PlaceHolder) {
return $this->genPlaceHolder($this->genArray([$object, $method]));
}
$magicMethod = true;
}
if (!$nativeFunc) {

@ -0,0 +1,69 @@
--TEST--
compiled __call fast path preserves arguments and dynamic subclass dispatch
--FILE--
<?php
class DirectMagic
{
public function __call(string $name, array $arguments): mixed
{
return [$name, $arguments];
}
}
class InheritedMagic extends DirectMagic
{
}
class RuntimeMethod extends DirectMagic
{
public function existing(): string
{
return 'runtime-method';
}
}
function callFromDeclaredBase(DirectMagic $object): mixed
{
return $object->existing();
}
function main(): void
{
$direct = new DirectMagic();
var_dump($direct->missing(1, second: 2));
$inherited = new InheritedMagic();
$values = [3, 4];
var_dump($inherited->packed(...$values, tail: 5));
// A declared base type is not an exact runtime type. The compiler must
// retain Zend dispatch so a subclass's real method wins over __call().
var_dump(callFromDeclaredBase(new RuntimeMethod()));
}
?>
--EXPECT--
array(2) {
[0]=>
string(7) "missing"
[1]=>
array(2) {
[0]=>
int(1)
["second"]=>
int(2)
}
}
array(2) {
[0]=>
string(6) "packed"
[1]=>
array(3) {
[0]=>
int(3)
[1]=>
int(4)
["tail"]=>
int(5)
}
}
string(14) "runtime-method"
Loading…
Cancel
Save