Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions phpunit/code/polymorphic-dispatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

class BaseAnimal
{
public static function identify(): string
{
return 'base';
}
}

class DogAnimal extends BaseAnimal
{
public static function identify(): string
{
return 'dog';
}
}

function makeAnimal(): BaseAnimal
{
return new DogAnimal();
}

function getExactClass(): string
{
$dog = new DogAnimal();
return get_class($dog);
}

function getPolymorphicClass(): string
{
$animal = makeAnimal();
return get_class($animal);
}

function callExactStatic(): string
{
$dog = new DogAnimal();
return $dog::identify();
}

function callPolymorphicStatic(): string
{
$animal = makeAnimal();
return $animal::identify();
}

class BaseScopedCaller
{
protected static function identifyProtected(): string
{
return static::class;
}

public static function exercise(BaseScopedCaller $obj): string
{
return $obj::identifyProtected();
}
}

class ChildScopedCaller extends BaseScopedCaller
{
}
51 changes: 51 additions & 0 deletions phpunit/src/PolymorphicClassDispatchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

use TypePhp\CompilerTest;

final class PolymorphicClassDispatchTest extends BaseTest
{
public function testPolymorphicClassIntrospectionAndStaticDispatch(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/polymorphic-dispatch.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

// Exact new object definition get_class() folds to compile-time literal string
self::assertMatchesRegularExpression(
'/php_getexactclass\(\) \{.*?tmp_var_\d+ = \(get_str\(\d+\)\);/s',
$code,
);

// Polymorphic get_class() must NOT fold or blindly invoke unchecked C++ helper;
// it must use runtime php::call to enforce PHP argument semantics (TypeError on null)
self::assertMatchesRegularExpression(
"/php_getpolymorphicclass\(\) \{.*?tmp_var_\d+ = \(php::call\(get_persistent_func\(PersistentFuncId\{0\}, get_str\(3\)\), php::VarList\{animal\}\)\);/s",
$code,
);

// Exact new object definition static call devirtualizes to cached call
self::assertMatchesRegularExpression(
'/php_callexactstatic\(\) \{.*?typephp_call_cached\(get_str\(\d+\)/s',
$code,
);

// Polymorphic static call from global function dispatches dynamically via callStaticMethod
self::assertMatchesRegularExpression(
'/php_callpolymorphicstatic\(\) \{.*?php::callStaticMethod\([^)]+\)/s',
$code,
);

// Polymorphic static call from within a class method preserves lexical callable scope
self::assertMatchesRegularExpression(
"/php_basescopedcaller__exercise\(.*?php::CallableScope (tmp_var_\d+) = php::getCallableScope\(.*?php::callScoped\(php::concat\(.*?, \\1\)/s",
$code,
);
}
}
6 changes: 3 additions & 3 deletions src/Optimizer/FuncCallOptimizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -1118,10 +1118,10 @@ protected function genGetClassOptimized(string $n, Node\Expr\FuncCall $e, array
'Native classes do not support runtime class introspection; use `NativeClass::class`',
);
}
if ($this->isVarExpr($obj) && $this->isStableObject($obj->name)) {
return $this->getLiteralString($this->getObjectType($obj->name));
if ($this->isVarExpr($obj) && isset($this->context->exactObjects[$obj->name])) {
return $this->getLiteralString($this->context->exactObjects[$obj->name]);
}
return 'php::fn::get_class(' . $this->parseIdentifier($obj) . ')';
return false;
}

protected function genGetParentClass(string $n, Node\Expr\FuncCall $e, array $c): string
Expand Down
18 changes: 15 additions & 3 deletions src/Parser/MethodCallTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,7 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
$rtClass = '';
$cacheCallable = false;
$directStaticCall = false;
$scopedStaticCall = false;
$staticCallTarget = '';
$staticCallMethod = '';
$canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name);
Expand Down Expand Up @@ -1103,10 +1104,10 @@ protected function parseStaticCall(Expr\StaticCall $expr): string

