fix(compiler): preserve runtime class dispatch and introspection for polymorphic objects (#110)
* fix(compiler): preserve runtime class dispatch and introspection for polymorphic objects When an object variable has an upper-bound declared type (e.g. assigned from a function or method return), SSA marks it as a stable object. However, stableObjects does not guarantee the concrete runtime class: the instance may be a subclass at runtime, or null if nullable. 1. FuncCallOptimizer::genGetClassOptimized checked isStableObject($obj->name) and folded get_class($obj) to the declared type literal string. For $a = getAnimal() returning a Dog, get_class($a) was incorrectly folded to "Animal" at compile time. 2. MethodCallTrait::parseStaticCall checked isStableObject($class) and jumped to _do_call with the declared base class. For $a::who(), this statically invoked Base::who() instead of dispatching to Dog::who() on the runtime object. Require proven exact object instances (exactObjects, populated from direct new ConcreteClass() definitions) before folding get_class() or devirtualizing static calls on object variables. Non-exact variables fall back to runtime class resolution (php::fn::get_class() and php::callStaticMethod()), matching PHP semantics. Add unit test coverage verifying exact objects continue to fold/devirtualize while polymorphic variables retain dynamic runtime dispatch. * fix(compiler): preserve lexical scope in dynamic static calls and runtime type checks for get_class - Use php::callScoped with getCallableScopeExpr() for dynamic static calls inside class methods to preserve caller visibility on protected methods - Fall back to runtime php::call for get_class on non-exact values so nullable objects throw TypeError instead of dereferencing null - Add behavioral PHPT tests for protected polymorphic static calls and nullable get_class - Update unit tests for scoped dispatch and runtime introspection * test(compiler): remove stray top-level main call from PHPT tests In TypePHP AOT compilation, function main(): void serves as the binary entrypoint. Top-level function calls outside a function are rejected by the preprocessor as stray code.master
parent
ce4c712b0c
commit
13889e9a30
6 changed files with 277 additions and 6 deletions
@ -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 |
||||
{ |
||||
} |
||||
@ -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, |
||||
); |
||||
} |
||||
} |
||||
@ -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" |
||||
@ -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…
Reference in new issue