From 3eb382b16f54d3ddae4df6ca1046ece1b7a342bf Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 6 Jul 2026 16:40:26 +0000 Subject: [PATCH] php-in-php: class_implements() JIT via ClassImplementsJitHelper PHP (#16960) Route dynamic class_implements() operands through VmReflection PHP helper and a thin __phpc_jit_class_implements bridge; keep compile-time object/value LLVM fast paths for AOT parity with master. Co-authored-by: Cursor --- ext/standard/ClassImplementsJitHelper.php | 37 ++++++ ext/standard/JitClassImplements.php | 111 ++++++++++++------ lib/JIT/Builtin/StringClassImplements.php | 87 ++++++++++++++ .../compiler_lib_spine_smoke/main.php | 2 + .../unit/ClassImplementsRuntimeShrinkTest.php | 41 +++++++ 5 files changed, 244 insertions(+), 34 deletions(-) create mode 100644 ext/standard/ClassImplementsJitHelper.php create mode 100644 lib/JIT/Builtin/StringClassImplements.php create mode 100644 test/unit/ClassImplementsRuntimeShrinkTest.php diff --git a/ext/standard/ClassImplementsJitHelper.php b/ext/standard/ClassImplementsJitHelper.php new file mode 100644 index 0000000000..932606ee3f --- /dev/null +++ b/ext/standard/ClassImplementsJitHelper.php @@ -0,0 +1,37 @@ +bool(false); + + return $result; + } + + return VmReflection::classImplementsArray($entry, $ctx); + } +} diff --git a/ext/standard/JitClassImplements.php b/ext/standard/JitClassImplements.php index 882049ce32..1cb0f5e381 100644 --- a/ext/standard/JitClassImplements.php +++ b/ext/standard/JitClassImplements.php @@ -5,6 +5,8 @@ namespace PHPCompiler\ext\standard; use PHPCompiler\JIT\BasicBlockHelper; +use PHPCompiler\JIT\Builtin\StringClassImplements; +use PHPCompiler\JIT\Builtin\Type\Object_ as ObjectBuiltin; use PHPCompiler\JIT\Context; use PHPCompiler\JIT\HashTableHelper; use PHPCompiler\JIT\JitStringArg; @@ -13,51 +15,90 @@ use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for class_implements() (issue #3099). */ +/** + * JIT/AOT helper for class_implements() via ClassImplementsJitHelper PHP (#3099, #16960). + * + * Compile-time literal class names keep registry fast path; boxed/value operands route through PHP. + * php-src: ext/standard/basic_functions.c — PHP_FUNCTION(class_implements) + */ final class JitClassImplements { private static int $seq = 0; public static function invoke(Context $context, JITVariable $whatArg, bool $autoload): Value { + $compileTimeEnum = $whatArg->compileTimeEnumCase ?? null; + if (\is_array($compileTimeEnum) && isset($compileTimeEnum['classId'])) { + $object = $context->type->object; + if ($object instanceof ObjectBuiltin) { + return self::invokeForClassName( + $context, + $object->classNameForId((int) $compileTimeEnum['classId']), + $autoload + ); + } + } + + $literal = JitStringArg::compileTimeLiteral($whatArg); + if (null !== $literal) { + return self::invokeForClassName($context, $literal, $autoload); + } + if (JITVariable::TYPE_OBJECT === $whatArg->type) { return self::invokeForObject($context, $whatArg, $autoload); } - $literal = JitStringArg::compileTimeLiteral($whatArg); - if (null === $literal) { - if (JITVariable::TYPE_VALUE === $whatArg->type) { - $valuePtr = JitValueBox::valuePtrFromVariable($context, $whatArg); - $obj = $context->builder->call( - $context->lookupFunction('__value__readObject'), - $valuePtr - ); - $objType = $context->getTypeFromString('__object__*'); - $isObject = $context->builder->icmp( - Builder::INT_NE, - $obj, - $objType->constNull() - ); - if (!$isObject) { - throw new \LogicException( - 'class_implements() argument must be an object or class name string in this compiler build' - ); - } - $objVar = new JITVariable( - $context, - JITVariable::TYPE_OBJECT, - JITVariable::KIND_VALUE, - $obj - ); + if (JITVariable::TYPE_VALUE === $whatArg->type) { + $valuePtr = JitValueBox::valuePtrFromVariable($context, $whatArg); + $obj = $context->builder->call( + $context->lookupFunction('__value__readObject'), + $valuePtr + ); + $objVar = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $obj + ); - return self::invokeForObject($context, $objVar, $autoload); - } - throw new \LogicException( - 'class_implements() class name must be a string literal in this compiler build' + return self::invokeForObject($context, $objVar, $autoload); + } + + return self::routeThroughPhpHelper($context, $whatArg, $autoload); + } + + private static function routeThroughPhpHelper( + Context $context, + JITVariable $whatArg, + bool $autoload + ): Value { + $operandPtr = self::operandToValueBox($context, $whatArg); + $i1 = $context->getTypeFromString('int1'); + $autoloadVal = $autoload ? $i1->constInt(1, false) : $i1->constInt(0, false); + + return StringClassImplements::invoke($context, $operandPtr, $autoloadVal); + } + + private static function operandToValueBox(Context $context, JITVariable $whatArg): Value + { + if (JITVariable::TYPE_VALUE === $whatArg->type) { + return JitValueBox::valuePtrFromVariable($context, $whatArg); + } + if (JITVariable::TYPE_STRING === $whatArg->type) { + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $ptr, + $context->helper->loadValue($whatArg) ); + + return $ptr; } - return self::invokeForClassName($context, $literal, $autoload); + throw new \LogicException( + 'class_implements() operand must be a compile-time literal, object, or value box in this compiler build' + ); } private static function invokeForObject( @@ -65,9 +106,7 @@ private static function invokeForObject( JITVariable $objectArg, bool $autoload ): Value { - $obj = JITVariable::TYPE_OBJECT === $objectArg->type - ? $context->helper->loadValue($objectArg) - : $objectArg->value; + $obj = $context->helper->loadValue($objectArg); $objMap = $context->structFieldMap['__object__']; $classId = $context->builder->load( $context->builder->structGep($obj, $objMap['class_id']) @@ -146,6 +185,10 @@ private static function invokeForClassName(Context $context, string $className, ); } + if (!$autoload) { + return self::returnFalse($context); + } + return self::returnFalse($context); } diff --git a/lib/JIT/Builtin/StringClassImplements.php b/lib/JIT/Builtin/StringClassImplements.php new file mode 100644 index 0000000000..61ab16bd40 --- /dev/null +++ b/lib/JIT/Builtin/StringClassImplements.php @@ -0,0 +1,87 @@ + */ + 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 $objectOrClass, Value $autoload): Value + { + self::ensureLinked($context); + + return $context->builder->call( + $context->lookupFunction(self::ABI), + $objectOrClass, + $autoload + ); + } + + 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) { + } + + $valuePtr = $context->getTypeFromString('__value__*'); + $i1 = $context->getTypeFromString('int1'); + JitVmHelperLink::ensureBridge( + $context, + self::ABI, + 'class_implements_bridge_entry', + [$valuePtr, $i1], + $valuePtr, + self::INVOKE_HELPER, + self::HELPER_PATH, + self::COMPILED_HELPERS, + '#16960' + ); + + 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 82a2b94e13..02c34db0fd 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -741,6 +741,7 @@ require_once __DIR__.'/../../../ext/standard/ChownJitHelper.php'; require_once __DIR__.'/../../../ext/standard/ChunkSplitJitHelper.php'; require_once __DIR__.'/../../../ext/standard/ClassExistsJitHelper.php'; +require_once __DIR__.'/../../../ext/standard/ClassImplementsJitHelper.php'; require_once __DIR__.'/../../../ext/standard/ClassParentsJitHelper.php'; require_once __DIR__.'/../../../ext/standard/ClassUsesJitHelper.php'; require_once __DIR__.'/../../../ext/standard/CliArgvJitHelper.php'; @@ -3169,6 +3170,7 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/StringChmod.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringChunkSplit.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassExists.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassImplements.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassParents.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassUses.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringClassUsesRecursive.php'; diff --git a/test/unit/ClassImplementsRuntimeShrinkTest.php b/test/unit/ClassImplementsRuntimeShrinkTest.php new file mode 100644 index 0000000000..1c0763c0d2 --- /dev/null +++ b/test/unit/ClassImplementsRuntimeShrinkTest.php @@ -0,0 +1,41 @@ +assertStringContainsString('StringClassImplements::invoke', $source); + $this->assertStringContainsString('routeThroughPhpHelper', $source); + $this->assertStringContainsString('ClassImplementsJitHelper', (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringClassImplements.php')); + } + + public function testStringClassImplementsUsesJitHelperNotInlineLlvm(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringClassImplements.php'); + $this->assertStringContainsString('ClassImplementsJitHelper', $source); + $this->assertStringContainsString('JitVmHelperLink', $source); + } + + public function testClassImplementsJitHelperDelegatesToVmReflection(): void + { + $source = (string) file_get_contents(__DIR__.'/../../ext/standard/ClassImplementsJitHelper.php'); + $this->assertStringContainsString('VmReflection::resolveClassForClassImplements', $source); + $this->assertStringContainsString('VmReflection::classImplementsArray', $source); + $this->assertStringContainsString('Superglobals::getActiveContext', $source); + } + + public function testSpineBundleIncludesClassImplementsJitHelper(): void + { + $spine = (string) file_get_contents(__DIR__.'/../../test/selfhost/compiler_lib_spine_smoke/main.php'); + $this->assertStringContainsString('ClassImplementsJitHelper.php', $spine); + $this->assertStringContainsString('StringClassImplements.php', $spine); + } +}