From c895ddb34364b72a4a0cda55b99a6b1de4d051fd Mon Sep 17 00:00:00 2001 From: PurHur Date: Wed, 20 May 2026 17:10:59 +0000 Subject: [PATCH] Fix AOT hashtable index reads for string elements and add array_fill PHPT. Packed-list elements stored as strings were fetched via readLongAt, so echo printed zeros. Read the slot type and box the value for mixed-type index access. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 20 ++-- docs/capabilities.md | 2 +- lib/JIT.php | 14 ++- lib/JIT/HashTableHelper.php | 71 ++++++++++++++- lib/JIT/JitValueBox.php | 116 ++++++++++++++++++++++++ lib/JIT/Variable.php | 13 +-- test/fixtures/aot/cases/array_fill.phpt | 15 +++ 7 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/aot/cases/array_fill.phpt diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index a43a3526..fef801e3 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -223,7 +223,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/JIT/Call/Vararg.php` | 0 | 2 | | `lib/JIT/CoalesceHelper.php` | 0 | 1 | | `lib/JIT/Context.php` | 0 | 15 | -| `lib/JIT/HashTableHelper.php` | 0 | 1 | +| `lib/JIT/HashTableHelper.php` | 0 | 2 | | `lib/JIT/Helper.php` | 0 | 7 | | `lib/JIT/IssetHelper.php` | 0 | 1 | | `lib/JIT/JitNativeString.php` | 0 | 5 | @@ -235,7 +235,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/JIT/StringOffsetHelper.php` | 0 | 1 | | `lib/JIT/SuperglobalInit.php` | 0 | 3 | | `lib/JIT/ValueEchoHelper.php` | 0 | 1 | -| `lib/JIT/Variable.php` | 0 | 18 | +| `lib/JIT/Variable.php` | 0 | 17 | | `lib/Lint/IncrementDetector.php` | 0 | 4 | | `lib/Lint/Issue.php` | 0 | 2 | | `lib/Lint/LintCompiler.php` | 0 | 6 | @@ -1301,10 +1301,10 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new JIT\Call\Native (line 149) - new ext\standard\boolval (line 268) - new Variable (line 432) -- new Variable (line 813) -- new Operand\Literal (line 883) -- new Operand\Literal (line 887) -- new Operand\Literal (line 891) +- new Variable (line 819) +- new Operand\Literal (line 889) +- new Operand\Literal (line 893) +- new Operand\Literal (line 897) - 12 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/Analyzer.php` @@ -1513,7 +1513,8 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/JIT/HashTableHelper.php` **Warnings** (review for bootstrap subset): -- 9 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new Variable (line 201) +- 11 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/Helper.php` @@ -1543,7 +1544,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/JIT/JitValueBox.php` **Warnings** (review for bootstrap subset): -- 4 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- 5 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/JitValueCompare.php` @@ -1602,8 +1603,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Variable (line 413) - new Variable (line 430) - new Variable (line 450) -- new Variable (line 463) -- new Variable (line 475) +- new Variable (line 464) - 15 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/Lint/IncrementDetector.php` diff --git a/docs/capabilities.md b/docs/capabilities.md index 36cce544..abafbd24 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -6,7 +6,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. |----------|:--:|:---:|:---:|--------|-------| | `abs` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `array_combine` | yes | no | no | standard | doc: VM only; not implemented for JIT in this compiler build | -| `array_fill` | yes | yes | yes | standard | JIT PHPT | +| `array_fill` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `array_flip` | yes | no | no | standard | doc: VM only; not implemented for JIT in this compiler build | | `array_key_exists` | yes | yes | yes | standard | AOT PHPT | | `array_keys` | yes | yes | yes | standard | doc: VM only | diff --git a/lib/JIT.php b/lib/JIT.php index 27f524e6..204a7ab6 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -757,8 +757,11 @@ private function assignOperand(Operand $result, Variable $value): void { return; case Variable::TYPE_VALUE: - $loaded = $this->context->builder->load($this->context->helper->loadValue($value)); - $this->context->builder->store($loaded, $result->value); + JIT\JitValueBox::copyFromPointer( + $this->context, + $valueRef, + $this->context->helper->loadValue($value) + ); return; default: @@ -789,8 +792,11 @@ private function assignOperand(Operand $result, Variable $value): void { return; } elseif (Variable::TYPE_VALUE === $result->type && Variable::TYPE_VALUE === $value->type) { - $loaded = $this->context->builder->load($this->context->helper->loadValue($value)); - $this->context->builder->store($loaded, $result->value); + JIT\JitValueBox::copyFromPointer( + $this->context, + $result->value, + $this->context->helper->loadValue($value) + ); return; } diff --git a/lib/JIT/HashTableHelper.php b/lib/JIT/HashTableHelper.php index 6af86dc6..f8e32a04 100644 --- a/lib/JIT/HashTableHelper.php +++ b/lib/JIT/HashTableHelper.php @@ -125,13 +125,19 @@ public static function buildArrayFill( return $ht; } - public static function readStringAt(Context $context, Value $ht, Value $index): Value + public static function listEntryPointer(Context $context, Value $ht, Value $index): Value { $map = $context->structFieldMap['__hashtable__']; $values = $context->builder->load( $context->builder->structGep($ht, $map['values']) ); - $entry = $context->builder->inBoundsGep($values, $index); + + return $context->builder->inBoundsGep($values, $index); + } + + public static function readStringAt(Context $context, Value $ht, Value $index): Value + { + $entry = self::listEntryPointer($context, $ht, $index); return $context->builder->call( $context->lookupFunction('__value__readString'), @@ -139,6 +145,67 @@ public static function readStringAt(Context $context, Value $ht, Value $index): ); } + /** + * Read a packed-list element into a stack {@see __value__} slot (for echo / mixed-type index). + */ + public static function readIndexedToValueBox(Context $context, Value $ht, Value $index): Variable + { + static $seq = 0; + $tag = 'rb'.(string) ++$seq; + $slot = JitValueBox::alloc($context); + $destPtr = JitValueBox::pointer($context, $slot); + $entryPtr = self::listEntryPointer($context, $ht, $index); + $valueMap = $context->structFieldMap['__value__']; + $typeByte = $context->builder->load( + $context->builder->structGep($entryPtr, $valueMap['type']) + ); + $i8 = $context->getTypeFromString('int8'); + + $stringBlock = BasicBlockHelper::append($context, 'ht_rb_string_'.$tag); + $longBlock = BasicBlockHelper::append($context, 'ht_rb_long_'.$tag); + $done = BasicBlockHelper::append($context, 'ht_rb_done_'.$tag); + + $isString = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_STRING, false) + ); + $context->builder->branchIf($isString, $stringBlock, $longBlock); + + $context->builder->positionAtEnd($stringBlock); + $str = $context->builder->call( + $context->lookupFunction('__value__readString'), + $entryPtr + ); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $destPtr, + $owned + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($longBlock); + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + $destPtr, + $context->builder->call($context->lookupFunction('__value__readLong'), $entryPtr) + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($done); + + return new Variable( + $context, + Variable::TYPE_VALUE, + Variable::KIND_VARIABLE, + $slot + ); + } + public static function initArray(Context $context, Variable $result): void { $result->nextFreeElement = 0; diff --git a/lib/JIT/JitValueBox.php b/lib/JIT/JitValueBox.php index 62d42b4c..314e948d 100644 --- a/lib/JIT/JitValueBox.php +++ b/lib/JIT/JitValueBox.php @@ -13,6 +13,8 @@ final class JitValueBox { + private static int $copySeq = 0; + public static function alloc(Context $context): Value { return $context->builder->alloca($context->getTypeFromString('__value__')); @@ -35,6 +37,120 @@ public static function writeLong(Context $context, Value $slot, Value $long): vo ); } + /** + * Copy a boxed value from a {@see __value__*} slot into a stack {@see __value__} alloca. + */ + public static function copyFromPointer(Context $context, Value $destSlot, Value $srcPtr): void + { + $map = $context->structFieldMap['__value__']; + $typeByte = $context->builder->load( + $context->builder->structGep($srcPtr, $map['type']) + ); + $i8 = $context->getTypeFromString('int8'); + $destPtr = self::pointer($context, $destSlot); + + $tag = 'v'.(string) self::$copySeq++; + $stringBlock = BasicBlockHelper::append($context, 'value_copy_string_'.$tag); + $longBlock = BasicBlockHelper::append($context, 'value_copy_long_'.$tag); + $doubleBlock = BasicBlockHelper::append($context, 'value_copy_double_'.$tag); + $boolBlock = BasicBlockHelper::append($context, 'value_copy_bool_'.$tag); + $nullBlock = BasicBlockHelper::append($context, 'value_copy_null_'.$tag); + $done = BasicBlockHelper::append($context, 'value_copy_done_'.$tag); + + $isString = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_STRING, false) + ); + $isLong = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NATIVE_LONG, false) + ); + $isBool = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NATIVE_BOOL, false) + ); + $isDouble = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NATIVE_DOUBLE, false) + ); + $isNull = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NULL, false) + ); + + $afterString = BasicBlockHelper::append($context, 'value_copy_after_string_'.$tag); + $context->builder->branchIf($isString, $stringBlock, $afterString); + + $context->builder->positionAtEnd($stringBlock); + $str = $context->builder->call( + $context->lookupFunction('__value__readString'), + $srcPtr + ); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $destPtr, + $owned + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($afterString); + $afterLong = BasicBlockHelper::append($context, 'value_copy_after_long_'.$tag); + $context->builder->branchIf($isLong, $longBlock, $afterLong); + + $context->builder->positionAtEnd($longBlock); + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + $destPtr, + $context->builder->call($context->lookupFunction('__value__readLong'), $srcPtr) + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($afterLong); + $afterBool = BasicBlockHelper::append($context, 'value_copy_after_bool_'.$tag); + $context->builder->branchIf($isBool, $boolBlock, $afterBool); + + $context->builder->positionAtEnd($boolBlock); + self::writeBool( + $context, + $destSlot, + $context->builder->truncOrBitCast( + $context->builder->call($context->lookupFunction('__value__readLong'), $srcPtr), + $context->getTypeFromString('int1') + ) + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($afterBool); + $afterDouble = BasicBlockHelper::append($context, 'value_copy_after_double_'.$tag); + $context->builder->branchIf($isDouble, $doubleBlock, $afterDouble); + + $context->builder->positionAtEnd($doubleBlock); + $context->builder->call( + $context->lookupFunction('__value__writeDouble'), + $destPtr, + $context->builder->call($context->lookupFunction('__value__readDouble'), $srcPtr) + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($afterDouble); + $context->builder->branchIf($isNull, $nullBlock, $done); + + $context->builder->positionAtEnd($nullBlock); + $context->builder->call($context->lookupFunction('__value__writeNull'), $destPtr); + $context->builder->branch($done); + + $context->builder->positionAtEnd($done); + } + public static function writeBool(Context $context, Value $slot, Value $bool): void { $map = $context->structFieldMap['__value__']; diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index 77760119..0136b920 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -454,18 +454,7 @@ public function dimFetch(self $dim, ?Type $expectedType = null): Variable { $str ); } - $long = $this->context->builder->call( - $this->context->lookupFunction('__hashtable__readLongAt'), - $ht, - $index - ); - - return new Variable( - $this->context, - self::TYPE_NATIVE_LONG, - self::KIND_VALUE, - $long - ); + return HashTableHelper::readIndexedToValueBox($this->context, $ht, $index); default: if (!$this->type & self::IS_NATIVE_ARRAY) { throw new \LogicException("Unsupported dim fetch on " . self::getStringType($this->type)); diff --git a/test/fixtures/aot/cases/array_fill.phpt b/test/fixtures/aot/cases/array_fill.phpt new file mode 100644 index 00000000..1524260e --- /dev/null +++ b/test/fixtures/aot/cases/array_fill.phpt @@ -0,0 +1,15 @@ +--TEST-- +AOT: array_fill() with string values (packed list) +--FILE-- +