diff --git a/ext/standard/ClassExistsJitHelper.php b/ext/standard/ClassExistsJitHelper.php new file mode 100644 index 000000000..bda2c4e56 --- /dev/null +++ b/ext/standard/ClassExistsJitHelper.php @@ -0,0 +1,32 @@ +getTypeFromString('int1'); - $i32 = $context->getTypeFromString('int32'); - $exists = $i1->constInt(0, false); - $strcasecmpFn = $context->lookupFunction('strcasecmp'); - $nameData = self::stringDataPtr($context, $nameStr); - - $candidates = $context->type->object->allDeclaredClassLowerNames(); - if (null !== $context->runtime->vmContext) { - $candidates = array_values(array_unique(array_merge( - $candidates, - array_keys($context->runtime->vmContext->classes) - ))); - } - - foreach ($candidates as $lc) { - if (\PHPCompiler\VM\ResourceSupport::isHiddenPseudoClassLc($lc)) { - continue; - } - $candidate = $context->builder->load($context->constantStringFromString($lc)); - $candidateData = self::stringDataPtr($context, $candidate); - $cmp = $context->builder->call($strcasecmpFn, $nameData, $candidateData); - $match = $context->builder->icmp(Builder::INT_EQ, $cmp, $i32->constInt(0, false)); - $exists = $context->builder->or($exists, $match); - } - - return $exists; + return StringClassExists::invoke($context, $nameStr); } public static function stringDataPtr(Context $context, Value $strPtr): Value diff --git a/ext/standard/JitInterfaceExists.php b/ext/standard/JitInterfaceExists.php index 1627ed021..afd4f07fa 100644 --- a/ext/standard/JitInterfaceExists.php +++ b/ext/standard/JitInterfaceExists.php @@ -2,17 +2,19 @@ declare(strict_types=1); +/** + * JIT/AOT helper for interface_exists() via InterfaceExistsJitHelper PHP (#1371, #16185). + */ + namespace PHPCompiler\ext\standard; -use PHPCompiler\JIT\Builtin\StringCaseCompare; +use PHPCompiler\JIT\Builtin\StringInterfaceExists; use PHPCompiler\JIT\Context; use PHPCompiler\JIT\JitStringArg; use PHPCompiler\JIT\ReflectionBuiltinHelper; use PHPCompiler\JIT\Variable as JITVariable; -use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for interface_exists() (issue #1371, #2312). */ final class JitInterfaceExists { public static function invoke(Context $context, JITVariable $nameArg): Value @@ -22,37 +24,9 @@ public static function invoke(Context $context, JITVariable $nameArg): Value return ReflectionBuiltinHelper::interfaceExistsLiteral($context, $literal); } - $nameStr = JitStringArg::lower($context, $nameArg, 'interface_exists() interface name'); - StringCaseCompare::ensureStrcasecmpLinked($context); - $i1 = $context->getTypeFromString('int1'); - $i32 = $context->getTypeFromString('int32'); - $exists = $i1->constInt(0, false); - $strcasecmpFn = $context->lookupFunction('strcasecmp'); - $nameData = JitClassExists::stringDataPtr($context, $nameStr); - - $candidates = []; - foreach ($context->type->object->allDeclaredClassLowerNames() as $lc) { - if ($context->type->object->isInterfaceClassLc($lc)) { - $candidates[] = $lc; - } - } - if (null !== $context->runtime->vmContext) { - foreach ($context->runtime->vmContext->classes as $lc => $entry) { - if ($entry->isInterface) { - $candidates[] = $lc; - } - } - $candidates = array_values(array_unique($candidates)); - } - - foreach ($candidates as $lc) { - $candidate = $context->builder->load($context->constantStringFromString($lc)); - $candidateData = JitClassExists::stringDataPtr($context, $candidate); - $cmp = $context->builder->call($strcasecmpFn, $nameData, $candidateData); - $match = $context->builder->icmp(Builder::INT_EQ, $cmp, $i32->constInt(0, false)); - $exists = $context->builder->or($exists, $match); - } - - return $exists; + return StringInterfaceExists::invoke( + $context, + JitStringArg::lower($context, $nameArg, 'interface_exists() interface name') + ); } } diff --git a/lib/JIT/Builtin/StringClassExists.php b/lib/JIT/Builtin/StringClassExists.php new file mode 100644 index 000000000..5c2511dae --- /dev/null +++ b/lib/JIT/Builtin/StringClassExists.php @@ -0,0 +1,98 @@ + */ + private const COMPILED_HELPERS = [ + self::INVOKE_HELPER, + ]; + + public static function ensureLinked(Context $context): void + { + self::implement($context); + } + + public static function ensureStandaloneBodies(Context $context): void + { + self::ensureLinked($context); + } + + public static function invoke(Context $context, Value $nameStr): Value + { + self::ensureLinked($context); + + return $context->builder->call( + $context->lookupFunction(self::ABI), + $nameStr + ); + } + + private static function implement(Context $context): void + { + $probe = $context->module->getNamedFunction(self::ABI); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction(self::ABI, $probe); + + return; + } + + $savedBlock = null; + try { + $savedBlock = $context->builder->getInsertBlock(); + } catch (\Throwable) { + } + + JitVmHelperLink::ensureCompiled($context, self::HELPER_PATH, self::COMPILED_HELPERS, '#16185'); + + $strPtr = $context->getTypeFromString('__string__*'); + $i1 = $context->getTypeFromString('int1'); + $fn = null !== $probe + ? $probe + : $context->module->addFunction( + self::ABI, + $context->context->functionType($i1, false, $strPtr) + ); + + $entry = $fn->appendBasicBlock('class_exists_bridge_entry'); + $context->builder->positionAtEnd($entry); + + $helperFn = JitVmHelperLink::lookupCompiled($context, self::INVOKE_HELPER, '#16185'); + $raw = JitNestedHelperCoerce::callHelper( + $context, + $helperFn, + [$fn->getParam(0)] + ); + $exists = JitNestedHelperCoerce::coerceHelperScalarResult($context, $raw, $i1); + $context->builder->returnValue($exists); + + $context->registerFunction(self::ABI, $fn); + + if (null !== $savedBlock) { + $context->builder->positionAtEnd($savedBlock); + } else { + $context->builder->clearInsertionPosition(); + } + } +} diff --git a/lib/JIT/Builtin/StringInterfaceExists.php b/lib/JIT/Builtin/StringInterfaceExists.php new file mode 100644 index 000000000..d95d9f1bd --- /dev/null +++ b/lib/JIT/Builtin/StringInterfaceExists.php @@ -0,0 +1,98 @@ + */ + private const COMPILED_HELPERS = [ + self::INVOKE_HELPER, + ]; + + public static function ensureLinked(Context $context): void + { + self::implement($context); + } + + public static function ensureStandaloneBodies(Context $context): void + { + self::ensureLinked($context); + } + + public static function invoke(Context $context, Value $nameStr): Value + { + self::ensureLinked($context); + + return $context->builder->call( + $context->lookupFunction(self::ABI), + $nameStr + ); + } + + private static function implement(Context $context): void + { + $probe = $context->module->getNamedFunction(self::ABI); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction(self::ABI, $probe); + + return; + } + + $savedBlock = null; + try { + $savedBlock = $context->builder->getInsertBlock(); + } catch (\Throwable) { + } + + JitVmHelperLink::ensureCompiled($context, self::HELPER_PATH, self::COMPILED_HELPERS, '#16185'); + + $strPtr = $context->getTypeFromString('__string__*'); + $i1 = $context->getTypeFromString('int1'); + $fn = null !== $probe + ? $probe + : $context->module->addFunction( + self::ABI, + $context->context->functionType($i1, false, $strPtr) + ); + + $entry = $fn->appendBasicBlock('interface_exists_bridge_entry'); + $context->builder->positionAtEnd($entry); + + $helperFn = JitVmHelperLink::lookupCompiled($context, self::INVOKE_HELPER, '#16185'); + $raw = JitNestedHelperCoerce::callHelper( + $context, + $helperFn, + [$fn->getParam(0)] + ); + $exists = JitNestedHelperCoerce::coerceHelperScalarResult($context, $raw, $i1); + $context->builder->returnValue($exists); + + $context->registerFunction(self::ABI, $fn); + + if (null !== $savedBlock) { + $context->builder->positionAtEnd($savedBlock); + } else { + $context->builder->clearInsertionPosition(); + } + } +} diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index 40dff40d2..f7f1f628c 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -788,6 +788,7 @@ require_once __DIR__.'/../../../ext/standard/JitChunkSplit.php'; require_once __DIR__.'/../../../ext/standard/JitClassAlias.php'; require_once __DIR__.'/../../../ext/standard/JitClassConstants.php'; +require_once __DIR__.'/../../../ext/standard/ClassExistsJitHelper.php'; require_once __DIR__.'/../../../ext/standard/JitClassExists.php'; require_once __DIR__.'/../../../ext/standard/JitClassImplements.php'; require_once __DIR__.'/../../../ext/standard/JitClassParents.php'; @@ -973,6 +974,7 @@ require_once __DIR__.'/../../../ext/standard/JitIniParseQuantity.php'; require_once __DIR__.'/../../../ext/standard/JitIniValueArg.php'; require_once __DIR__.'/../../../ext/standard/JitIntdiv.php'; +require_once __DIR__.'/../../../ext/standard/InterfaceExistsJitHelper.php'; require_once __DIR__.'/../../../ext/standard/JitInterfaceExists.php'; require_once __DIR__.'/../../../ext/standard/JitIptcEmbed.php'; require_once __DIR__.'/../../../ext/standard/JitIptcParse.php'; @@ -3072,6 +3074,7 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/StringHttpBuildQuery.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringIdate.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringInfo.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/StringInterfaceExists.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringJsonDecode.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringJsonDecodeInventoryStubs.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringJsonEncode.php'; @@ -3087,6 +3090,7 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/StringMicrotime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringMktime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringCaseCompare.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassExists.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringNCompare.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringNaturalCompare.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringNetInterfacesJit.php'; diff --git a/test/unit/ClassInterfaceExistsRuntimeShrinkTest.php b/test/unit/ClassInterfaceExistsRuntimeShrinkTest.php new file mode 100644 index 000000000..34396765c --- /dev/null +++ b/test/unit/ClassInterfaceExistsRuntimeShrinkTest.php @@ -0,0 +1,65 @@ +assertStringContainsString('StringClassExists::invoke', $source); + $this->assertStringNotContainsString("lookupFunction('strcasecmp'", $source); + $this->assertStringContainsString('stringDataPtr', $source); + $this->assertLessThan(35, \substr_count($source, "\n") + 1); + } + + public function testJitInterfaceExistsDelegatesToStringInterfaceExistsBridge(): void + { + $source = (string) file_get_contents(__DIR__.'/../../ext/standard/JitInterfaceExists.php'); + $this->assertStringContainsString('StringInterfaceExists::invoke', $source); + $this->assertStringNotContainsString("lookupFunction('strcasecmp'", $source); + $this->assertStringNotContainsString('JitClassExists::stringDataPtr', $source); + $this->assertLessThan(35, \substr_count($source, "\n") + 1); + } + + public function testStringClassExistsUsesJitHelperNotStrcasecmpLoop(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringClassExists.php'); + $this->assertStringContainsString('ClassExistsJitHelper', $source); + $this->assertStringContainsString('JitVmHelperLink', $source); + $this->assertStringNotContainsString("lookupFunction('strcasecmp'", $source); + } + + public function testStringInterfaceExistsUsesJitHelperNotStrcasecmpLoop(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringInterfaceExists.php'); + $this->assertStringContainsString('InterfaceExistsJitHelper', $source); + $this->assertStringContainsString('JitVmHelperLink', $source); + $this->assertStringNotContainsString("lookupFunction('strcasecmp'", $source); + } + + public function testJitHelpersDelegateToVmReflection(): void + { + $classHelper = (string) file_get_contents(__DIR__.'/../../ext/standard/ClassExistsJitHelper.php'); + $this->assertStringContainsString('VmReflection::classExists', $classHelper); + $this->assertStringContainsString('Superglobals::getActiveContext', $classHelper); + + $interfaceHelper = (string) file_get_contents(__DIR__.'/../../ext/standard/InterfaceExistsJitHelper.php'); + $this->assertStringContainsString('VmReflection::interfaceExists', $interfaceHelper); + $this->assertStringContainsString('Superglobals::getActiveContext', $interfaceHelper); + } + + public function testSpineBundleIncludesClassInterfaceExistsJitHelpers(): void + { + $spine = (string) file_get_contents(__DIR__.'/../../test/selfhost/compiler_lib_spine_smoke/main.php'); + $this->assertStringContainsString('ClassExistsJitHelper.php', $spine); + $this->assertStringContainsString('InterfaceExistsJitHelper.php', $spine); + $this->assertStringContainsString('StringClassExists.php', $spine); + $this->assertStringContainsString('StringInterfaceExists.php', $spine); + } +}