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
64 changes: 64 additions & 0 deletions lib/JIT/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,14 @@ public function resolveFunctionProxy(string $proxyName): Call
if (null !== $bound) {
return $bound;
}
if ($this->isUserScriptAot() && str_contains($lc, '::')) {
$vmOnly = self::findInternalClassMethodInVmRegistry($this, $lc);
if (null !== $vmOnly && !self::internalBuiltinHasJitLowering($vmOnly)) {
throw new \LogicException(
\sprintf('%s is registered in the VM but has no JIT lowering (#36202)', $proxyName)
);
}
}
$this->functionProxies[$lc] = new Call\ExternalMethod($proxyName);

return $this->functionProxies[$lc];
Expand Down Expand Up @@ -1243,9 +1251,65 @@ private function resolveRegisteredInternalBuiltin(string $lc): ?FuncInternal
}
}

// Registered builtin class methods with JIT lowering — one table for VM and JIT (#36202).
// VM-only handlers stay on ExternalMethod until call() is implemented (helper infra).
if (!\PHPCompiler\AOT\ExternalMethodBind::spineChunkMode()) {
$vmMethod = self::findInternalClassMethodInVmRegistry($this, $lc);
if (null !== $vmMethod && self::internalBuiltinHasJitLowering($vmMethod)) {
return $vmMethod;
}
}

return null;
}

/**
* VM registry lookup for `Class::method` — one table for VM and JIT (#36202).
*/
private static function findInternalClassMethodInVmRegistry(self $context, string $lc): ?FuncInternal
{
if (!str_contains($lc, '::')) {
return null;
}
[$classLc, $methodLc] = explode('::', $lc, 2);
$classLc = strtolower(ltrim($classLc, '\\'));
$methodLc = strtolower($methodLc);
if ('' === $classLc || '' === $methodLc) {
return null;
}

$vm = $context->runtime->vmContext;
while (isset($vm->classAliases[$classLc])) {
$classLc = $vm->classAliases[$classLc];
}
if (!isset($vm->classes[$classLc])) {
return null;
}

$entry = $vm->classes[$classLc];
if (!isset($entry->methods[$methodLc])) {
return null;
}

$method = $entry->methods[$methodLc];
if (!$method instanceof FuncInternal) {
return null;
}

return $method;
}

/** True when an Internal / VmClassMethod overrides {@see VmClassMethod::call()} for JIT. */
private static function internalBuiltinHasJitLowering(FuncInternal $method): bool
{
if (!$method instanceof \PHPCompiler\VM\Builtin\VmClassMethod) {
return true;
}
$ref = new \ReflectionMethod($method, 'call');

return \PHPCompiler\VM\Builtin\VmClassMethod::class !== $ref->getDeclaringClass()->getName();
}

/**
* Internal builtins safe to resolve from Runtime modules during SPINE_CHUNK chunk emits.
*
Expand Down
42 changes: 42 additions & 0 deletions test/unit/ExternalMethodBindTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPCompiler\AOT\ExternalMethodBind;
use PHPCompiler\JIT\Call\ExternalMethod;
use PHPCompiler\JIT\UserScriptAotEnv;
use PHPUnit\Framework\TestCase;

/**
Expand All @@ -23,6 +24,9 @@ protected function tearDown(): void
unset($_ENV[ExternalMethodBind::ENV_MANIFEST], $_SERVER[ExternalMethodBind::ENV_MANIFEST]);
putenv('PHP_COMPILER_HELPER_RUNTIME_O');
unset($_ENV['PHP_COMPILER_HELPER_RUNTIME_O'], $_SERVER['PHP_COMPILER_HELPER_RUNTIME_O']);
putenv('PHP_COMPILER_AOT_USER_SCRIPT');
unset($_ENV['PHP_COMPILER_AOT_USER_SCRIPT'], $_SERVER['PHP_COMPILER_AOT_USER_SCRIPT']);
UserScriptAotEnv::resetLatchForTest();
ExternalMethodBind::resetManifestForTests();
parent::tearDown();
}
Expand Down Expand Up @@ -198,6 +202,44 @@ public function testTryBindFromChunkManifestBitcode(): void
$this->assertInstanceOf(\PHPLLVM\Type::class, $bound->argTypes[0]);
}

/**
* User-script AOT must not silently null a registered-but-unlowered builtin method (#36202).
*/
public function testUserScriptAotRejectsUnloweredVmClassMethod(): void
{
putenv('PHP_COMPILER_AOT_USER_SCRIPT=1');
$_ENV['PHP_COMPILER_AOT_USER_SCRIPT'] = '1';
$runtime = new Runtime(Runtime::MODE_AOT);
$ctx = new JIT\Context($runtime, JIT\Builtin::LOAD_TYPE_STANDALONE);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('datetime::__serialize');
$ctx->resolveFunctionProxy('datetime::__serialize');
}

/**
* VM-only class methods keep ExternalMethod stubs outside user-script AOT (#36202).
*/
public function testVmOnlyClassMethodStaysExternalMethodOutsideUserScript(): void
{
$runtime = new Runtime(Runtime::MODE_AOT);
$ctx = new JIT\Context($runtime, JIT\Builtin::LOAD_TYPE_STANDALONE);
$proxy = $ctx->resolveFunctionProxy('datetime::__serialize');
$this->assertInstanceOf(ExternalMethod::class, $proxy);
}

/**
* Spine chunk keeps cross-TU class methods on ExternalMethod until manifest bind (#24429).
*/
public function testSpineChunkSkipsVmRegistryClassMethodLookup(): void
{
putenv(ExternalMethodBind::ENV_SPINE_CHUNK.'=1');
$_ENV[ExternalMethodBind::ENV_SPINE_CHUNK] = '1';
$runtime = new Runtime(Runtime::MODE_AOT);
$ctx = new JIT\Context($runtime, JIT\Builtin::LOAD_TYPE_STANDALONE);
$proxy = $ctx->resolveFunctionProxy('datetime::__serialize');
$this->assertInstanceOf(ExternalMethod::class, $proxy);
}

/**
* Consumer chunk may bind symbols from multiple producer manifests (#36155 Phase C).
*/
Expand Down
Loading