From 7633947d4f00a7b65cb43dea965c7f5837965ee9 Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 6 Aug 2026 09:14:40 +0000 Subject: [PATCH] Fix AOT array_first/array_last via HashTableElemLlvm thin path (#27596) NestedJIT ArrayElemJitHelper returns wrong values under thin standalone AOT; route user-script builds through pure LLVM (peer HashTableShiftLlvm #24025) while JIT/MCJIT keep the php-in-PHP helper bridge. Co-authored-by: Cursor --- lib/JIT/Builtin/ArrayElemRuntime.php | 16 +- lib/JIT/HashTableElemLlvm.php | 183 +++++++++++++++++++++++ lib/JIT/JitVmHelperLink.php | 22 ++- test/unit/ArrayElemRuntimeShrinkTest.php | 9 ++ 4 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 lib/JIT/HashTableElemLlvm.php diff --git a/lib/JIT/Builtin/ArrayElemRuntime.php b/lib/JIT/Builtin/ArrayElemRuntime.php index 82d67bcc242..0bf3ed28da1 100644 --- a/lib/JIT/Builtin/ArrayElemRuntime.php +++ b/lib/JIT/Builtin/ArrayElemRuntime.php @@ -5,7 +5,9 @@ namespace PHPCompiler\JIT\Builtin; use PHPCompiler\JIT\ArrayBuiltinHelper; +use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\HashTableElemLlvm; use PHPCompiler\JIT\JitVmHelperLink; use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; @@ -13,7 +15,9 @@ /** * JIT/AOT link for array_first()/array_last() via ArrayElemJitHelper PHP (#15063). * - * Replaces ~150 LOC inline LLVM in ext/standard/JitArrayElem.php. + * Thin standalone AOT uses pure LLVM ({@see HashTableElemLlvm}) — NestedJIT Variable + * returns from ArrayElemJitHelper are broken under thin AOT (#27596, peer #24025). + * * SSOT: {@see \PHPCompiler\ext\standard\VmArray}. * php-src: ext/standard/array.c — php_array_first, php_array_last */ @@ -37,8 +41,11 @@ final class ArrayElemRuntime public static function first(Context $context, JITVariable $array): Value { - self::ensureLinked($context); $ht = ArrayBuiltinHelper::loadHashTable($context, $array); + if ($context->isThinStandaloneAotMain()) { + return HashTableElemLlvm::valueFirst($context, $ht); + } + self::ensureLinked($context); return $context->builder->call( $context->lookupFunction(self::ABI_FIRST), @@ -48,8 +55,11 @@ public static function first(Context $context, JITVariable $array): Value public static function last(Context $context, JITVariable $array): Value { - self::ensureLinked($context); $ht = ArrayBuiltinHelper::loadHashTable($context, $array); + if ($context->isThinStandaloneAotMain()) { + return HashTableElemLlvm::valueLast($context, $ht); + } + self::ensureLinked($context); return $context->builder->call( $context->lookupFunction(self::ABI_LAST), diff --git a/lib/JIT/HashTableElemLlvm.php b/lib/JIT/HashTableElemLlvm.php new file mode 100644 index 00000000000..3d593787b35 --- /dev/null +++ b/lib/JIT/HashTableElemLlvm.php @@ -0,0 +1,183 @@ +structFieldMap['__hashtable__']; + $nodeMap = $context->structFieldMap['__strkey_node__']; + $sizeT = $context->getTypeFromString('size_t'); + $nodePtrType = $context->getTypeFromString('__strkey_node__*'); + $zero = $sizeT->constInt(0, false); + $one = $sizeT->constInt(1, false); + $num = $context->builder->call( + $context->lookupFunction('__hashtable__getNumElements'), + $ht + ); + $isEmpty = $context->builder->icmp(Builder::INT_EQ, $num, $zero); + + $emptyBb = BasicBlockHelper::append($context, 'array_elem_'.($first ? 'first' : 'last').'_empty'); + $workBb = BasicBlockHelper::append($context, 'array_elem_'.($first ? 'first' : 'last').'_work'); + $doneBb = BasicBlockHelper::append($context, 'array_elem_'.($first ? 'first' : 'last').'_done'); + $context->builder->branchIf($isEmpty, $emptyBb, $workBb); + + $context->builder->positionAtEnd($emptyBb); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + $resultPtr + ); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($workBb); + $nextFree = $context->builder->load( + $context->builder->structGep($ht, $map['nextFreeElement']) + ); + $hasPacked = $context->builder->icmp(Builder::INT_NE, $nextFree, $zero); + $packedBb = BasicBlockHelper::append($context, 'array_elem_'.($first ? 'first' : 'last').'_packed'); + $stringBb = BasicBlockHelper::append($context, 'array_elem_'.($first ? 'first' : 'last').'_string'); + $context->builder->branchIf($hasPacked, $packedBb, $stringBb); + + $tag = $first ? 'first' : 'last'; + $context->builder->positionAtEnd($packedBb); + $idxSlot = $context->builder->alloca($sizeT, 1, 'array_elem_'.$tag.'_idx'); + if ($first) { + $context->builder->store($zero, $idxSlot); + } else { + $context->builder->store($context->builder->sub($nextFree, $one), $idxSlot); + } + $loopHead = BasicBlockHelper::append($context, 'array_elem_'.$tag.'_head'); + $loopBody = BasicBlockHelper::append($context, 'array_elem_'.$tag.'_body'); + $loopFound = BasicBlockHelper::append($context, 'array_elem_'.$tag.'_found'); + $loopNext = BasicBlockHelper::append($context, 'array_elem_'.$tag.'_next'); + $loopFail = BasicBlockHelper::append($context, 'array_elem_'.$tag.'_fail'); + $context->builder->branch($loopHead); + + $context->builder->positionAtEnd($loopHead); + $idx = $context->builder->load($idxSlot); + if ($first) { + $atEnd = $context->builder->icmp(Builder::INT_SGE, $idx, $nextFree); + $context->builder->branchIf($atEnd, $loopFail, $loopBody); + } else { + $atStart = $context->builder->icmp(Builder::INT_EQ, $idx, $zero); + $context->builder->branchIf($atStart, $loopFail, $loopBody); + } + + $context->builder->positionAtEnd($loopBody); + $present = $context->builder->call( + $context->lookupFunction('__hashtable__offsetIsSet'), + $ht, + $idx + ); + $context->builder->branchIf($present, $loopFound, $loopNext); + + $context->builder->positionAtEnd($loopFound); + $entryPtr = HashTableHelper::listEntryPointer($context, $ht, $idx); + JitValueBox::copyFromPointer($context, $resultSlot, $entryPtr); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($loopNext); + if ($first) { + $context->builder->store($context->builder->addNoSignedWrap($idx, $one), $idxSlot); + } else { + $context->builder->store($context->builder->sub($idx, $one), $idxSlot); + } + $context->builder->branch($loopHead); + + $context->builder->positionAtEnd($loopFail); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + $resultPtr + ); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($stringBb); + $head = $context->builder->load($context->builder->structGep($ht, $map['strKeys'])); + if ($first) { + $headNull = $context->builder->icmp(Builder::INT_EQ, $head, $nodePtrType->constNull()); + $strEmpty = BasicBlockHelper::append($context, 'array_elem_first_str_empty'); + $strFound = BasicBlockHelper::append($context, 'array_elem_first_str_found'); + $context->builder->branchIf($headNull, $strEmpty, $strFound); + $context->builder->positionAtEnd($strEmpty); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + $resultPtr + ); + $context->builder->branch($doneBb); + $context->builder->positionAtEnd($strFound); + $valEntry = $context->builder->structGep($head, $nodeMap['value']); + JitValueBox::copyFromPointer($context, $resultSlot, $valEntry); + $context->builder->branch($doneBb); + } else { + $walkSlot = $context->builder->alloca($nodePtrType, 1, 'array_elem_last_walk'); + $lastSlot = $context->builder->alloca($nodePtrType, 1, 'array_elem_last_node'); + $context->builder->store($head, $walkSlot); + $context->builder->store($nodePtrType->constNull(), $lastSlot); + $walkHead = BasicBlockHelper::append($context, 'array_elem_last_walk_head'); + $walkBody = BasicBlockHelper::append($context, 'array_elem_last_walk_body'); + $walkDone = BasicBlockHelper::append($context, 'array_elem_last_walk_done'); + $context->builder->branch($walkHead); + + $context->builder->positionAtEnd($walkHead); + $walkNode = $context->builder->load($walkSlot); + $walkEnd = $context->builder->icmp(Builder::INT_EQ, $walkNode, $nodePtrType->constNull()); + $context->builder->branchIf($walkEnd, $walkDone, $walkBody); + + $context->builder->positionAtEnd($walkBody); + $context->builder->store($walkNode, $lastSlot); + $nextWalk = $context->builder->load($context->builder->structGep($walkNode, $nodeMap['next'])); + $context->builder->store($nextWalk, $walkSlot); + $context->builder->branch($walkHead); + + $context->builder->positionAtEnd($walkDone); + $lastNode = $context->builder->load($lastSlot); + $lastNull = $context->builder->icmp(Builder::INT_EQ, $lastNode, $nodePtrType->constNull()); + $strEmpty = BasicBlockHelper::append($context, 'array_elem_last_str_empty'); + $strFound = BasicBlockHelper::append($context, 'array_elem_last_str_found'); + $context->builder->branchIf($lastNull, $strEmpty, $strFound); + $context->builder->positionAtEnd($strEmpty); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + $resultPtr + ); + $context->builder->branch($doneBb); + $context->builder->positionAtEnd($strFound); + $valEntry = $context->builder->structGep($lastNode, $nodeMap['value']); + JitValueBox::copyFromPointer($context, $resultSlot, $valEntry); + $context->builder->branch($doneBb); + } + + $context->builder->positionAtEnd($doneBb); + + return $resultPtr; + } +} diff --git a/lib/JIT/JitVmHelperLink.php b/lib/JIT/JitVmHelperLink.php index b9f283bea08..77ddfde2360 100644 --- a/lib/JIT/JitVmHelperLink.php +++ b/lib/JIT/JitVmHelperLink.php @@ -19,9 +19,16 @@ public static function ensureCompiled( Context $context, string $relativeHelperPath, array $compiledHelpers, - string $compileLabel + string $compileLabel, + bool $skipHelperRuntimeCache = false ): void { - self::ensureCompiledBundle($context, [$relativeHelperPath], $compiledHelpers, $compileLabel); + self::ensureCompiledBundle( + $context, + [$relativeHelperPath], + $compiledHelpers, + $compileLabel, + $skipHelperRuntimeCache + ); } /** @@ -232,7 +239,8 @@ public static function ensureBridge( string $helperLogical, string $relativeHelperPath, array $compiledHelpers, - string $issueTag + string $issueTag, + bool $skipHelperRuntimeCache = false ): void { $probe = $context->module->getNamedFunction($abiName); if (self::hasNamedBridgeEntry($probe, $entryBlockName)) { @@ -247,7 +255,13 @@ public static function ensureBridge( } catch (\Throwable) { } - self::ensureCompiled($context, $relativeHelperPath, $compiledHelpers, $issueTag); + self::ensureCompiled( + $context, + $relativeHelperPath, + $compiledHelpers, + $issueTag, + $skipHelperRuntimeCache + ); $helperFn = self::lookupCompiled($context, $helperLogical, $issueTag); $ft = $context->context->functionType($returnType, false, ...$paramTypes); diff --git a/test/unit/ArrayElemRuntimeShrinkTest.php b/test/unit/ArrayElemRuntimeShrinkTest.php index 4c7c10ee9f7..817f2e4d8d0 100644 --- a/test/unit/ArrayElemRuntimeShrinkTest.php +++ b/test/unit/ArrayElemRuntimeShrinkTest.php @@ -59,4 +59,13 @@ public function testSpineBundleIncludesArrayElemJitHelper(): void $this->assertStringContainsString('ArrayElemJitHelper.php', $spine); $this->assertStringContainsString('ArrayElemRuntime.php', $spine); } + + public function testThinAotRoutesThroughHashTableElemLlvm(): void + { + $runtime = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/ArrayElemRuntime.php'); + $this->assertStringContainsString('HashTableElemLlvm::valueFirst', $runtime); + $this->assertStringContainsString('isThinStandaloneAotMain', $runtime); + $llvm = (string) file_get_contents(__DIR__.'/../../lib/JIT/HashTableElemLlvm.php'); + $this->assertStringContainsString('#27596', $llvm); + } }