Preserve get_called_class in directly compiled methods (#107)

* fix(compiler): preserve called class in direct method calls

* fix(compiler): resolve called-class function targets

* fix(compiler): retain optimized namespace builtins
master
yavon007 4 weeks ago committed by GitHub
parent 62ec66bb6c
commit df6c0dfd99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      phpunit/code/direct-get-called-class.php
  2. 26
      phpunit/code/get-called-class-namespace-fallback.php
  3. 20
      phpunit/code/get-called-class-shadow-alias.php
  4. 42
      phpunit/code/namespaced-builtin-fallback-optimizer.php
  5. 13
      phpunit/code/namespaced-restricted-functions.php
  6. 8
      phpunit/code/namespaced-wasi-restricted-function.php
  7. 12
      phpunit/code/native-class-get-called-class-namespaced.php
  8. 10
      phpunit/code/native-class-get-called-class-qualified.php
  9. 17
      phpunit/code/native-class-get-called-class-shadow.php
  10. 8
      phpunit/code/native-class-get-called-class.php
  11. 12
      phpunit/code/native-class-get-class-namespaced.php
  12. 22
      phpunit/code/native-class-get-class-shadow.php
  13. 15
      phpunit/code/native-class-get-parent-class-namespaced.php
  14. 15
      phpunit/code/qualified-runtime-function.php
  15. 2
      phpunit/code/random-invalid-random-int-arity.php
  16. 2
      phpunit/code/scoped-call-context-reuse.php
  17. 94
      phpunit/src/DirectGetCalledClassTest.php
  18. 20
      phpunit/src/FuncCallOptimizerTest.php
  19. 30
      phpunit/src/NanoCapabilityPolicyTest.php
  20. 40
      phpunit/src/NativeClass/NativeClassValidationTest.php
  21. 12
      src/Build/NanoExtensionSelector.php
  22. 25
      src/CompilerBase.php
  23. 204
      src/Parser/FunctionCallTrait.php
  24. 2
      src/Resolver/DeclarationSymbolTrait.php
  25. 5
      src/Resolver/NameResolutionTrait.php
  26. 8
      src/Translator.php
  27. 84
      tests/compiler/class/direct-get-called-class.phpt
  28. 45
      tests/compiler/function/namespaced-builtin-fallback-optimizer.phpt

@ -0,0 +1,23 @@
<?php
class DirectCalledBase
{
final public function instanceName(): string
{
return get_called_class();
}
public static function staticName(): string
{
return \GET_CALLED_CLASS();
}
}
class DirectCalledChild extends DirectCalledBase
{
}
function directCalledName(DirectCalledBase $object): string
{
return $object->instanceName();
}

@ -0,0 +1,26 @@
<?php
namespace CalledClassKnown;
function get_called_class(): string
{
return 'known';
}
class KnownProbe
{
public function name(): string
{
return get_called_class();
}
}
namespace CalledClassDynamic;
class DynamicProbe
{
public function name(): string
{
return get_called_class();
}
}

@ -0,0 +1,20 @@
<?php
namespace ExternalFunctions;
function shadow(): string
{
return 'shadow';
}
namespace CalledAlias;
use function ExternalFunctions\shadow as GET_CALLED_CLASS;
class Probe
{
public function name(): string
{
return gEt_CaLlEd_CLaSs();
}
}

@ -0,0 +1,42 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
namespace NamespacedBuiltinFallback;
class Probe
{
private array $blocks;
private int $size = 0;
public function __construct(array $blocks)
{
$this->blocks = $blocks;
}
public function countBlocks(): int
{
$this->size = count($this->blocks);
return $this->size;
}
public function matches(): bool
{
return in_array('needle', $this->blocks, true)
&& str_contains('needle in haystack', 'needle');
}
}
function strlen(string $value): int
{
return 7;
}
function shadowedLength(): int
{
return strlen('ignored');
}

@ -0,0 +1,13 @@
<?php
namespace NamespacedRestricted;
function nano(): void
{
ShElL_ExEc('true');
}
function wasi(): void
{
StReAm_SoCkEt_ClIeNt('tcp://127.0.0.1:80');
}

@ -0,0 +1,8 @@
<?php
namespace NamespacedRestricted;
function wasi(): void
{
StReAm_SoCkEt_ClIeNt('tcp://127.0.0.1:80');
}

@ -0,0 +1,12 @@
<?php
namespace NativeCalledClassShort;
#[\Native]
class NativeGetCalledClassShort
{
public function name(): string
{
return gEt_CaLlEd_CLaSs();
}
}

@ -0,0 +1,10 @@
<?php
#[\Native]
class NativeGetCalledClassQualified
{
public function name(): string
{
return \GET_CALLED_CLASS();
}
}

@ -0,0 +1,17 @@
<?php
namespace NativeCalledClassShadow;
function get_called_class(): string
{
return 'shadow';
}
#[\Native]
class NativeGetCalledClassShadow
{
public function name(): string
{
return get_called_class();
}
}

@ -1,10 +1,14 @@
<?php
#[Native]
namespace NativeCalledClass;
use function \get_called_class as CALLED_CLASS;
#[\Native]
class NativeGetCalledClass
{
public function name(): string
{
return get_called_class();
return cAlLeD_cLaSs();
}
}

@ -0,0 +1,12 @@
<?php
namespace NativeClassIntrospection;
#[\Native]
class NativeGetClassNamespaced
{
public function name(): string
{
return gEt_CLaSs();
}
}

@ -0,0 +1,22 @@
<?php
namespace NativeClassIntrospectionShadow;
function get_class(): string
{
return 'class';
}
function get_parent_class(): string
{
return 'parent';
}
#[\Native]
class NativeGetClassShadow
{
public function names(): array
{
return [get_class(), get_parent_class()];
}
}

@ -0,0 +1,15 @@
<?php
namespace NativeClassIntrospection;
#[\Native]
class NativeGetParentClassNamespacedBase {}
#[\Native]
class NativeGetParentClassNamespaced extends NativeGetParentClassNamespacedBase
{
public function parentName(): string|false
{
return gEt_PaReNt_ClAsS($this);
}
}

@ -0,0 +1,15 @@
<?php
namespace Consumer;
use Vendor\Package as Lib;
function imported(): mixed
{
return Lib\runtime_function();
}
function currentNamespace(): mixed
{
return Sub\runtime_function();
}

@ -1,5 +1,7 @@
<?php
namespace RandomArity;
function invalidRandomIntArity(): int
{
return random_int(1);

@ -1,5 +1,7 @@
<?php
namespace ScopedCallContext;
class ScopedCallContextReuse
{
private function mapValue(int $value): int

@ -0,0 +1,94 @@
<?php
use TypePhp\Build\NanoExtensionSelector;
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\CompilerBase;
use TypePhp\CompilerTest;
final class DirectGetCalledClassTest extends BaseTest
{
public function testBuiltinUsesRuntimeCalledClassInInstanceAndStaticMethods(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/direct-get-called-class.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
self::assertSame(2, substr_count($code, 'const php::Str _typephp_called_class ='));
self::assertStringNotContainsString('php::call(', $code);
self::assertStringContainsString('php_directcalledbase__instancename(object)', $code);
}
public function testCaseInsensitiveUserFunctionAliasIsNotReplacedWithClassName(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/get-called-class-shadow-alias.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
self::assertStringNotContainsString('_typephp_called_class', $code);
self::assertStringContainsString('php_externalfunctions__shadow(', $code);
}
public function testNamespacedFunctionShadowsAndFallsBackAtRuntime(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$compiler->setTargetName('get_called_class_fallback');
(new \ReflectionProperty($compiler, 'noLiteralStrings'))->setValue($compiler, true);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/get-called-class-namespace-fallback.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
self::assertStringContainsString('php_calledclassknown__get_called_class(', $code);
self::assertStringContainsString('php::fn::function_exists(', $code);
self::assertStringContainsString('typephp_call_cached(', $code);
self::assertStringContainsString('_typephp_called_class', $code);
self::assertStringContainsString('CalledClassDynamic\\\\get_called_class', $code);
self::assertStringNotContainsString('ZEND_STRL("get_called_class")', $code);
self::assertSame(1, substr_count($code, 'typephp_get_function_resolution_cache('));
self::assertSame(1, substr_count($code, 'typephp_get_function_call_cache('));
self::assertStringContainsString('resolution == 0', $code);
self::assertStringContainsString('resolution = php::fn::function_exists(', $code);
self::assertStringContainsString(') ? 1 : 2;', $code);
self::assertStringContainsString('resolution == 1', $code);
$extension = file_get_contents($compiler->genExtension());
self::assertIsString($extension);
self::assertStringContainsString('uint8_t function_resolution_cache_map[1]', $extension);
self::assertStringContainsString(
'uint8_t &typephp_get_function_resolution_cache(FunctionResolutionCacheId cache_id)',
$extension,
);
$statistics = $compiler->getCompilationStatistics();
self::assertTrue($statistics->has(CompilationStatistics::DIRECT_FUNCTIONS, 'function_exists'));
$selection = (new NanoExtensionSelector())->select($statistics, ['standard']);
self::assertSame(['standard.core'], $selection->features);
}
public function testQualifiedRuntimeFunctionUsesResolvedTarget(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
(new \ReflectionProperty($compiler, 'noLiteralStrings'))->setValue($compiler, true);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/qualified-runtime-function.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
self::assertStringContainsString('Vendor\\\\Package\\\\runtime_function', $code);
self::assertStringContainsString('Consumer\\\\Sub\\\\runtime_function', $code);
self::assertStringNotContainsString('Lib\\\\runtime_function', $code);
}
}

@ -36,4 +36,24 @@ final class FuncCallOptimizerTest extends BaseTest
self::assertStringContainsString('php::fn::floor(1.5)', $code);
self::assertStringContainsString('php::fn::round(1.25)', $code);
}
public function testNamespacedBuiltinFallbackUsesDirectOptimizerPaths(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/namespaced-builtin-fallback-optimizer.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertSame(1, substr_count($code, 'php::fn::count('));
self::assertSame(1, substr_count($code, 'php::fn::in_array('));
self::assertSame(1, substr_count($code, 'php::fn::str_contains('));
self::assertStringContainsString('php_namespacedbuiltinfallback__strlen(', $code);
self::assertStringNotContainsString('php::call(', $code);
}
}

@ -200,6 +200,36 @@ final class NanoCapabilityPolicyTest extends BaseTest
$compiler->convertFile($source);
}
public function testNanoRejectsNamespacedRestrictedBuiltinFallback(): void
{
global $translator;
$compiler = new NanoCapabilityPolicyCompiler(TYPEPHP_ROOT_PATH);
$compiler->enableNanoForTest();
$translator = $compiler;
$source = __DIR__ . '/../code/namespaced-restricted-functions.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Function `shell_exec` is not supported in nano mode');
$compiler->convertFile($source);
}
public function testWasiRejectsNamespacedRestrictedBuiltinFallback(): void
{
global $translator;
$compiler = new NanoCapabilityPolicyCompiler(TYPEPHP_ROOT_PATH);
$compiler->enableWasiForTest();
$translator = $compiler;
$source = __DIR__ . '/../code/namespaced-wasi-restricted-function.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Function `stream_socket_client` is not supported by the WASI target');
$compiler->convertFile($source);
}
public function testNanoKeepsTypedFileStreamMethods(): void
{
global $translator;

@ -929,6 +929,26 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-get-called-class.php');
}
public function testRejectsQualifiedGetCalledClassInNativeClass(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support late static binding');
$this->compile('native-class-get-called-class-qualified.php');
}
public function testRejectsNamespacedGetCalledClassInNativeClass(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support late static binding');
$this->compile('native-class-get-called-class-namespaced.php');
}
public function testAllowsCompiledNamespacedGetCalledClassShadowInNativeClass(): void
{
$this->compile('native-class-get-called-class-shadow.php');
$this->addToAssertionCount(1);
}
public function testRejectsGetClassForNativeObject(): void
{
$this->expectException(TestError::class);
@ -936,6 +956,13 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-get-class.php');
}
public function testRejectsNamespacedGetClassForNativeObject(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support runtime class introspection');
$this->compile('native-class-get-class-namespaced.php');
}
public function testRejectsImplicitGetClassInNativeMethod(): void
{
$this->expectException(TestError::class);
@ -950,6 +977,19 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-get-parent-class.php');
}
public function testRejectsNamespacedGetParentClassForNativeObject(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support runtime class introspection');
$this->compile('native-class-get-parent-class-namespaced.php');
}
public function testAllowsCompiledNamespacedGetClassShadowsInNativeClass(): void
{
$this->compile('native-class-get-class-shadow.php');
$this->addToAssertionCount(1);
}
public function testRejectsChangingAnInferredNativeGlobalType(): void
{
$this->expectException(TestError::class);

@ -284,6 +284,11 @@ final class NanoExtensionSelector
}
}
foreach (array_keys($statistics->get(CompilationStatistics::DIRECT_FUNCTIONS)) as $function) {
$feature = $this->standardFeature($function);
if ($feature !== null) {
$features[$feature] = true;
continue;
}
$owner = $this->functionExtension($function);
if ($owner === 'date') {
// Common date helpers are coupled to timelib and the DateTime
@ -292,12 +297,7 @@ final class NanoExtensionSelector
continue;
}
if ($owner === 'standard') {
$feature = $this->standardFeature($function);
if ($feature === null) {
$selected['standard'] = true;
} else {
$features[$feature] = true;
}
$selected['standard'] = true;
continue;
}
if ($owner !== null && isset($available[$owner])) {

@ -441,6 +441,7 @@ class CompilerBase implements PropertyAccessContext
protected int $propertyAccessCacheIndex = 0;
protected int $methodCallCacheIndex = 0;
protected int $functionCallCacheIndex = 0;
protected int $functionResolutionCacheIndex = 0;
/** @var array<string, array<Node\Stmt>> Prepared declaration ASTs keyed by real path. */
protected array $preparedFileAsts = [];
protected bool $traitDeclarationsComposed = false;
@ -1557,6 +1558,14 @@ class CompilerBase implements PropertyAccessContext
return 'typephp_get_function_call_cache(FunctionCallCacheId{' . $id . '})';
}
/** Reserve a request-local namespace-function resolution slot per call site. */
protected function getFunctionResolutionCache(): string
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'function resolution cache ID allocation');
$id = $this->functionResolutionCacheIndex++;
return 'typephp_get_function_resolution_cache(FunctionResolutionCacheId{' . $id . '})';
}
/** Return the function-local late-static-bound class entry. */
protected function getCalledCeExpr(): string
{
@ -3653,12 +3662,16 @@ class CompilerBase implements PropertyAccessContext
$this->escapeName($this->getNamespacedClassName($funcName)),
];
} else {
$possibleFunctionNames = [$this->escapeName($funcName)];
if ($this->namespace) {
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
}
if (isset($this->useFunctions[$funcName])) {
$possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]);
$import = strtolower($funcName);
if (isset($this->useFunctions[$import])) {
$possibleFunctionNames = [$this->escapeNamespace($this->useFunctions[$import])];
} else {
$possibleFunctionNames = [];
if ($this->namespace) {
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace)
. self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
}
$possibleFunctionNames[] = $this->escapeName($funcName);
}
}

