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