From d6eec128cf20422595e43fe5375c1e52426871c8 Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 25 Jun 2026 11:23:14 +0000 Subject: [PATCH] php-in-php: route ArrayAccess JIT through VmArrayAccess PHP SSOT (#10246) Move ~225 lines of ArrayAccess offset LLVM lowering from lib/JIT/ArrayAccessHelper.php into lib/VM/VmArrayAccess.php (FromCallableHelper pattern). JIT helper is now a thin trampoline; VM ArrayAccessDimension remains runtime SSOT. Verification: OK (2 tests) ArrayAccessHelperRuntimeShrinkTest OK (3 tests, 1 skipped) ArrayAccessJITTest + ForeachIteratorPolymorphicJitTest filter check-selfhost-spine-coverage-sync: OK Co-authored-by: Cursor --- lib/JIT/ArrayAccessHelper.php | 249 +------------- lib/VM/VmArrayAccess.php | 308 ++++++++++++++++++ .../ArrayAccessHelperRuntimeShrinkTest.php | 29 ++ 3 files changed, 349 insertions(+), 237 deletions(-) create mode 100644 lib/VM/VmArrayAccess.php create mode 100644 test/unit/ArrayAccessHelperRuntimeShrinkTest.php diff --git a/lib/JIT/ArrayAccessHelper.php b/lib/JIT/ArrayAccessHelper.php index a460a48ad62..ede11261291 100644 --- a/lib/JIT/ArrayAccessHelper.php +++ b/lib/JIT/ArrayAccessHelper.php @@ -4,37 +4,23 @@ namespace PHPCompiler\JIT; -use PHPCompiler\JIT\Call; -use PHPCompiler\JIT\Call\RuntimeIndirectInstanceMethodCall; -use PHPCompiler\VM\ErrorReporter; +use PHPCompiler\VM\VmArrayAccess; use PHPCfg\Operand; -use PHPCfg\Operand\Literal; -use PHPTypes\Type; -use PHPLLVM\Builder; use PHPLLVM\Value; /** - * JIT lowering for ArrayAccess $obj[$key] (Zend read_dimension / write_dimension, #3331, #4012). + * JIT trampoline for ArrayAccess $obj[$key] (Zend read_dimension / write_dimension, #3331, #4012, #10246). + * + * SSOT: {@see \PHPCompiler\VM\VmArrayAccess} */ final class ArrayAccessHelper { - private const IFACE_LC = 'arrayaccess'; - public static function containerImplementsArrayAccess( Context $context, Variable $container, ?Operand $containerOp ): bool { - $classLc = self::resolveContainerClassLc($container, $containerOp); - if (null === $classLc || 'object' === $classLc) { - return false; - } - - return in_array( - self::IFACE_LC, - $context->type->object->allInterfacesForClassLc($classLc), - true - ); + return VmArrayAccess::containerImplementsArrayAccess($context, $container, $containerOp); } public static function tryCompileDimFetch( @@ -44,32 +30,7 @@ public static function tryCompileDimFetch( ?Operand $containerOp, bool $forWrite ): ?Variable { - if ($container->isArrayAccessWritableOffset) { - if ($forWrite) { - $receiver = $container->writableArrayAccessReceiver; - if (null === $receiver) { - throw new \LogicException('ArrayAccess writable offset missing receiver'); - } - self::emitIndirectModifyNotice( - $context, - self::resolveContainerClassLc($receiver, $containerOp) ?? 'ArrayAccess' - ); - - return self::discardAssignTarget($context); - } - - return self::offsetGet($context, $container->writableArrayAccessReceiver, $dim); - } - - if (!self::canUseArrayAccess($context, $container, $containerOp)) { - return null; - } - - if ($forWrite) { - return self::writableOffset($context, $container, $dim); - } - - return self::offsetGet($context, $container, $dim); + return VmArrayAccess::tryCompileDimFetch($context, $container, $dim, $containerOp, $forWrite); } public static function tryCompileOffsetIsSet( @@ -78,21 +39,7 @@ public static function tryCompileOffsetIsSet( Variable $dim, ?Operand $containerOp ): ?Value { - if (!self::canUseArrayAccess($context, $container, $containerOp)) { - return null; - } - - $raw = self::invokeOffsetMethod($context, 'offsetexists', $container, $dim); - $slot = JitValueBox::alloc($context); - JitValueBox::copyFromPointer( - $context, - $slot, - JitValueBox::normalizeValuePtr($context, $raw) - ); - $boxed = new Variable($context, Variable::TYPE_VALUE, Variable::KIND_VARIABLE, $slot); - $boxed->addref(); - - return (new \PHPCompiler\ext\standard\boolval())->call($context, $boxed); + return VmArrayAccess::tryCompileOffsetIsSet($context, $container, $dim, $containerOp); } public static function tryCompileOffsetUnset( @@ -101,12 +48,7 @@ public static function tryCompileOffsetUnset( Variable $dim, ?Operand $containerOp ): bool { - if (!self::canUseArrayAccess($context, $container, $containerOp)) { - return false; - } - self::invokeOffsetMethod($context, 'offsetunset', $container, $dim); - - return true; + return VmArrayAccess::tryCompileOffsetUnset($context, $container, $dim, $containerOp); } public static function isKnownNonArrayAccessObject( @@ -114,188 +56,21 @@ public static function isKnownNonArrayAccessObject( Variable $container, ?Operand $containerOp ): bool { - if (Variable::TYPE_OBJECT !== $container->type) { - return false; - } - $classLc = self::resolveContainerClassLc($container, $containerOp); - if (null === $classLc || 'object' === $classLc) { - return false; - } - - return !in_array( - self::IFACE_LC, - $context->type->object->allInterfacesForClassLc($classLc), - true - ); + return VmArrayAccess::isKnownNonArrayAccessObject($context, $container, $containerOp); } public static function emitIllegalOffset(Context $context): void { - $message = 'Illegal offset'; - $context->builder->call( - $context->lookupFunction('__compiler_jit_raise_logic_exception'), - self::stringDataPtrFromLiteral($context, $message), - $context->constantFromInteger(strlen($message), 'size_t') - ); + VmArrayAccess::emitIllegalOffset($context); } public static function emitIndirectModifyNotice(Context $context, string $className): void { - $message = sprintf( - 'Indirect modification of overloaded element of %s has no effect', - $className - ); - $i8p = $context->getTypeFromString('int8*'); - $sizeT = $context->getTypeFromString('size_t'); - $i32 = $context->getTypeFromString('int32'); - $msgPtr = $context->builder->pointerCast($context->constantFromString($message), $i8p); - $msgLen = $sizeT->constInt(\strlen($message), false); - $emptyFile = $context->builder->pointerCast($context->constantFromString(''), $i8p); - $context->builder->call( - $context->lookupFunction('__compiler_trigger_error'), - $msgPtr, - $msgLen, - $i32->constInt(ErrorReporter::E_NOTICE, false), - $emptyFile, - $i32->constInt(0, false) - ); - } - - private static function discardAssignTarget(Context $context): Variable - { - $slot = JitValueBox::alloc($context); - $var = new Variable($context, Variable::TYPE_VALUE, Variable::KIND_VALUE, $slot); - $var->addref(); - - return $var; + VmArrayAccess::emitIndirectModifyNotice($context, $className); } public static function assignWritableOffset(Context $context, Variable $lvalue, Variable $value): void { - if (null === $lvalue->writableArrayAccessReceiver || null === $lvalue->writableArrayAccessKey) { - throw new \LogicException('ArrayAccess writable offset missing receiver or key'); - } - self::invokeOffsetMethod( - $context, - 'offsetset', - $lvalue->writableArrayAccessReceiver, - $lvalue->writableArrayAccessKey, - $value - ); - } - - private static function canUseArrayAccess( - Context $context, - Variable $container, - ?Operand $containerOp - ): bool { - if (Variable::TYPE_OBJECT !== $container->type) { - return false; - } - $classLc = self::resolveContainerClassLc($container, $containerOp); - if (null !== $classLc && 'object' !== $classLc) { - return in_array( - self::IFACE_LC, - $context->type->object->allInterfacesForClassLc($classLc), - true - ); - } - - return self::hasRuntimeArrayAccessCandidates($context); - } - - private static function hasRuntimeArrayAccessCandidates(Context $context): bool - { - return [] !== self::arrayAccessMethodCandidates($context, 'offsetget'); - } - - private static function writableOffset( - Context $context, - Variable $container, - Variable $dim - ): Variable { - $slot = JitValueBox::alloc($context); - $var = new Variable($context, Variable::TYPE_VALUE, Variable::KIND_VARIABLE, $slot); - $var->writableArrayAccessReceiver = $container; - $var->writableArrayAccessKey = $dim; - $var->isArrayAccessWritableOffset = true; - - return $var; - } - - private static function offsetGet( - Context $context, - Variable $container, - Variable $dim - ): Variable { - $raw = self::invokeOffsetMethod($context, 'offsetget', $container, $dim); - $slot = JitValueBox::alloc($context); - JitValueBox::copyFromPointer( - $context, - $slot, - JitValueBox::normalizeValuePtr($context, $raw) - ); - $var = new Variable($context, Variable::TYPE_VALUE, Variable::KIND_VARIABLE, $slot); - $var->addref(); - - return $var; - } - - private static function invokeOffsetMethod( - Context $context, - string $methodLc, - Variable $receiver, - Variable ...$extraArgs - ): Value { - $candidates = self::arrayAccessMethodCandidates($context, $methodLc); - if ([] === $candidates) { - throw new \LogicException('No JIT lowering for ArrayAccess::'.$methodLc.'()'); - } - $call = new RuntimeIndirectInstanceMethodCall($receiver, $methodLc, $candidates); - return $call->call($context, $receiver, ...$extraArgs); - } - - /** - * @return array - */ - private static function arrayAccessMethodCandidates(Context $context, string $methodLc): array - { - $methodLc = strtolower($methodLc); - $candidates = []; - foreach ($context->type->object->allClassNamesById() as $classId => $className) { - $classLc = strtolower(ltrim($className, '\\')); - if (!in_array(self::IFACE_LC, $context->type->object->allInterfacesForClassLc($classLc), true)) { - continue; - } - $proxyName = $classLc.'::'.$methodLc; - if (!$context->functionIsRegistered($proxyName)) { - continue; - } - $candidates[$classId] = $context->resolveFunctionProxy($proxyName); - } - - return $candidates; - } - - private static function resolveContainerClassLc( - Variable $container, - ?Operand $containerOp - ): ?string { - if (null !== $containerOp && null !== $containerOp->type && Type::TYPE_OBJECT === $containerOp->type->type) { - $userType = $containerOp->type->userType ?? ''; - if ('' !== $userType && 'object' !== strtolower(ltrim($userType, '\\'))) { - return strtolower(ltrim($userType, '\\')); - } - } - - return null; - } - - private static function stringDataPtrFromLiteral(Context $context, string $message): Value - { - return $context->builder->pointerCast( - $context->constantFromString($message), - $context->getTypeFromString('int8*') - ); + VmArrayAccess::assignWritableOffset($context, $lvalue, $value); } } diff --git a/lib/VM/VmArrayAccess.php b/lib/VM/VmArrayAccess.php new file mode 100644 index 00000000000..d07b0f8024d --- /dev/null +++ b/lib/VM/VmArrayAccess.php @@ -0,0 +1,308 @@ +type->object->allInterfacesForClassLc($classLc), + true + ); + } + + public static function tryCompileDimFetch( + Context $context, + JitVariable $container, + JitVariable $dim, + ?Operand $containerOp, + bool $forWrite + ): ?JitVariable { + if ($container->isArrayAccessWritableOffset) { + if ($forWrite) { + $receiver = $container->writableArrayAccessReceiver; + if (null === $receiver) { + throw new \LogicException('ArrayAccess writable offset missing receiver'); + } + self::emitIndirectModifyNotice( + $context, + self::resolveContainerClassLc($receiver, $containerOp) ?? 'ArrayAccess' + ); + + return self::discardAssignTarget($context); + } + + return self::offsetGet($context, $container->writableArrayAccessReceiver, $dim); + } + + if (!self::canUseArrayAccess($context, $container, $containerOp)) { + return null; + } + + if ($forWrite) { + return self::writableOffset($context, $container, $dim); + } + + return self::offsetGet($context, $container, $dim); + } + + public static function tryCompileOffsetIsSet( + Context $context, + JitVariable $container, + JitVariable $dim, + ?Operand $containerOp + ): ?Value { + if (!self::canUseArrayAccess($context, $container, $containerOp)) { + return null; + } + + $raw = self::invokeOffsetMethod($context, 'offsetexists', $container, $dim); + $slot = JitValueBox::alloc($context); + JitValueBox::copyFromPointer( + $context, + $slot, + JitValueBox::normalizeValuePtr($context, $raw) + ); + $boxed = new JitVariable($context, JitVariable::TYPE_VALUE, JitVariable::KIND_VARIABLE, $slot); + $boxed->addref(); + + return (new \PHPCompiler\ext\standard\boolval())->call($context, $boxed); + } + + public static function tryCompileOffsetUnset( + Context $context, + JitVariable $container, + JitVariable $dim, + ?Operand $containerOp + ): bool { + if (!self::canUseArrayAccess($context, $container, $containerOp)) { + return false; + } + self::invokeOffsetMethod($context, 'offsetunset', $container, $dim); + + return true; + } + + public static function isKnownNonArrayAccessObject( + Context $context, + JitVariable $container, + ?Operand $containerOp + ): bool { + if (JitVariable::TYPE_OBJECT !== $container->type) { + return false; + } + $classLc = self::resolveContainerClassLc($container, $containerOp); + if (null === $classLc || 'object' === $classLc) { + return false; + } + + return !in_array( + self::IFACE_LC, + $context->type->object->allInterfacesForClassLc($classLc), + true + ); + } + + public static function emitIllegalOffset(Context $context): void + { + $message = 'Illegal offset'; + $context->builder->call( + $context->lookupFunction('__compiler_jit_raise_logic_exception'), + self::stringDataPtrFromLiteral($context, $message), + $context->constantFromInteger(strlen($message), 'size_t') + ); + } + + public static function emitIndirectModifyNotice(Context $context, string $className): void + { + $message = sprintf( + 'Indirect modification of overloaded element of %s has no effect', + $className + ); + $i8p = $context->getTypeFromString('int8*'); + $sizeT = $context->getTypeFromString('size_t'); + $i32 = $context->getTypeFromString('int32'); + $msgPtr = $context->builder->pointerCast($context->constantFromString($message), $i8p); + $msgLen = $sizeT->constInt(\strlen($message), false); + $emptyFile = $context->builder->pointerCast($context->constantFromString(''), $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_trigger_error'), + $msgPtr, + $msgLen, + $i32->constInt(ErrorReporter::E_NOTICE, false), + $emptyFile, + $i32->constInt(0, false) + ); + } + + public static function assignWritableOffset(Context $context, JitVariable $lvalue, JitVariable $value): void + { + if (null === $lvalue->writableArrayAccessReceiver || null === $lvalue->writableArrayAccessKey) { + throw new \LogicException('ArrayAccess writable offset missing receiver or key'); + } + self::invokeOffsetMethod( + $context, + 'offsetset', + $lvalue->writableArrayAccessReceiver, + $lvalue->writableArrayAccessKey, + $value + ); + } + + private static function discardAssignTarget(Context $context): JitVariable + { + $slot = JitValueBox::alloc($context); + $var = new JitVariable($context, JitVariable::TYPE_VALUE, JitVariable::KIND_VALUE, $slot); + $var->addref(); + + return $var; + } + + private static function canUseArrayAccess( + Context $context, + JitVariable $container, + ?Operand $containerOp + ): bool { + if (JitVariable::TYPE_OBJECT !== $container->type) { + return false; + } + $classLc = self::resolveContainerClassLc($container, $containerOp); + if (null !== $classLc && 'object' !== $classLc) { + return in_array( + self::IFACE_LC, + $context->type->object->allInterfacesForClassLc($classLc), + true + ); + } + + return self::hasRuntimeArrayAccessCandidates($context); + } + + private static function hasRuntimeArrayAccessCandidates(Context $context): bool + { + return [] !== self::arrayAccessMethodCandidates($context, 'offsetget'); + } + + private static function writableOffset( + Context $context, + JitVariable $container, + JitVariable $dim + ): JitVariable { + $slot = JitValueBox::alloc($context); + $var = new JitVariable($context, JitVariable::TYPE_VALUE, JitVariable::KIND_VARIABLE, $slot); + $var->writableArrayAccessReceiver = $container; + $var->writableArrayAccessKey = $dim; + $var->isArrayAccessWritableOffset = true; + + return $var; + } + + private static function offsetGet( + Context $context, + JitVariable $container, + JitVariable $dim + ): JitVariable { + $raw = self::invokeOffsetMethod($context, 'offsetget', $container, $dim); + $slot = JitValueBox::alloc($context); + JitValueBox::copyFromPointer( + $context, + $slot, + JitValueBox::normalizeValuePtr($context, $raw) + ); + $var = new JitVariable($context, JitVariable::TYPE_VALUE, JitVariable::KIND_VARIABLE, $slot); + $var->addref(); + + return $var; + } + + private static function invokeOffsetMethod( + Context $context, + string $methodLc, + JitVariable $receiver, + JitVariable ...$extraArgs + ): Value { + $candidates = self::arrayAccessMethodCandidates($context, $methodLc); + if ([] === $candidates) { + throw new \LogicException('No JIT lowering for ArrayAccess::'.$methodLc.'()'); + } + $call = new RuntimeIndirectInstanceMethodCall($receiver, $methodLc, $candidates); + + return $call->call($context, $receiver, ...$extraArgs); + } + + /** + * @return array + */ + private static function arrayAccessMethodCandidates(Context $context, string $methodLc): array + { + $methodLc = strtolower($methodLc); + $candidates = []; + foreach ($context->type->object->allClassNamesById() as $classId => $className) { + $classLc = strtolower(ltrim($className, '\\')); + if (!in_array(self::IFACE_LC, $context->type->object->allInterfacesForClassLc($classLc), true)) { + continue; + } + $proxyName = $classLc.'::'.$methodLc; + if (!$context->functionIsRegistered($proxyName)) { + continue; + } + $candidates[$classId] = $context->resolveFunctionProxy($proxyName); + } + + return $candidates; + } + + private static function resolveContainerClassLc( + JitVariable $container, + ?Operand $containerOp + ): ?string { + if (null !== $containerOp && null !== $containerOp->type && Type::TYPE_OBJECT === $containerOp->type->type) { + $userType = $containerOp->type->userType ?? ''; + if ('' !== $userType && 'object' !== strtolower(ltrim($userType, '\\'))) { + return strtolower(ltrim($userType, '\\')); + } + } + + return null; + } + + private static function stringDataPtrFromLiteral(Context $context, string $message): Value + { + return $context->builder->pointerCast( + $context->constantFromString($message), + $context->getTypeFromString('int8*') + ); + } +} diff --git a/test/unit/ArrayAccessHelperRuntimeShrinkTest.php b/test/unit/ArrayAccessHelperRuntimeShrinkTest.php new file mode 100644 index 00000000000..39cc4e9abb7 --- /dev/null +++ b/test/unit/ArrayAccessHelperRuntimeShrinkTest.php @@ -0,0 +1,29 @@ +assertStringContainsString('VmArrayAccess', $source); + $this->assertStringNotContainsString('invokeOffsetMethod', $source); + $this->assertStringNotContainsString('arrayAccessMethodCandidates', $source); + $this->assertLessThanOrEqual(85, substr_count($source, "\n") + 1); + } + + public function testVmArrayAccessOwnsOffsetLowering(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/VM/VmArrayAccess.php'); + $this->assertStringContainsString('invokeOffsetMethod', $source); + $this->assertStringContainsString('arrayAccessMethodCandidates', $source); + $this->assertStringContainsString('writableArrayAccessReceiver', $source); + $this->assertGreaterThan(200, substr_count($source, "\n") + 1); + } +}