@ -20,6 +20,66 @@ use TypePhp\Exception\PlaceHolder;
trait FunctionCallTrait
{
/**
* Resolve the one static function name used by every call path. Function
* imports and function names are case-insensitive, unlike constant names.
*
* @return array{source: string, target: string, nativeLookup: string, lower: string, definitelyGlobal: bool, namespacedFallback: bool}
*/
protected function resolveStaticFunctionCallTarget(Node\Name $name): array
{
$source = $this->parseIdentifier($name);
$bare = ltrim($source, '\\');
$unqualified = !str_contains($bare, '\\');
$fullyQualified = $name instanceof Node\Name\FullyQualified;
$import = $unqualified && !$fullyQualified ? strtolower($bare) : '';
$imported = $import !== '' && isset($this->useFunctions[$import]);
$resolved = $name->getAttribute('resolvedName');
$target = $resolved instanceof Node\Name
? ltrim($resolved->toString(), '\\')
: ($imported ? $this->useFunctions[$import] : $bare);
$lower = strtolower($target);
$namespacedFallback = !$fullyQualified
&& !$imported
&& $unqualified
&& $this->namespace !== '';
return [
'source' => $source,
'target' => $target,
// The raw short name is the only form that retains PHP's
// namespace-to-global lookup fallback. All resolved/imported/
// qualified names must not be interpreted through imports again.
'nativeLookup' => !$fullyQualified && !$imported && $unqualified
? $source
: '\\' . $target,
'lower' => $lower,
// A namespaced short name can be shadowed at runtime, so only
// these forms are known to name a global builtin directly.
'definitelyGlobal' => $fullyQualified
|| ($imported && !str_contains($target, '\\'))
|| ($this->namespace === '' && $unqualified),
'namespacedFallback' => $namespacedFallback,
];
}
/**
* PHP resolves an unqualified namespaced function dynamically before
* falling back to its global builtin. Resolve on the first execution and
* cache that target choice for the rest of the request.
*/
protected function parseNamespacedGetCalledClassFallback(string $function): string
{
$function = $this->getLiteralString($function);
$resolution = $this->getFunctionResolutionCache();
$this->compilationStatistics->record(CompilationStatistics::DIRECT_FUNCTIONS, 'function_exists');
return '([&]() -> php::Var { auto &resolution = ' . $resolution
. '; if (resolution == 0) { resolution = php::fn::function_exists(' . $function
. ') ? 1 : 2; } return resolution == 1 ? typephp_call_cached('
. $function . ', ' . $this->getFunctionCallCache() . ') : php::Var('
. $this->getCalledClassExpr() . '); })()';
}
protected function parsePipeOperator(Expr\BinaryOp\Pipe $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->left, 'pipe left operand');
@ -112,18 +172,25 @@ trait FunctionCallTrait
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;
$name = '';
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name);
$globalName = ltrim($name, '\\');
} elseif ($expr->name instanceof Node\Name) {
$functionTarget = $this->resolveStaticFunctionCallTarget($expr->name);
$name = $functionTarget['target'];
$globalName = $functionTarget['lower'];
$this->compilationStatistics->record(
CompilationStatistics::FUNCTIONS,
strtolower($globalName),
$globalName,
);
$namedExit = $this->parseNamedExitMessageCall($globalName, $expr);
$namedExit = $functionTarget['definitelyGlobal']
? $this->parseNamedExitMessageCall($globalName, $expr)
: null;
if ($namedExit !== null) {
return $namedExit;
}
if ($globalName === 'clone' && !$expr->isFirstClassCallable() && $this->class) {
if ($functionTarget['definitelyGlobal']
&& $globalName === 'clone'
&& !$expr->isFirstClassCallable()
&& $this->class
) {
// PHP 8.5 clone-with applies property updates in the lexical
// scope of the call site. Direct AOT method calls do not leave
// a Zend execute frame on top, so preserve that scope while
@ -132,53 +199,23 @@ trait FunctionCallTrait
? 'php::FakeScopeGuard::current()'
: $this->getClassEntryPtr($this->getFullClassName());
}
if ($globalName === 'get_called_class' && $this->classDef?->nativeObject) {
$this->fatalError(
$expr,
'Native classes do not support late static binding; use `self::class` or a concrete class name',
);
}
if (($globalName === 'get_class' || $globalName === 'get_parent_class')
&& (($expr->args === [] && $this->classDef?->nativeObject)
|| ($expr->args !== []
&& $this->isNativeObjectClass($this->detectClassOfExpr($expr->args[0]->value))))
) {
$replacement = $globalName === 'get_class'
? '`self::class` or a concrete class name'
: '`parent::class` or a concrete class name';
$this->fatalError(
$expr,
"Native classes do not support runtime class introspection; use {$replacement}",
);
}
// Capability policy is determined by the target, not by the
// extensions loaded into the build-time PHP process. Otherwise a
// forbidden direct call could bypass validation merely because
// that host PHP does not expose the function.
$this->assertWasiFunctionSupported($expr, $globalName);
$this->assertNanoFunctionSupported($expr, $globalName);
if ($this->isInternalFunction($globalName)) {
$this->markInternalFunctionCallbackCall($globalName, $expr->args);
}
if (in_array($globalName, Constants::UNSUPPORTED_FUNCTIONS, true)) {
$this->fatalError($expr, 'Unsupported function: `' . $globalName . '`');
}
$nativeFn = $this->findNativeFunction($name);
$nativeFn = $this->findNativeFunction($functionTarget['nativeLookup']);
if ($nativeFn) {
$functionDef = $this->getFunction($nativeFn);
$resolvedTarget = $functionDef->getNamespacedName();
$expr->setAttribute('nativeCall', $nativeFn);
if ($expr->isFirstClassCallable()
&& $this->functionUsesNativeObject($this->getFunction($nativeFn))
&& $this->functionUsesNativeObject($functionDef)
) {
$this->fatalError($expr, 'Native ABI functions cannot be converted to Zend closures');
}
// Function call placeholder, not a real function call
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
return $this->genPlaceHolder($this->getLiteralString($resolvedTarget));
}
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
$this->checkNativeCallArgs($expr, $functionDef, $expr->args, $resolvedTarget);
if ($this->shouldUseDynamicCallForNativeArgs($nativeFn, $expr->args)) {
$functionDef = $this->getFunction($nativeFn);
return $this->genRuntimeFunctionCall($this->getFuncPtr($functionDef->getNamespacedName()), $expr->args, $name);
return $this->genRuntimeFunctionCall($this->getFuncPtr($resolvedTarget), $expr->args, $resolvedTarget);
}
try {
$callee = $expr->getAttribute(self::ATTR_MULTI_RETURN_IMPL, false)
@ -186,13 +223,84 @@ trait FunctionCallTrait
: self::PREFIX . $nativeFn;
return $callee . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
} catch (PlaceHolder) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
return $this->genPlaceHolder($this->getLiteralString($resolvedTarget));
}
}
$mayCallGlobalBuiltin = $functionTarget['definitelyGlobal'] || $functionTarget['namespacedFallback'];
$isNamespacedGetCalledClassFallback = $functionTarget['namespacedFallback']
&& $globalName === 'get_called_class'
&& $expr->args === []
&& $this->methodDef !== null
&& !$this->classDef?->nativeObject;
// Policy applies only after compiled namespace shadows/imported
// user functions have had a chance to resolve. A missing
// namespaced short name can still fall back to a global builtin.
if ($mayCallGlobalBuiltin) {
$this->assertWasiFunctionSupported($expr, $globalName);
$this->assertNanoFunctionSupported($expr, $globalName);
if (!$isNamespacedGetCalledClassFallback && $this->isInternalFunction($globalName)) {
$this->markInternalFunctionCallbackCall($globalName, $expr->args);
}
if (in_array($globalName, Constants::UNSUPPORTED_FUNCTIONS, true)) {
$this->fatalError($expr, 'Unsupported function: `' . $globalName . '`');
}
}
if ($mayCallGlobalBuiltin
&& ($globalName === 'get_class' || $globalName === 'get_parent_class')
&& (($expr->args === [] && $this->classDef?->nativeObject)
|| ($expr->args !== []
&& $this->isNativeObjectClass($this->detectClassOfExpr($expr->args[0]->value))))
) {
$replacement = $globalName === 'get_class'
? '`self::class` or a concrete class name'
: '`parent::class` or a concrete class name';
$this->fatalError(
$expr,
"Native classes do not support runtime class introspection; use {$replacement}",
);
}
if ($mayCallGlobalBuiltin
&& $globalName === 'get_called_class'
&& $this->classDef?->nativeObject
) {
$this->fatalError(
$expr,
'Native classes do not support late static binding; use `self::class` or a concrete class name',
);
}
// For dynamically dispatched functions, convert the function name to its fully qualified name including the namespace
$name = $this->getNamespacedFuncName($name);
$this->checkInternalFunctionArgCount($name, $expr);
$code = $this->parseFuncCallWithOptimizer($name, $expr);
$name = $functionTarget['target'];
if ($functionTarget['definitelyGlobal']) {
$name = strtolower($name);
}
if ($mayCallGlobalBuiltin && !$isNamespacedGetCalledClassFallback) {
$this->checkInternalFunctionArgCount($name, $expr);
}
if ($functionTarget['definitelyGlobal']
&& $globalName === 'get_called_class'
&& $expr->args === []
&& $this->methodDef !== null
&& !$this->classDef?->nativeObject
) {
// Direct AOT calls have no Zend method frame for the builtin
// to inspect. Reuse the runtime scope used by static::class.
return $this->getCalledClassExpr();
}
if ($functionTarget['namespacedFallback']
&& $globalName === 'get_called_class'
&& $expr->args === []
&& $this->methodDef !== null
&& !$this->classDef?->nativeObject
) {
return $this->parseNamespacedGetCalledClassFallback(
$this->namespace . '\\' . ltrim($functionTarget['source'], '\\'),
);
}
$canOptimizeBuiltinFallback = $functionTarget['definitelyGlobal']
|| ($functionTarget['namespacedFallback'] && $globalName !== 'get_called_class');
$code = $canOptimizeBuiltinFallback
? $this->parseFuncCallWithOptimizer($globalName, $expr)
: false;
if ($code !== false) {
// Constant folding and native container operations do not
// retain the PHP function. Record only emitted stdlib calls.
@ -208,7 +316,7 @@ trait FunctionCallTrait
CompilationStatistics::RUNTIME_FUNCTIONS,
strtolower($globalName),
);
$placeHolder = $this->identifierToStr($expr->name);
$placeHolder = $this->getLiteralString($functionTarget['target']);
$fn = $this->getFuncPtr($name);
if ($this->debug) {
$this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()');

@ -78,7 +78,7 @@ trait DeclarationSymbolTrait
$type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type;
$alias = $this->registerUseImportAlias($use, $type, $id);
if ($type === Node\Stmt\Use_::TYPE_FUNCTION) {
$this->useFunctions[$alias] = $id;
$this->useFunctions[strtolower($alias)] = $id;
} elseif ($type === Node\Stmt\Use_::TYPE_CONSTANT) {
// $id is already the fully qualified constant name. Splitting
// and re-joining it on `\` corrupted single-segment imports:

@ -146,8 +146,9 @@ trait NameResolutionTrait
if ($funcName[0] == '\\') {
return ltrim($funcName, '\\');
}
if (isset($this->useFunctions[$funcName])) {
return $this->useFunctions[$funcName];
$import = strtolower($funcName);
if (isset($this->useFunctions[$import])) {
return $this->useFunctions[$import];
}
return $funcName;
}

@ -998,6 +998,7 @@ class Translator extends Preprocessor
$lines[] = 'enum class PropertyCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'enum class MethodCallCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'enum class FunctionCallCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'enum class FunctionResolutionCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_func(RequestFuncId func_id, const php::Str &func_name);';
@ -1009,6 +1010,7 @@ class Translator extends Preprocessor
$lines[] = 'php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id) noexcept;' . PHP_EOL;
$lines[] = 'php::MethodCallCacheSlot &typephp_get_method_call_cache(MethodCallCacheId cache_id) noexcept;' . PHP_EOL;
$lines[] = 'php::FunctionCallCacheSlot &typephp_get_function_call_cache(FunctionCallCacheId cache_id) noexcept;' . PHP_EOL;
$lines[] = 'uint8_t &typephp_get_function_resolution_cache(FunctionResolutionCacheId cache_id) noexcept;' . PHP_EOL;
foreach ($this->getClassLikesWithConstants() as $classDef) {
foreach ($classDef->constants as $constant) {
@ -1199,6 +1201,8 @@ class Translator extends Preprocessor
. max(1, $this->methodCallCacheIndex) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'php::FunctionCallCacheSlot function_call_cache_map['
. max(1, $this->functionCallCacheIndex) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'uint8_t function_resolution_cache_map['
. max(1, $this->functionResolutionCacheIndex) . ']{};' . PHP_EOL;
$code .= '};' . PHP_EOL;
$code .= 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;' . PHP_EOL;
@ -1289,6 +1293,10 @@ php::MethodCallCacheSlot &typephp_get_method_call_cache(MethodCallCacheId cache_
php::FunctionCallCacheSlot &typephp_get_function_call_cache(FunctionCallCacheId cache_id) noexcept {
return php_request_cache->function_call_cache_map[static_cast<uint32_t>(cache_id)];
}
uint8_t &typephp_get_function_resolution_cache(FunctionResolutionCacheId cache_id) noexcept {
return php_request_cache->function_resolution_cache_map[static_cast<uint32_t>(cache_id)];
}
CODE;
$code .= "\n\n";

@ -0,0 +1,84 @@
--TEST--
get_called_class preserves runtime scope across direct instance and static calls
--FILE--
<?php
namespace CalledShadow {
function get_called_class(): string { return 'shadow'; }
class Probe {
public function name(): string { return get_called_class(); }
}
}
namespace CalledAliasTarget {
function get_called_class(): string { return 'alias'; }
}
namespace CalledAlias {
use function CalledAliasTarget\get_called_class as CALLED_ALIAS;
class Probe {
public function name(): string { return cAlLeD_aLiAs(); }
}
}
namespace CalledDynamicShadow {
class Probe {
public function name(): string { return get_called_class(); }
}
}
namespace CalledDynamicFrozen {
class Probe {
public function name(): string { return get_called_class(); }
}
class Child extends Probe {}
}
namespace {
use function get_called_class as calledName;
trait CalledNameTrait {
public function traitName(): string { return get_called_class(); }
}
class CalledScopeBase {
use CalledNameTrait;
final public function name(): string { return get_called_class(); }
public static function staticName(): string { return \GET_CALLED_CLASS(); }
public function aliasName(): string { return calledName(); }
public function closureName(): string {
$name = function (): string { return get_called_class(); };
return $name();
}
}
class CalledScopeChild extends CalledScopeBase {}
function printCalledNames(CalledScopeBase $object): void {
echo $object->name(), ':', $object->traitName(), ':',
$object->aliasName(), ':', $object->closureName(), "\n";
}
function main(): void {
printCalledNames(new CalledScopeBase());
printCalledNames(new CalledScopeChild());
echo CalledScopeBase::staticName(), ':', CalledScopeChild::staticName(), "\n";
eval('class RuntimeCalledScope extends CalledScopeBase {}');
$runtime = eval('return new RuntimeCalledScope();');
printCalledNames($runtime);
echo (new \CalledShadow\Probe())->name(), "\n";
echo (new \CalledAlias\Probe())->name(), "\n";
eval('namespace CalledDynamicShadow { function get_called_class(): string { return "shadow-first"; } }');
echo (new \CalledDynamicShadow\Probe())->name(), "\n";
echo (new \CalledDynamicFrozen\Probe())->name(), "\n";
eval('namespace CalledDynamicFrozen { function get_called_class(): string { return "shadow-late"; } }');
echo (new \CalledDynamicFrozen\Child())->name(), "\n";
try {
get_called_class();
} catch (\Error $error) {
echo $error->getMessage(), "\n";
}
}
}
?>
--EXPECT--
CalledScopeBase:CalledScopeBase:CalledScopeBase:CalledScopeBase
CalledScopeChild:CalledScopeChild:CalledScopeChild:CalledScopeChild
CalledScopeBase:CalledScopeChild
RuntimeCalledScope:RuntimeCalledScope:RuntimeCalledScope:RuntimeCalledScope
shadow
alias
shadow-first
CalledDynamicFrozen\Probe
CalledDynamicFrozen\Child
get_called_class() must be called from within a class

@ -0,0 +1,45 @@
--TEST--
namespaced builtin fallbacks retain direct optimizer paths while compiled shadows win
--FILE--
<?php
namespace NamespacedBuiltinFallback {
class Probe {
private array $blocks;
private int $size = 0;
public function __construct(array $blocks) {
$this->blocks = $blocks;
}
public function countBlocks(): int {
$this->size = count($this->blocks);
return $this->size;
}
public function matches(): bool {
return in_array('needle', $this->blocks, true)
&& str_contains('needle in haystack', 'needle');
}
}
function strlen(string $value): int {
return 7;
}
function shadowedLength(): int {
return strlen('ignored');
}
}
namespace {
function main(): void {
$probe = new \NamespacedBuiltinFallback\Probe(['needle']);
echo $probe->countBlocks(), "\n";
var_dump($probe->matches());
echo \NamespacedBuiltinFallback\shadowedLength(), "\n";
}
}
?>
--EXPECT--
1
bool(true)
7
Loading…
Cancel
Save