diff --git a/lib/JIT/Context.php b/lib/JIT/Context.php index 79746fbc35a..bc4f6191e48 100644 --- a/lib/JIT/Context.php +++ b/lib/JIT/Context.php @@ -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]; @@ -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. * diff --git a/test/unit/ExternalMethodBindTest.php b/test/unit/ExternalMethodBindTest.php index da69aacf75c..22a33ee1533 100644 --- a/test/unit/ExternalMethodBindTest.php +++ b/test/unit/ExternalMethodBindTest.php @@ -6,6 +6,7 @@ use PHPCompiler\AOT\ExternalMethodBind; use PHPCompiler\JIT\Call\ExternalMethod; +use PHPCompiler\JIT\UserScriptAotEnv; use PHPUnit\Framework\TestCase; /** @@ -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(); } @@ -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). */