if (!$this->isNameExpr($expr->class)) {
if ($this->isVarExpr($expr->class)
&& $this->isStableObject($class)
&& isset($this->context->exactObjects[$class])
&& $this->isIdExpr($expr->name)
) {
$class = $this->getObjectType($class);
$class = $this->context->exactObjects[$class];
goto _do_call;
}
$classTarget = $this->materializeDynamicStaticCallTarget($expr->class);
Expand All @@ -1125,7 +1126,11 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
}
}
$placeHolder = $fn;
$directStaticCall = true;
if ($this->methodDef !== null) {
$scopedStaticCall = true;
} else {
$directStaticCall = true;
}
} elseif ($this->isVarExpr($expr->name)) {
$staticCallMethod = $this->methodNameToStr($expr->name, literal: true);
if ($class === 'static') {
Expand Down Expand Up @@ -1236,6 +1241,9 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
}

if (empty($expr->args)) {
if ($scopedStaticCall) {
return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ')';
}
if ($directStaticCall) {
return 'php::callStaticMethod(' . $staticCallTarget . ', ' . $staticCallMethod . ')';
}
Expand All @@ -1245,6 +1253,10 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
return 'php::call(' . $fn . ')';
}
try {
if ($scopedStaticCall) {
return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ', '
. $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')';
}
if ($directStaticCall) {
return 'php::callStaticMethod(' . $staticCallTarget . ', ' . $staticCallMethod . ', '
. $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')';
Expand Down
78 changes: 78 additions & 0 deletions tests/compiler/static/polymorphic-static-call-scope.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
--TEST--
Polymorphic object static call preserves lexical scope for protected methods
--FILE--
<?php

class Base
{
protected static function identify(): string
{
return static::class;
}

public static function who(): string
{
return static::class;
}

public static function exerciseStatic(Base $obj): string
{
return $obj::identify();
}

public function exerciseInstance(Base $obj): string
{
return $obj::identify();
}
}

class Child extends Base
{
}

class SubChild extends Child
{
public static function who(): string
{
return 'subchild:' . static::class;
}
}

function makeChild(): Base
{
return new Child();
}

function makeSubChild(): Base
{
return new SubChild();
}

function globalCallPublic(Base $obj): string
{
return $obj::who();
}

function main(): void
{
$child = makeChild();
var_dump(Base::exerciseStatic($child));

$base = new Base();
var_dump($base->exerciseInstance($child));

$sub = makeSubChild();
var_dump(Base::exerciseStatic($sub));
var_dump($base->exerciseInstance($sub));

var_dump(globalCallPublic($child));
var_dump(globalCallPublic($sub));
}
?>
--EXPECT--
string(5) "Child"
string(5) "Child"
string(8) "SubChild"
string(8) "SubChild"
string(5) "Child"
string(17) "subchild:SubChild"
67 changes: 67 additions & 0 deletions tests/compiler/stdlib/get_class_nullable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
get_class on nullable and polymorphic objects behaves identically to PHP
--FILE--
<?php

class Animal
{
}

class Dog extends Animal
{
}

class Cat extends Animal
{
}

function makeAnimal(): ?Animal
{
return new Dog();
}

function makeNullAnimal(): ?Animal
{
return null;
}

function testPolymorphic(): void
{
$animal = makeAnimal();
var_dump(get_class($animal));
}

function testExact(): void
{
$cat = new Cat();
var_dump(get_class($cat));
}

function testNullable(): void
{
$animal = makeNullAnimal();
try {
var_dump(get_class($animal));
} catch (TypeError $e) {
echo "Caught nullable TypeError: " . $e->getMessage() . "\n";
}

try {
var_dump(get_class(null));
} catch (TypeError $e) {
echo "Caught literal TypeError: " . $e->getMessage() . "\n";
}
}

function main(): void
{
testPolymorphic();
testExact();
testNullable();
}
?>
--EXPECT--
string(3) "Dog"
string(3) "Cat"
Caught nullable TypeError: get_class(): Argument #1 ($object) must be of type object, null given
Caught literal TypeError: get_class(): Argument #1 ($object) must be of type object, null given
Loading