diff --git a/phpunit/code/polymorphic-dispatch.php b/phpunit/code/polymorphic-dispatch.php new file mode 100644 index 00000000..c10c20b3 --- /dev/null +++ b/phpunit/code/polymorphic-dispatch.php @@ -0,0 +1,63 @@ +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, + ); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 2a28700a..4cb8f8d8 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -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 diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index f1fe3ae3..272e5c3d 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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); @@ -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); @@ -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') { @@ -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 . ')'; } @@ -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) . ')'; diff --git a/tests/compiler/static/polymorphic-static-call-scope.phpt b/tests/compiler/static/polymorphic-static-call-scope.phpt new file mode 100644 index 00000000..63c29e41 --- /dev/null +++ b/tests/compiler/static/polymorphic-static-call-scope.phpt @@ -0,0 +1,78 @@ +--TEST-- +Polymorphic object static call preserves lexical scope for protected methods +--FILE-- +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" diff --git a/tests/compiler/stdlib/get_class_nullable.phpt b/tests/compiler/stdlib/get_class_nullable.phpt new file mode 100644 index 00000000..2b06c0c6 --- /dev/null +++ b/tests/compiler/stdlib/get_class_nullable.phpt @@ -0,0 +1,67 @@ +--TEST-- +get_class on nullable and polymorphic objects behaves identically to PHP +--FILE-- +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