diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 9a552b2d7ee..8acc192b51b 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 8588 | -| Phase A inventory files (M2 ratio SSOT) | 8588 | +| PHP files on vm.php path | 8592 | +| Phase A inventory files (M2 ratio SSOT) | 8592 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 26775 | +| Source constructs flagged (warnings) | 26785 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -6722,6 +6722,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Builtin/WeakRefRegistryRuntime.php` | 0 | 2 | | `lib/JIT/Builtin/WeakRefRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/WeakRefSetup.php` | 0 | 1 | +| `lib/JIT/Builtin/ZendCompareLlvm.php` | 0 | 4 | | `lib/JIT/Builtin/ZendDoubleStringRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/ZipArchiveEmbedBridge.php` | 0 | 1 | | `lib/JIT/Builtin/ZlibRuntime.php` | 0 | 1 | @@ -7437,6 +7438,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/InstanceOfHelper.php` | 0 | 6 | | `lib/JIT/InstancePropertyVisibilityJitGuard.php` | 0 | 2 | | `lib/JIT/InstantiableClassJitGuard.php` | 0 | 1 | +| `lib/JIT/IntRadixSortLlvm.php` | 0 | 2 | | `lib/JIT/InternalStrictArg.php` | 0 | 1 | | `lib/JIT/IntersectionParamCheck.php` | 0 | 3 | | `lib/JIT/IntlExtensionHooks.php` | 0 | 1 | @@ -7504,6 +7506,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/MbConvertVariablesLlvm.php` | 0 | 2 | | `lib/JIT/McjitEmbedHostEcho.php` | 0 | 2 | | `lib/JIT/McjitEmbedRuntime.php` | 0 | 1 | +| `lib/JIT/MergeSortLlvm.php` | 0 | 1 | | `lib/JIT/MultipleIteratorZipLlvm.php` | 0 | 4 | | `lib/JIT/NamedArgs.php` | 0 | 11 | | `lib/JIT/NamedOptionalCallArgs.php` | 0 | 2 | @@ -7628,6 +7631,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/XmlWriterInstanceMethodJit.php` | 0 | 5 | | `lib/JIT/XslExtensionHooks.php` | 0 | 1 | | `lib/JIT/XsltInstanceMethodJit.php` | 0 | 2 | +| `lib/JIT/ZendSortLlvm.php` | 0 | 3 | | `lib/JIT/ZipExtensionHooks.php` | 0 | 1 | | `lib/JitMcjitEmbed.php` | 0 | 1 | | `lib/JitVmLoweringPolicy.php` | 0 | 1 | @@ -56263,8 +56267,8 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/Type/HashTable.php` **Warnings** (review for bootstrap subset): -- 77 class method(s) -- 2 closure(s) +- 81 class method(s) +- 14 closure(s) ### `lib/JIT/Builtin/Type/HashTableDtorLlvm.php` @@ -56541,6 +56545,14 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 4 class method(s) +### `lib/JIT/Builtin/ZendCompareLlvm.php` + +**Warnings** (review for bootstrap subset): +- new self (line 65) +- new self (line 84) +- 26 class method(s) +- 17 closure(s) + ### `lib/JIT/Builtin/ZendDoubleStringRuntime.php` **Warnings** (review for bootstrap subset): @@ -60526,8 +60538,8 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - trait ContextModuleOptimizationPasses (line 25) -- 7 class method(s) -- 1 closure(s) +- 9 class method(s) +- 3 closure(s) ### `lib/JIT/ContextModuleVerify.php` @@ -61314,6 +61326,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 3 class method(s) +### `lib/JIT/IntRadixSortLlvm.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) +- 8 closure(s) + ### `lib/JIT/InternalStrictArg.php` **Warnings** (review for bootstrap subset): @@ -61757,6 +61775,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 5 class method(s) +### `lib/JIT/MergeSortLlvm.php` + +**Warnings** (review for bootstrap subset): +- 3 class method(s) + ### `lib/JIT/MultipleIteratorZipLlvm.php` **Warnings** (review for bootstrap subset): @@ -62603,6 +62626,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Call\XsltMethod (line 45) - 3 class method(s) +### `lib/JIT/ZendSortLlvm.php` + +**Warnings** (review for bootstrap subset): +- new self (line 57) +- 17 class method(s) +- 3 closure(s) + ### `lib/JIT/ZipExtensionHooks.php` **Warnings** (review for bootstrap subset): diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index 2615c3292c8..7a9ffedba85 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -6809,6 +6809,7 @@ "lib/JIT/Builtin/WeakRefRegistryRuntime.php", "lib/JIT/Builtin/WeakRefRuntime.php", "lib/JIT/Builtin/WeakRefSetup.php", + "lib/JIT/Builtin/ZendCompareLlvm.php", "lib/JIT/Builtin/ZendDoubleStringRuntime.php", "lib/JIT/Builtin/ZipArchiveEmbedBridge.php", "lib/JIT/Builtin/ZlibRuntime.php", @@ -7528,6 +7529,7 @@ "lib/JIT/InstanceOfHelper.php", "lib/JIT/InstancePropertyVisibilityJitGuard.php", "lib/JIT/InstantiableClassJitGuard.php", + "lib/JIT/IntRadixSortLlvm.php", "lib/JIT/InternalStrictArg.php", "lib/JIT/IntersectionParamCheck.php", "lib/JIT/IntlExtensionHooks.php", @@ -7595,6 +7597,7 @@ "lib/JIT/MbConvertVariablesLlvm.php", "lib/JIT/McjitEmbedHostEcho.php", "lib/JIT/McjitEmbedRuntime.php", + "lib/JIT/MergeSortLlvm.php", "lib/JIT/MultipleIteratorZipLlvm.php", "lib/JIT/NamedArgs.php", "lib/JIT/NamedOptionalCallArgs.php", @@ -7720,6 +7723,7 @@ "lib/JIT/XmlWriterInstanceMethodJit.php", "lib/JIT/XslExtensionHooks.php", "lib/JIT/XsltInstanceMethodJit.php", + "lib/JIT/ZendSortLlvm.php", "lib/JIT/ZipExtensionHooks.php", "lib/JitMcjitEmbed.php", "lib/JitVmLoweringPolicy.php", @@ -8822,9 +8826,9 @@ "test/bootstrap-aot/lib_opcode/main.php" ], "totals": { - "inventory_files": 8588, + "inventory_files": 8592, "excluded": 0, - "eligible": 8588, + "eligible": 8592, "aot_lint_targets": 113, "aot_link_targets": 100, "aot_link_lib_targets": 1 diff --git a/lib/JIT/Builtin/NumericStringLlvm.php b/lib/JIT/Builtin/NumericStringLlvm.php index d0ee69e5360..169618cdb8f 100644 --- a/lib/JIT/Builtin/NumericStringLlvm.php +++ b/lib/JIT/Builtin/NumericStringLlvm.php @@ -19,8 +19,10 @@ * IS_LONG, everything else numeric IS_DOUBLE (zend_strtod — libc strtod here, same * result for the decimal forms this accepts). * - * Returns {@see self::NUMERIC} (whole string numeric), {@see self::LEADING} (numeric - * prefix followed by garbage — callers emit "A non-numeric value encountered") or + * Returns {@see self::NUMERIC} (whole string numeric), {@see self::NUMERIC_OFLOW_POS} / + * {@see self::NUMERIC_OFLOW_NEG} (whole string numeric, integer-shaped, overflowed + * zend_long — php-src's `oflow` out-param), {@see self::LEADING} (numeric prefix followed + * by garbage — callers emit "A non-numeric value encountered") or * {@see self::NON_NUMERIC} (out untouched — callers throw the operator's TypeError). */ final class NumericStringLlvm @@ -33,6 +35,12 @@ final class NumericStringLlvm public const NON_NUMERIC = 2; + /** Whole string numeric, integer-shaped but beyond ZEND_LONG_MAX (out is IS_DOUBLE). */ + public const NUMERIC_OFLOW_POS = 3; + + /** Whole string numeric, integer-shaped but below ZEND_LONG_MIN (out is IS_DOUBLE). */ + public const NUMERIC_OFLOW_NEG = 4; + /** @return Value i32 status */ public static function call(Context $context, Value $str, Value $out): Value { @@ -80,6 +88,21 @@ private static function ensure(Context $context): Value\Function_ $len = $b->load($b->structGep($str, $map['length'])); $pos = $b->alloca($i64); $b->store($zero, $pos); + // php-src's fast reject: every byte that can start a numeric string (whitespace, + // sign, '.', digit) is <= '9'. + $scan = $fn->appendBasicBlock('scan'); + $reject = $fn->appendBasicBlock('fast_reject'); + $b->branchIf( + $b->bitwiseAnd( + $b->icmp(Builder::INT_UGT, $len, $zero), + $b->icmp(Builder::INT_UGT, $b->load($data), $ch('9')) + ), + $reject, + $scan + ); + $b->positionAtEnd($reject); + $b->returnValue($i32->constInt(self::NON_NUMERIC, false)); + $b->positionAtEnd($scan); // byte at index (caller guarantees idx < len) $at = static fn (Value $idx): Value => $b->load($b->gep($data, $idx)); @@ -220,6 +243,12 @@ private static function ensure(Context $context): Value\Function_ // Accumulate negatively so ZEND_LONG_MIN parses; overflow → double (zend_strtod). $b->positionAtEnd($longPath); + $negative = $b->icmp( + Builder::INT_EQ, + $b->select($b->icmp(Builder::INT_UGT, $digitsStart, $start), $at($start), $ch('+')), + $ch('-') + ); + $oflowPath = $fn->appendBasicBlock('oflow_path'); $accSlot = $b->alloca($i64); $idxSlot = $b->alloca($i64); $b->store($zero, $accSlot); @@ -236,10 +265,10 @@ private static function ensure(Context $context): Value\Function_ $b->positionAtEnd($accBody); $digit = $b->zExt($b->sub($at($idx), $ch('0')), $i64); $mul = $b->call($smul, $b->load($accSlot), $i64->constInt(10, false)); - $b->branchIf($b->extractValue($mul, 1), $doublePath, $accMulOk); + $b->branchIf($b->extractValue($mul, 1), $oflowPath, $accMulOk); $b->positionAtEnd($accMulOk); $sub = $b->call($ssub, $b->extractValue($mul, 0), $digit); - $b->branchIf($b->extractValue($sub, 1), $doublePath, $accStep); + $b->branchIf($b->extractValue($sub, 1), $oflowPath, $accStep); $b->positionAtEnd($accStep); $b->store($b->extractValue($sub, 0), $accSlot); $b->store($b->add($idx, $one), $idxSlot); @@ -247,11 +276,6 @@ private static function ensure(Context $context): Value\Function_ $b->positionAtEnd($accDone); $acc = $b->load($accSlot); - $negative = $b->icmp( - Builder::INT_EQ, - $b->select($b->icmp(Builder::INT_UGT, $digitsStart, $start), $at($start), $ch('+')), - $ch('-') - ); $negWrite = $fn->appendBasicBlock('neg_write'); $posCheck = $fn->appendBasicBlock('pos_check'); $posWrite = $fn->appendBasicBlock('pos_write'); @@ -261,11 +285,21 @@ private static function ensure(Context $context): Value\Function_ $b->returnValue($status); $b->positionAtEnd($posCheck); $minLong = $i64->constInt(\PHP_INT_MIN, true); - $b->branchIf($b->icmp(Builder::INT_EQ, $acc, $minLong), $doublePath, $posWrite); + $b->branchIf($b->icmp(Builder::INT_EQ, $acc, $minLong), $oflowPath, $posWrite); $b->positionAtEnd($posWrite); $b->call($writeLong, $out, $b->sub($zero, $acc)); $b->returnValue($status); + // Integer-shaped but out of zend_long range: double value + php-src's oflow sign. + $b->positionAtEnd($oflowPath); + $od = $b->call($strtod, $b->gep($data, $start), $context->getTypeFromString('int8**')->constNull()); + $b->call($writeDouble, $out, $od); + $b->returnValue($b->select( + $b->icmp(Builder::INT_EQ, $status, $i32->constInt(self::NUMERIC, false)), + $b->select($negative, $i32->constInt(self::NUMERIC_OFLOW_NEG, false), $i32->constInt(self::NUMERIC_OFLOW_POS, false)), + $status + )); + // strtod stops at the first byte it cannot use, which is at or before numEnd for // every form accepted above (no hex/inf/nan can reach here). $b->positionAtEnd($doublePath); diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php index 6b944bb779a..bdd33d42338 100644 --- a/lib/JIT/Builtin/Type/HashTable.php +++ b/lib/JIT/Builtin/Type/HashTable.php @@ -22,6 +22,8 @@ use PHPCompiler\JIT\Variable; use PHPLLVM; use PHPLLVM\Builder; +use PHPCompiler\JIT\ZendSortLlvm; +use PHPCompiler\JIT\MergeSortLlvm; class HashTable extends Type { @@ -3360,123 +3362,322 @@ private function implementSortPacked(bool $reverse, bool $caseInsensitive = fals $this->context->builder->branchIf($tooSmall, $done, $work); $this->context->builder->positionAtEnd($work); - // Hoist loop scratch allocas to the work entry — alloca inside walk/swap - // grows the native stack O(n²) under thin AOT and SIGSEGVs around n≈1000 - // (benchmarks/v2/sort-mixed.php, default 8 MiB stack) (#36385). - $swappedSlot = $this->context->builder->alloca($i1, 1, $tag.'_swapped'); - $iSlot = $this->context->builder->alloca($sizeT, 1, $tag.'_i'); - $needsSwapSlot = $this->context->builder->alloca($i1, 1, $tag.'_needs_swap'); - $tmp = $this->context->builder->alloca($valueType, 1, $tag.'_tmp'); - $this->context->builder->store($i1->constInt(1, false), $swappedSlot); - $passHead = $fn->appendBasicBlock($tag.'_pass_head'); - $passBody = $fn->appendBasicBlock($tag.'_pass_body'); - $this->context->builder->branch($passHead); - - $this->context->builder->positionAtEnd($passHead); - $didSwap = $this->context->builder->load($swappedSlot); - $this->context->builder->branchIf($didSwap, $passBody, $done); - - $this->context->builder->positionAtEnd($passBody); - $this->context->builder->store($i1->constInt(0, false), $swappedSlot); - $this->context->builder->store($zero, $iSlot); - $limit = $this->context->builder->sub($n, $one); - $walkHead = $fn->appendBasicBlock($tag.'_walk_head'); - $walkBody = $fn->appendBasicBlock($tag.'_walk_body'); - $passExit = $fn->appendBasicBlock($tag.'_pass_exit'); - $this->context->builder->branch($walkHead); - - $this->context->builder->positionAtEnd($walkHead); - $i = $this->context->builder->load($iSlot); - $atEnd = $this->context->builder->icmp(Builder::INT_UGE, $i, $limit); - $this->context->builder->branchIf($atEnd, $passExit, $walkBody); + // php-src zend_sort + stable tiebreak (was a bubble sort, O(n²): 1.5M ints never + // finished). Same algorithm => same order even for non-transitive mixed compares. + $values = $this->context->builder->load($this->context->builder->structGep($ht, $htMap['values'])); + if (!$caseInsensitive) { + $this->emitSortPackedHomogeneousFastPaths($fn, $tag, $values, $n, $valueMap, $reverse, $done); + } + ZendSortLlvm::emit( + $this->context, + $values, + $n, + fn (PHPLLVM\Value $a, PHPLLVM\Value $b): PHPLLVM\Value => $this->emitSortPackedCompare( + $tag, + $valueMap, + $i8, + $integerTag, + $reverse, + $caseInsensitive, + $a, + $b + ), + $tag + ); + $this->context->builder->branch($done); - $this->context->builder->positionAtEnd($walkBody); - $j = $this->context->builder->addNoSignedWrap($i, $one); - $valCur = $this->listEntryAt($ht, $htMap, $i); - $valNext = $this->listEntryAt($ht, $htMap, $j); - $typeCur = $this->context->builder->load($this->context->builder->structGep($valCur, $valueMap['type'])); - $typeKind = $this->context->builder->and($typeCur, $i8->constInt(0x7f, false)); - $isString = $this->context->builder->icmp(Builder::INT_EQ, $typeKind, $i8->constInt(Variable::TYPE_STRING & 0x7f, false)); - $cmpStr = $fn->appendBasicBlock($tag.'_cmp_str'); - $cmpLong = $fn->appendBasicBlock($tag.'_cmp_long'); - $cmpValue = $fn->appendBasicBlock($tag.'_cmp_value'); - $cmpObject = $fn->appendBasicBlock($tag.'_cmp_object'); - $typeDispatch = $fn->appendBasicBlock($tag.'_type_dispatch'); - $cmpDone = $fn->appendBasicBlock($tag.'_cmp_done'); - $this->context->builder->branchIf($isString, $cmpStr, $typeDispatch); + $this->context->builder->positionAtEnd($done); + $this->context->builder->returnVoid(); + } - $this->context->builder->positionAtEnd($cmpStr); - $strCur = $this->context->builder->call($this->context->lookupFunction('__value__readString'), $valCur); - $strNext = $this->context->builder->call($this->context->lookupFunction('__value__readString'), $valNext); - $cmp = $caseInsensitive - ? JitStringCompare::strcasecmp($this->context, $strCur, $strNext) - : JitStringCompare::strcmp($this->context, $strCur, $strNext); + /** + * sort()/rsort() SORT_REGULAR on arrays where zend_compare is a consistent total order, + * so any correct stable sort reproduces php-src's zend_sort result exactly: + * - all ints: O(n) radix sort on the payloads, no comparator calls at all; + * - all non-numeric strings: zend_compare is zend_binary_strcmp, so a stable merge sort + * on memcmp skips php-src's per-comparison numeric-string checks. + * Branches to $done when it sorted; otherwise falls through to the generic zend_sort. + * + * @param array $valueMap + */ + private function emitSortPackedHomogeneousFastPaths( + PHPLLVM\Value\Function_ $fn, + string $tag, + PHPLLVM\Value $values, + PHPLLVM\Value $n, + array $valueMap, + bool $reverse, + PHPLLVM\BasicBlock $done + ): void { + $b = $this->context->builder; $i64 = $this->context->getTypeFromString('int64'); - $zeroI64 = $i64->constInt(0, false); - $strOutOfOrder = $reverse - ? $this->context->builder->icmp(Builder::INT_SLT, $cmp, $zeroI64) - : $this->context->builder->icmp(Builder::INT_SGT, $cmp, $zeroI64); - $this->context->builder->store($strOutOfOrder, $needsSwapSlot); - $this->context->builder->branch($cmpDone); - - $this->context->builder->positionAtEnd($typeDispatch); - $isInteger = $this->context->builder->icmp(Builder::INT_EQ, $typeKind, $integerTag); - $checkObject = $fn->appendBasicBlock($tag.'_check_object'); - $this->context->builder->branchIf($isInteger, $cmpLong, $checkObject); - - $this->context->builder->positionAtEnd($checkObject); - $isObject = $this->context->builder->icmp(Builder::INT_EQ, $typeKind, $objectTag); - $this->context->builder->branchIf($isObject, $cmpObject, $cmpValue); - - $this->context->builder->positionAtEnd($cmpLong); - $longCur = $this->context->builder->call($this->context->lookupFunction('__value__readLong'), $valCur); - $longNext = $this->context->builder->call($this->context->lookupFunction('__value__readLong'), $valNext); - $longOutOfOrder = $reverse - ? $this->context->builder->icmp(Builder::INT_SLT, $longCur, $longNext) - : $this->context->builder->icmp(Builder::INT_SGT, $longCur, $longNext); - $this->context->builder->store($longOutOfOrder, $needsSwapSlot); - $this->context->builder->branch($cmpDone); - - $this->context->builder->positionAtEnd($cmpObject); - $objCur = $this->context->builder->call($this->context->lookupFunction('__value__readObject'), $valCur); - $objNext = $this->context->builder->call($this->context->lookupFunction('__value__readObject'), $valNext); - $objectCmp = SpaceshipRuntime::callObjectCompareSpaceship($this->context, $objCur, $objNext); - $objectOutOfOrder = $reverse - ? $this->context->builder->icmp(Builder::INT_SLT, $objectCmp, $zeroI64) - : $this->context->builder->icmp(Builder::INT_SGT, $objectCmp, $zeroI64); - $this->context->builder->store($objectOutOfOrder, $needsSwapSlot); - $this->context->builder->branch($cmpDone); + $i8 = $this->context->getTypeFromString('int8'); + $i1 = $this->context->getTypeFromString('int1'); + $count = $b->intCast($n, $i64); + $c = static fn (int $v): PHPLLVM\Value => $i64->constInt($v, false); + $kindAt = fn (PHPLLVM\Value $i): PHPLLVM\Value => $b->and( + $b->load($b->structGep($b->inBoundsGep($values, $i), $valueMap['type'])), + $i8->constInt(0x7f, false) + ); + $allLong = $b->alloca($i1); + $allStr = $b->alloca($i1); + $b->store($i1->constInt(1, false), $allLong); + $b->store($i1->constInt(1, false), $allStr); + $idx = $b->alloca($i64); + $b->store($c(0), $idx); + $head = $fn->appendBasicBlock($tag.'_homog_head'); + $body = $fn->appendBasicBlock($tag.'_homog_body'); + $decide = $fn->appendBasicBlock($tag.'_homog_decide'); + $b->branch($head); + $b->positionAtEnd($head); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($idx), $count), $body, $decide); + $b->positionAtEnd($body); + $i = $b->load($idx); + $k = $kindAt($i); + $b->store($b->and($b->load($allLong), $b->icmp(Builder::INT_EQ, $k, $i8->constInt(Variable::TYPE_NATIVE_LONG, false))), $allLong); + $b->store($b->and($b->load($allStr), $b->icmp(Builder::INT_EQ, $k, $i8->constInt(Variable::TYPE_STRING & 0x7f, false))), $allStr); + $b->store($b->add($i, $c(1)), $idx); + $b->branch($head); + + $b->positionAtEnd($decide); + $radix = $fn->appendBasicBlock($tag.'_radix'); + $notLong = $fn->appendBasicBlock($tag.'_not_all_long'); + $generic = $fn->appendBasicBlock($tag.'_generic'); + $b->branchIf($b->load($allLong), $radix, $notLong); + + // all ints: radix sort the payloads in place (type bytes are all IS_LONG already) + $b->positionAtEnd($radix); + $keys = $b->pointerCast( + $b->call($this->context->lookupFunction('__mm__malloc'), $b->mul($count, $c(8))), + $this->context->getTypeFromString('int64*') + ); + $payload = fn (PHPLLVM\Value $i): PHPLLVM\Value => $b->pointerCast( + $b->structGep($b->inBoundsGep($values, $i), $valueMap['value']), + $this->context->getTypeFromString('int64*') + ); + $this->emitCountedLoop($fn, $tag.'_radix_in', $count, fn (PHPLLVM\Value $i) => $b->store($b->load($payload($i)), $b->gep($keys, $i))); + \PHPCompiler\JIT\IntRadixSortLlvm::emit($this->context, $keys, $count, $reverse, $tag); + $this->emitCountedLoop($fn, $tag.'_radix_out', $count, fn (PHPLLVM\Value $i) => $b->store($b->load($b->gep($keys, $i)), $payload($i))); + $b->call($this->context->lookupFunction('__mm__free'), $b->pointerCast($keys, $this->context->getTypeFromString('int8*'))); + $b->branch($done); + + // all strings: usable only when none is numeric (else zend_compare goes numeric) + $b->positionAtEnd($notLong); + $strCheck = $fn->appendBasicBlock($tag.'_str_check'); + $b->branchIf($b->load($allStr), $strCheck, $generic); + $b->positionAtEnd($strCheck); + $anyNumeric = $b->alloca($i1); + $b->store($i1->constInt(0, false), $anyNumeric); + $scratch = $b->alloca($this->context->getTypeFromString('__value__')); + // IS_NULL first: __value__write* (inside the numeric parser) releases old contents. + $b->store($i8->constInt(Variable::TYPE_NULL, false), $b->structGep($scratch, $valueMap['type'])); + $i32 = $this->context->getTypeFromString('int32'); + $this->emitCountedLoop($fn, $tag.'_numchk', $count, function (PHPLLVM\Value $i) use ($b, $values, $scratch, $anyNumeric, $i32): void { + $st = \PHPCompiler\JIT\Builtin\NumericStringLlvm::call( + $this->context, + $b->call($this->context->lookupFunction('__value__readString'), $b->inBoundsGep($values, $i)), + $scratch + ); + $numeric = $b->or( + $b->icmp(Builder::INT_EQ, $st, $i32->constInt(\PHPCompiler\JIT\Builtin\NumericStringLlvm::NUMERIC, false)), + $b->icmp(Builder::INT_UGE, $st, $i32->constInt(\PHPCompiler\JIT\Builtin\NumericStringLlvm::NUMERIC_OFLOW_POS, false)) + ); + $b->store($b->or($b->load($anyNumeric), $numeric), $anyNumeric); + }); + $strSort = $fn->appendBasicBlock($tag.'_str_msort'); + $b->branchIf($b->load($anyNumeric), $generic, $strSort); + $b->positionAtEnd($strSort); + $this->emitSortStringsByPrefix($fn, $tag, $values, $count, $reverse); + $b->branch($done); + + $b->positionAtEnd($generic); + } - $this->context->builder->positionAtEnd($cmpValue); - $valueCmp = SpaceshipRuntime::callValueSpaceship($this->context, $valCur, $valNext); - $valueOutOfOrder = $reverse - ? $this->context->builder->icmp(Builder::INT_SLT, $valueCmp, $zeroI64) - : $this->context->builder->icmp(Builder::INT_SGT, $valueCmp, $zeroI64); - $this->context->builder->store($valueOutOfOrder, $needsSwapSlot); - $this->context->builder->branch($cmpDone); + /** + * Stable sort of an all-string value array by zend_binary_strcmp: sorts contiguous + * {big-endian 8-byte prefix, original index} keys and compares string bytes only when + * prefixes tie, so the hot loop never chases a string pointer. + */ + private function emitSortStringsByPrefix( + PHPLLVM\Value\Function_ $fn, + string $tag, + PHPLLVM\Value $values, + PHPLLVM\Value $count, + bool $reverse + ): void { + $b = $this->context->builder; + $i64 = $this->context->getTypeFromString('int64'); + $i8p = $this->context->getTypeFromString('int8*'); + $c = static fn (int $v): PHPLLVM\Value => $i64->constInt($v, false); + $strMap = $this->context->structFieldMap['__string__']; + $keyTy = $this->context->context->structType(false, $i64, $i64); + $keyPtrTy = $keyTy->pointerType(0); + $malloc = $this->context->lookupFunction('__mm__malloc'); + $free = $this->context->lookupFunction('__mm__free'); + $readString = $this->context->lookupFunction('__value__readString'); + $keys = $b->pointerCast($b->call($malloc, $b->mul($count, $c(16))), $keyPtrTy); + + $prefixSlot = $b->alloca($i64); + $byteIdx = $b->alloca($i64); + $this->emitCountedLoop($fn, $tag.'_pfx', $count, function (PHPLLVM\Value $i) use ($b, $values, $keys, $readString, $strMap, $i64, $i8p, $c, $prefixSlot, $byteIdx, $fn, $tag): void { + $s = $b->call($readString, $b->inBoundsGep($values, $i)); + $len = $b->load($b->structGep($s, $strMap['length'])); + $data = $b->pointerCast($b->structGep($s, $strMap['value']), $i8p); + $lim = $b->select($b->icmp(Builder::INT_ULT, $len, $c(8)), $len, $c(8)); + $b->store($c(0), $prefixSlot); + $b->store($c(0), $byteIdx); + $h = $fn->appendBasicBlock($tag.'_pfx_byte_head'); + $bd = $fn->appendBasicBlock($tag.'_pfx_byte_body'); + $o = $fn->appendBasicBlock($tag.'_pfx_byte_out'); + $b->branch($h); + $b->positionAtEnd($h); + $k = $b->load($byteIdx); + $b->branchIf($b->icmp(Builder::INT_ULT, $k, $lim), $bd, $o); + $b->positionAtEnd($bd); + $byte = $b->zExt($b->load($b->gep($data, $k)), $i64); + $shift = $b->mul($b->sub($c(7), $k), $c(8)); + $b->store($b->bitwiseOr($b->load($prefixSlot), $b->shl($byte, $shift)), $prefixSlot); + $b->store($b->add($k, $c(1)), $byteIdx); + $b->branch($h); + $b->positionAtEnd($o); + $entry = $b->gep($keys, $i); + $b->store($b->load($prefixSlot), $b->structGep($entry, 0)); + $b->store($i, $b->structGep($entry, 1)); + }); + + // Hoisted: the comparator body is emitted inside the merge loop. + $res = $b->alloca($this->context->getTypeFromString('int1')); + MergeSortLlvm::emit( + $this->context, + $keys, + $count, + function (PHPLLVM\Value $x, PHPLLVM\Value $y) use ($b, $values, $readString, $reverse, $c, $fn, $tag, $res): PHPLLVM\Value { + $px = $b->load($b->structGep($x, 0)); + $py = $b->load($b->structGep($y, 0)); + $differ = $fn->appendBasicBlock($tag.'_pfx_differ'); + $tie = $fn->appendBasicBlock($tag.'_pfx_tie'); + $join = $fn->appendBasicBlock($tag.'_pfx_join'); + $b->branchIf($b->icmp(Builder::INT_NE, $px, $py), $differ, $tie); + $b->positionAtEnd($differ); + $b->store($reverse ? $b->icmp(Builder::INT_ULT, $px, $py) : $b->icmp(Builder::INT_UGT, $px, $py), $res); + $b->branch($join); + $b->positionAtEnd($tie); + $r = \PHPCompiler\JIT\Builtin\ZendCompareLlvm::emitBinaryStrcmp( + $this->context, + $b->call($readString, $b->inBoundsGep($values, $b->load($b->structGep($x, 1)))), + $b->call($readString, $b->inBoundsGep($values, $b->load($b->structGep($y, 1)))) + ); + $b->store($reverse ? $b->icmp(Builder::INT_SLT, $r, $c(0)) : $b->icmp(Builder::INT_SGT, $r, $c(0)), $res); + $b->branch($join); + $b->positionAtEnd($join); + + return $b->load($res); + }, + $tag.'_strpfx' + ); + + // Apply the permutation. + $elemSize = $b->ptrToInt($b->gep($values->typeOf()->constNull(), $c(1)), $i64); + $bytes = $b->mul($count, $elemSize); + $sorted = $b->pointerCast($b->call($malloc, $bytes), $values->typeOf()); + LibcExtern::ensureMemcpyDecl($this->context); + $memcpy = $this->context->lookupFunction('memcpy'); + $this->emitCountedLoop($fn, $tag.'_pfx_gather', $count, function (PHPLLVM\Value $j) use ($b, $keys, $values, $sorted, $memcpy, $elemSize, $i8p): void { + $src = $b->inBoundsGep($values, $b->load($b->structGep($b->gep($keys, $j), 1))); + $b->call($memcpy, $b->pointerCast($b->inBoundsGep($sorted, $j), $i8p), $b->pointerCast($src, $i8p), $elemSize); + }); + $b->call($memcpy, $b->pointerCast($values, $i8p), $b->pointerCast($sorted, $i8p), $bytes); + $b->call($free, $b->pointerCast($sorted, $i8p)); + $b->call($free, $b->pointerCast($keys, $i8p)); + } - $this->context->builder->positionAtEnd($cmpDone); - $needsSwap = $this->context->builder->load($needsSwapSlot); - $swapBlock = $fn->appendBasicBlock($tag.'_swap'); - $advance = $fn->appendBasicBlock($tag.'_advance'); - $this->context->builder->branchIf($needsSwap, $swapBlock, $advance); + /** for (i = 0; i < $count; i++) $body(i) */ + private function emitCountedLoop(PHPLLVM\Value\Function_ $fn, string $tag, PHPLLVM\Value $count, callable $body): void + { + $b = $this->context->builder; + $i64 = $this->context->getTypeFromString('int64'); + $x = $b->alloca($i64); + $b->store($i64->constInt(0, false), $x); + $head = $fn->appendBasicBlock($tag.'_head'); + $bodyB = $fn->appendBasicBlock($tag.'_body'); + $out = $fn->appendBasicBlock($tag.'_out'); + $b->branch($head); + $b->positionAtEnd($head); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($x), $count), $bodyB, $out); + $b->positionAtEnd($bodyB); + $xv = $b->load($x); + $body($xv); + $b->store($b->add($xv, $i64->constInt(1, false)), $x); + $b->branch($head); + $b->positionAtEnd($out); + } - $this->context->builder->positionAtEnd($swapBlock); - $this->context->builder->store($this->context->builder->load($valCur), $tmp); - $this->context->builder->store($this->context->builder->load($valNext), $valCur); - $this->context->builder->store($this->context->builder->load($tmp), $valNext); - $this->context->builder->store($i1->constInt(1, false), $swappedSlot); - $this->context->builder->branch($advance); + /** + * Three-way compare for sort()/rsort(): php_array_data_compare_unstable_i (zend_compare) + * for SORT_REGULAR, strcasecmp on strings for SORT_STRING|SORT_FLAG_CASE; negated for + * the reverse variants (php_array_reverse_*: result * -1). + * + * @param array $valueMap + */ + private function emitSortPackedCompare( + string $tag, + array $valueMap, + PHPLLVM\Type $i8, + PHPLLVM\Value $integerTag, + bool $reverse, + bool $caseInsensitive, + PHPLLVM\Value $a, + PHPLLVM\Value $b + ): PHPLLVM\Value { + $bd = $this->context->builder; + $i64 = $this->context->getTypeFromString('int64'); + $fn = BasicBlockHelper::parentFunction($this->context); + $cmpSlot = $bd->alloca($i64); + $kind = fn (PHPLLVM\Value $v): PHPLLVM\Value => $bd->and( + $bd->load($bd->structGep($v, $valueMap['type'])), + $i8->constInt(0x7f, false) + ); + $ka = $kind($a); + $kb = $kind($b); + $longs = $fn->appendBasicBlock($tag.'_cmp_longs'); + $generic = $fn->appendBasicBlock($tag.'_cmp_generic'); + $joined = $fn->appendBasicBlock($tag.'_cmp_join'); + $bothLong = $bd->and($bd->icmp(Builder::INT_EQ, $ka, $integerTag), $bd->icmp(Builder::INT_EQ, $kb, $integerTag)); + $bd->branchIf($bothLong, $longs, $generic); + + $bd->positionAtEnd($longs); + $la = $bd->call($this->context->lookupFunction('__value__readLong'), $a); + $lb = $bd->call($this->context->lookupFunction('__value__readLong'), $b); + $bd->store( + $bd->sub( + $bd->zExt($bd->icmp(Builder::INT_SGT, $la, $lb), $i64), + $bd->zExt($bd->icmp(Builder::INT_SLT, $la, $lb), $i64) + ), + $cmpSlot + ); + $bd->branch($joined); - $this->context->builder->positionAtEnd($advance); - $this->context->builder->store($this->context->builder->addNoSignedWrap($i, $one), $iSlot); - $this->context->builder->branch($walkHead); + $bd->positionAtEnd($generic); + if ($caseInsensitive) { + $strs = $fn->appendBasicBlock($tag.'_cmp_strcase'); + $other = $fn->appendBasicBlock($tag.'_cmp_other'); + $stringKind = $i8->constInt(Variable::TYPE_STRING & 0x7f, false); + $bothStr = $bd->and($bd->icmp(Builder::INT_EQ, $ka, $stringKind), $bd->icmp(Builder::INT_EQ, $kb, $stringKind)); + $bd->branchIf($bothStr, $strs, $other); + $bd->positionAtEnd($strs); + $sa = $bd->call($this->context->lookupFunction('__value__readString'), $a); + $sb = $bd->call($this->context->lookupFunction('__value__readString'), $b); + $bd->store($bd->intCast(JitStringCompare::strcasecmp($this->context, $sa, $sb), $i64), $cmpSlot); + $bd->branch($joined); + $bd->positionAtEnd($other); + } + $bd->store(\PHPCompiler\JIT\Builtin\ZendCompareLlvm::call($this->context, $a, $b), $cmpSlot); + $bd->branch($joined); - $this->context->builder->positionAtEnd($passExit); - $this->context->builder->branch($passHead); + $bd->positionAtEnd($joined); + $r = $bd->load($cmpSlot); - $this->context->builder->positionAtEnd($done); - $this->context->builder->returnVoid(); + return $reverse ? $bd->sub($i64->constInt(0, false), $r) : $r; } /** diff --git a/lib/JIT/Builtin/ZendCompareLlvm.php b/lib/JIT/Builtin/ZendCompareLlvm.php new file mode 100644 index 00000000000..f98355f38fa --- /dev/null +++ b/lib/JIT/Builtin/ZendCompareLlvm.php @@ -0,0 +1,583 @@ +getTypeFromString('__value__*'); + + return $context->builder->call( + self::ensure($context), + $context->builder->pointerCast($a, $valuePtr), + $context->builder->pointerCast($b, $valuePtr) + ); + } + + /** + * zend_binary_strcmp(s1, s2) normalized to -1/0/1, emitted at the current position. + * + * @return Value i64 + */ + public static function emitBinaryStrcmp(Context $context, Value $s1, Value $s2): Value + { + $fn = BasicBlockHelper::parentFunction($context); + + return (new self($context, $fn))->binaryStrcmp($s1, $s2); + } + + private static function ensure(Context $context): Value\Function_ + { + $fn = $context->module->getNamedFunction(self::FN); + if ($fn instanceof Value\Function_ && $fn->countBasicBlocks() > 0) { + return $fn; + } + $valuePtr = $context->getTypeFromString('__value__*'); + $i64 = $context->getTypeFromString('int64'); + if (!$fn instanceof Value\Function_) { + $fn = $context->module->addFunction( + self::FN, + $context->context->functionType($i64, false, $valuePtr, $valuePtr) + ); + $context->llvm->lib->LLVMSetLinkage($fn->value, \llvm\llvm::LLVMInternalLinkage); + } + $saved = BasicBlockHelper::tryGetInsertBlock($context); + (new self($context, $fn))->emitBody(); + if (null !== $saved) { + $context->builder->positionAtEnd($saved); + } else { + $context->builder->clearInsertionPosition(); + } + + return $fn; + } + + private function __construct(Context $context, Value\Function_ $fn) + { + $this->context = $context; + $this->b = $context->builder; + $this->fn = $fn; + $this->i64 = $context->getTypeFromString('int64'); + } + + private function blk(string $name): \PHPLLVM\BasicBlock + { + return $this->fn->appendBasicBlock($name); + } + + private function c(int $v): Value + { + return $this->i64->constInt($v, $v < 0); + } + + private function ret(Value $v): void + { + $this->b->returnValue($this->b->intCast($v, $this->i64)); + } + + private function emitBody(): void + { + $b = $this->b; + $b->positionAtEnd($this->blk('entry')); + $a = $this->fn->getParam(0); + $bb = $this->fn->getParam(1); + $valueTy = $this->context->getTypeFromString('__value__'); + $numA = $b->alloca($valueTy); + $numB = $b->alloca($valueTy); + // Parse targets must start as IS_NULL: __value__write* releases the old contents. + $typeIdx = $this->context->structFieldMap['__value__']['type']; + $nullTag = $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_NULL, false); + $b->store($nullTag, $b->structGep($numA, $typeIdx)); + $b->store($nullTag, $b->structGep($numB, $typeIdx)); + $za = $this->zendType($a); + $zb = $this->zendType($bb); + $isZ = fn (Value $z, int $t): Value => $b->icmp(Builder::INT_EQ, $z, $this->c($t)); + $pair = fn (int $x, int $y): Value => $b->bitwiseAnd($isZ($za, $x), $isZ($zb, $y)); + + $dispatch = function (Value $cond, string $name, callable $emit) use ($b): void { + $yes = $this->blk($name); + $no = $this->blk($name.'_no'); + $b->branchIf($cond, $yes, $no); + $b->positionAtEnd($yes); + $emit(); + $b->positionAtEnd($no); + }; + + $dispatch( + $b->bitwiseOr($isZ($za, self::Z_OTHER), $isZ($zb, self::Z_OTHER)), + 'other', + fn () => $this->ret(SpaceshipRuntime::callValueSpaceship($this->context, $a, $bb)) + ); + $dispatch($pair(self::Z_LONG, self::Z_LONG), 'long_long', function () use ($a, $bb): void { + $this->ret($this->sign($this->readLong($a), $this->readLong($bb))); + }); + $dispatch($pair(self::Z_DOUBLE, self::Z_DOUBLE), 'dbl_dbl', function () use ($a, $bb): void { + $da = $this->readDouble($a); + $db = $this->readDouble($bb); + $this->ret($this->b->select( + $this->b->fcmp(Builder::REAL_OEQ, $da, $db), + $this->c(0), + $this->normalizeDouble($this->b->fSub($da, $db)) + )); + }); + $dispatch($b->bitwiseOr($pair(self::Z_LONG, self::Z_DOUBLE), $pair(self::Z_DOUBLE, self::Z_LONG)), 'mixed_num', function () use ($a, $bb): void { + $this->ret($this->normalizeDouble($this->b->fSub($this->numberAsDouble($a), $this->numberAsDouble($bb)))); + }); + $dispatch($pair(self::Z_STRING, self::Z_STRING), 'str_str', function () use ($a, $bb, $numA, $numB): void { + $sa = $this->readString($a); + $sb = $this->readString($bb); + $same = $this->blk('str_same'); + $diff = $this->blk('str_diff'); + $this->b->branchIf($this->b->icmp(Builder::INT_EQ, $sa, $sb), $same, $diff); + $this->b->positionAtEnd($same); + $this->ret($this->c(0)); + $this->b->positionAtEnd($diff); + $this->ret($this->smartStrcmp($sa, $sb, $numA, $numB)); + }); + $dispatch($pair(self::Z_NULL, self::Z_STRING), 'null_str', function () use ($bb): void { + $len = $this->strlen($this->readString($bb)); + $this->ret($this->b->select($this->b->icmp(Builder::INT_EQ, $len, $this->c(0)), $this->c(0), $this->c(-1))); + }); + $dispatch($pair(self::Z_STRING, self::Z_NULL), 'str_null', function () use ($a): void { + $len = $this->strlen($this->readString($a)); + $this->ret($this->b->select($this->b->icmp(Builder::INT_EQ, $len, $this->c(0)), $this->c(0), $this->c(1))); + }); + $dispatch($pair(self::Z_LONG, self::Z_STRING), 'long_str', function () use ($a, $bb, $numB): void { + $this->ret($this->compareLongToString($this->readLong($a), $this->readString($bb), $numB)); + }); + $dispatch($pair(self::Z_STRING, self::Z_LONG), 'str_long', function () use ($a, $bb, $numA): void { + $this->ret($this->b->sub($this->c(0), $this->compareLongToString($this->readLong($bb), $this->readString($a), $numA))); + }); + $dispatch($pair(self::Z_DOUBLE, self::Z_STRING), 'dbl_str', function () use ($a, $bb, $numB): void { + $d = $this->readDouble($a); + $this->ret($this->b->select( + $this->isNan($d), + $this->c(1), + $this->compareDoubleToString($d, $this->readString($bb), $numB) + )); + }); + $dispatch($pair(self::Z_STRING, self::Z_DOUBLE), 'str_dbl', function () use ($a, $bb, $numA): void { + $d = $this->readDouble($bb); + $this->ret($this->b->select( + $this->isNan($d), + $this->c(1), + $this->b->sub($this->c(0), $this->compareDoubleToString($d, $this->readString($a), $numA)) + )); + }); + + // Remaining pairs involve null/false/true: zend_compare's bool fallback. + $ta = $this->isTrue($a, $za); + $tb = $this->isTrue($bb, $zb); + $zero = $this->c(0); + $one = $this->c(1); + $neg = $this->c(-1); + $r = $b->select( + $b->icmp(Builder::INT_SLT, $za, $this->c(self::Z_TRUE)), + $b->select($tb, $neg, $zero), + $b->select( + $isZ($za, self::Z_TRUE), + $b->select($tb, $zero, $one), + $b->select( + $b->icmp(Builder::INT_SLT, $zb, $this->c(self::Z_TRUE)), + $b->select($ta, $one, $zero), + $b->select($ta, $zero, $neg) + ) + ) + ); + $this->ret($r); + } + + private function zendType(Value $v): Value + { + $b = $this->b; + $i8 = $this->context->getTypeFromString('int8'); + $map = $this->context->structFieldMap['__value__']; + $kind = $b->bitwiseAnd($b->load($b->structGep($v, $map['type'])), $i8->constInt(0x7f, false)); + $boolByte = $b->load($b->pointerCast($b->structGep($v, $map['value']), $this->context->getTypeFromString('int8*'))); + $isKind = fn (int $t): Value => $b->icmp(Builder::INT_EQ, $kind, $i8->constInt($t, false)); + $z = $this->c(self::Z_OTHER); + $z = $b->select($isKind(Variable::TYPE_STRING & 0x7f), $this->c(self::Z_STRING), $z); + $z = $b->select($isKind(Variable::TYPE_NATIVE_DOUBLE), $this->c(self::Z_DOUBLE), $z); + $z = $b->select($isKind(Variable::TYPE_NATIVE_LONG), $this->c(self::Z_LONG), $z); + $z = $b->select( + $isKind(Variable::TYPE_NATIVE_BOOL), + $b->select($b->icmp(Builder::INT_NE, $boolByte, $i8->constInt(0, false)), $this->c(self::Z_TRUE), $this->c(self::Z_FALSE)), + $z + ); + + return $b->select($isKind(Variable::TYPE_NULL), $this->c(self::Z_NULL), $z); + } + + private function readLong(Value $v): Value + { + return $this->b->call($this->context->lookupFunction('__value__readLong'), $v); + } + + private function readDouble(Value $v): Value + { + return $this->b->call($this->context->lookupFunction('__value__readDouble'), $v); + } + + private function readString(Value $v): Value + { + return $this->b->call($this->context->lookupFunction('__value__readString'), $v); + } + + /** IS_LONG or IS_DOUBLE box as double. */ + private function numberAsDouble(Value $v): Value + { + return $this->b->call($this->context->lookupFunction('__value__readDouble'), $v); + } + + private function strlen(Value $s): Value + { + return $this->b->load($this->b->structGep($s, $this->context->structFieldMap['__string__']['length'])); + } + + private function sign(Value $x, Value $y): Value + { + $b = $this->b; + + return $b->sub( + $b->zExt($b->icmp(Builder::INT_SGT, $x, $y), $this->i64), + $b->zExt($b->icmp(Builder::INT_SLT, $x, $y), $this->i64) + ); + } + + /** ZEND_NORMALIZE_BOOL(d): NaN is truthy and not < 0, so it normalizes to 1. */ + private function normalizeDouble(Value $d): Value + { + $b = $this->b; + $zero = $this->context->getTypeFromString('double')->constReal(0.0); + + return $b->select( + $b->fcmp(Builder::REAL_OEQ, $d, $zero), + $this->c(0), + $b->select($b->fcmp(Builder::REAL_OLT, $d, $zero), $this->c(-1), $this->c(1)) + ); + } + + private function isNan(Value $d): Value + { + return $this->b->fcmp(Builder::REAL_UNO, $d, $d); + } + + /** zval_is_true for null/bool/int/float/string boxes. */ + private function isTrue(Value $v, Value $z): Value + { + $b = $this->b; + $isZ = fn (int $t): Value => $b->icmp(Builder::INT_EQ, $z, $this->c($t)); + $long = $b->icmp(Builder::INT_NE, $this->readLong($v), $this->c(0)); + $dbl = $b->fcmp(Builder::REAL_UNE, $this->readDouble($v), $this->context->getTypeFromString('double')->constReal(0.0)); + // Strings: only read the payload when the box is a string. + $strBlock = $this->blk('is_true_str'); + $join = $this->blk('is_true_join'); + $from = $b->getInsertBlock(); + $b->branchIf($isZ(self::Z_STRING), $strBlock, $join); + $b->positionAtEnd($strBlock); + $s = $this->readString($v); + $len = $this->strlen($s); + $first = $b->load($b->pointerCast( + $b->structGep($s, $this->context->structFieldMap['__string__']['value']), + $this->context->getTypeFromString('int8*') + )); + $strTrue = $b->bitwiseOr( + $b->icmp(Builder::INT_UGT, $len, $this->c(1)), + $b->bitwiseAnd( + $b->icmp(Builder::INT_EQ, $len, $this->c(1)), + $b->icmp(Builder::INT_NE, $first, $this->context->getTypeFromString('int8')->constInt(\ord('0'), false)) + ) + ); + $strEnd = $b->getInsertBlock(); + $b->branch($join); + $b->positionAtEnd($join); + $i1 = $this->context->getTypeFromString('int1'); + $strPhi = $b->phi($i1); + $strPhi->addIncoming($i1->constInt(0, false), $from); + $strPhi->addIncoming($strTrue, $strEnd); + + return $b->select( + $isZ(self::Z_TRUE), + $i1->constInt(1, false), + $b->select( + $isZ(self::Z_LONG), + $long, + $b->select($isZ(self::Z_DOUBLE), $dbl, $b->select($isZ(self::Z_STRING), $strPhi, $i1->constInt(0, false))) + ) + ); + } + + /** zend_binary_strcmp, normalized. */ + private function binaryStrcmp(Value $s1, Value $s2): Value + { + $b = $this->b; + $map = $this->context->structFieldMap['__string__']; + $i8p = $this->context->getTypeFromString('int8*'); + $l1 = $this->strlen($s1); + $l2 = $this->strlen($s2); + $min = $b->select($b->icmp(Builder::INT_ULT, $l1, $l2), $l1, $l2); + $memcmp = $this->memcmpFn(); + $r = $b->call( + $memcmp, + $b->pointerCast($b->structGep($s1, $map['value']), $i8p), + $b->pointerCast($b->structGep($s2, $map['value']), $i8p), + $min + ); + $r64 = $b->sExt($r, $this->i64); + + return $b->select( + $b->icmp(Builder::INT_NE, $r64, $this->c(0)), + $this->sign($r64, $this->c(0)), + $this->sign($l1, $l2) + ); + } + + /** @return array{Value, Value} [status i32, type byte of the parsed box] */ + private function parseNumeric(Value $s, Value $box): array + { + $status = NumericStringLlvm::call($this->context, $s, $box); + $map = $this->context->structFieldMap['__value__']; + + return [$status, $this->b->load($this->b->structGep($box, $map['type']))]; + } + + private function statusIsNumeric(Value $status): Value + { + $b = $this->b; + $i32 = $this->context->getTypeFromString('int32'); + + return $b->bitwiseOr( + $b->icmp(Builder::INT_EQ, $status, $i32->constInt(NumericStringLlvm::NUMERIC, false)), + $b->icmp(Builder::INT_UGE, $status, $i32->constInt(NumericStringLlvm::NUMERIC_OFLOW_POS, false)) + ); + } + + private function boxIsLong(Value $typeByte): Value + { + return $this->b->icmp( + Builder::INT_EQ, + $typeByte, + $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_NATIVE_LONG, false) + ); + } + + /** compare_long_to_string() */ + private function compareLongToString(Value $l, Value $s, Value $box): Value + { + $b = $this->b; + [$status, $type] = $this->parseNumeric($s, $box); + $numeric = $this->blk('lts_numeric'); + $textual = $this->blk('lts_textual'); + $join = $this->blk('lts_join'); + $b->branchIf($this->statusIsNumeric($status), $numeric, $textual); + + $b->positionAtEnd($numeric); + $numRes = $b->select( + $this->boxIsLong($type), + $this->sign($l, $this->readLong($box)), + $this->normalizeDouble($b->fSub($b->siToFp($l, $this->context->getTypeFromString('double')), $this->readDouble($box))) + ); + $numEnd = $b->getInsertBlock(); + $b->branch($join); + + $b->positionAtEnd($textual); + $str = $b->call($this->context->lookupFunction('__string__fromLong'), $l); + $txtRes = $this->binaryStrcmp($str, $s); + $this->release($str); + $txtEnd = $b->getInsertBlock(); + $b->branch($join); + + $b->positionAtEnd($join); + $phi = $b->phi($this->i64); + $phi->addIncoming($numRes, $numEnd); + $phi->addIncoming($txtRes, $txtEnd); + + return $phi; + } + + /** compare_double_to_string() */ + private function compareDoubleToString(Value $d, Value $s, Value $box): Value + { + $b = $this->b; + $f64 = $this->context->getTypeFromString('double'); + [$status, $type] = $this->parseNumeric($s, $box); + $numeric = $this->blk('dts_numeric'); + $textual = $this->blk('dts_textual'); + $join = $this->blk('dts_join'); + $b->branchIf($this->statusIsNumeric($status), $numeric, $textual); + + $b->positionAtEnd($numeric); + $sd = $b->select($this->boxIsLong($type), $b->siToFp($this->readLong($box), $f64), $this->readDouble($box)); + // d is never NaN here (checked by the caller), so d == sd normalizes to 0 already. + $numRes = $this->normalizeDouble($b->fSub($d, $sd)); + $numEnd = $b->getInsertBlock(); + $b->branch($join); + + $b->positionAtEnd($textual); + $str = ZendDoubleStringRuntime::format($this->context, $d); + $txtRes = $this->binaryStrcmp($str, $s); + $txtEnd = $b->getInsertBlock(); + $b->branch($join); + + $b->positionAtEnd($join); + $phi = $b->phi($this->i64); + $phi->addIncoming($numRes, $numEnd); + $phi->addIncoming($txtRes, $txtEnd); + + return $phi; + } + + /** zendi_smart_strcmp() for two distinct strings. */ + private function smartStrcmp(Value $s1, Value $s2, Value $box1, Value $box2): Value + { + $b = $this->b; + $f64 = $this->context->getTypeFromString('double'); + $i32 = $this->context->getTypeFromString('int32'); + $out = $b->alloca($this->i64); + $done = $this->blk('ss_done'); + $strcmp = $this->blk('ss_strcmp'); + $second = $this->blk('ss_second'); + $both = $this->blk('ss_both'); + + [$st1, $t1] = $this->parseNumeric($s1, $box1); + $b->branchIf($this->statusIsNumeric($st1), $second, $strcmp); + $b->positionAtEnd($second); + [$st2, $t2] = $this->parseNumeric($s2, $box2); + $b->branchIf($this->statusIsNumeric($st2), $both, $strcmp); + + $b->positionAtEnd($both); + $oflow = fn (Value $st): Value => $b->select( + $b->icmp(Builder::INT_EQ, $st, $i32->constInt(NumericStringLlvm::NUMERIC_OFLOW_POS, false)), + $this->c(1), + $b->select($b->icmp(Builder::INT_EQ, $st, $i32->constInt(NumericStringLlvm::NUMERIC_OFLOW_NEG, false)), $this->c(-1), $this->c(0)) + ); + $o1 = $oflow($st1); + $o2 = $oflow($st2); + $isLong1 = $this->boxIsLong($t1); + $isLong2 = $this->boxIsLong($t2); + $d1 = $b->select($isLong1, $b->siToFp($this->readLong($box1), $f64), $this->readDouble($box1)); + $d2 = $b->select($isLong2, $b->siToFp($this->readLong($box2), $f64), $this->readDouble($box2)); + $zeroD = $f64->constReal(0.0); + // Both integers overflowed to the same side with equal doubles: compare as strings. + $sameOflow = $b->bitwiseAnd( + $b->bitwiseAnd($b->icmp(Builder::INT_NE, $o1, $this->c(0)), $b->icmp(Builder::INT_EQ, $o1, $o2)), + $b->fcmp(Builder::REAL_OEQ, $b->fSub($d1, $d2), $zeroD) + ); + $num = $this->blk('ss_num'); + $b->branchIf($sameOflow, $strcmp, $num); + + $b->positionAtEnd($num); + $longs = $this->blk('ss_longs'); + $dbls = $this->blk('ss_dbls'); + $b->branchIf($b->bitwiseAnd($isLong1, $isLong2), $longs, $dbls); + $b->positionAtEnd($longs); + $b->store($this->sign($this->readLong($box1), $this->readLong($box2)), $out); + $b->branch($done); + + $b->positionAtEnd($dbls); + $only2Double = $this->blk('ss_only2'); + $not2 = $this->blk('ss_not_only2'); + $b->branchIf($isLong1, $only2Double, $not2); + // ret1 != IS_DOUBLE: oflow2 ? -oflow2 : numeric + $b->positionAtEnd($only2Double); + $diff = $this->blk('ss_diff'); + $retO2 = $this->blk('ss_ret_o2'); + $b->branchIf($b->icmp(Builder::INT_NE, $o2, $this->c(0)), $retO2, $diff); + $b->positionAtEnd($retO2); + $b->store($b->sub($this->c(0), $o2), $out); + $b->branch($done); + $b->positionAtEnd($not2); + $only1Double = $this->blk('ss_only1'); + $bothDouble = $this->blk('ss_both_double'); + $b->branchIf($isLong2, $only1Double, $bothDouble); + $b->positionAtEnd($only1Double); + $retO1 = $this->blk('ss_ret_o1'); + $b->branchIf($b->icmp(Builder::INT_NE, $o1, $this->c(0)), $retO1, $diff); + $b->positionAtEnd($retO1); + $b->store($o1, $out); + $b->branch($done); + // both doubles, equal and non-finite: compare as strings + $b->positionAtEnd($bothDouble); + $isFinite = $b->fcmp(Builder::REAL_OEQ, $b->fSub($d1, $d1), $zeroD); + $b->branchIf( + $b->bitwiseAnd($b->fcmp(Builder::REAL_OEQ, $d1, $d2), $b->xor($isFinite, $this->context->getTypeFromString('int1')->constInt(1, false))), + $strcmp, + $diff + ); + $b->positionAtEnd($diff); + $b->store($this->normalizeDouble($b->fSub($d1, $d2)), $out); + $b->branch($done); + + $b->positionAtEnd($strcmp); + $b->store($this->binaryStrcmp($s1, $s2), $out); + $b->branch($done); + + $b->positionAtEnd($done); + + return $b->load($out); + } + + private function release(Value $str): void + { + $this->b->call( + $this->context->lookupFunction('__ref__delref'), + $this->b->pointerCast($str, $this->context->getTypeFromString('__ref__virtual*')) + ); + } + + private function memcmpFn(): Value\Function_ + { + $fn = $this->context->module->getNamedFunction('memcmp'); + if ($fn instanceof Value\Function_) { + return $fn; + } + $i8p = $this->context->getTypeFromString('int8*'); + + return $this->context->module->addFunction('memcmp', $this->context->context->functionType( + $this->context->getTypeFromString('int32'), + false, + $i8p, + $i8p, + $this->context->getTypeFromString('size_t') + )); + } +} diff --git a/lib/JIT/IntRadixSortLlvm.php b/lib/JIT/IntRadixSortLlvm.php new file mode 100644 index 00000000000..af94063c344 --- /dev/null +++ b/lib/JIT/IntRadixSortLlvm.php @@ -0,0 +1,124 @@ +builder; + $fn = BasicBlockHelper::parentFunction($context); + $i64 = $context->getTypeFromString('int64'); + $i64p = $context->getTypeFromString('int64*'); + $i8p = $context->getTypeFromString('int8*'); + $c = static fn (int $v): Value => $i64->constInt($v, $v < 0); + $malloc = $context->lookupFunction('__mm__malloc'); + $free = $context->lookupFunction('__mm__free'); + LibcExtern::ensureMemcpyDecl($context); + $memcpy = $context->lookupFunction('memcpy'); + $blk = static fn (string $n) => $fn->appendBasicBlock($tag.'_radix_'.$n); + $loop = static function (string $name, Value $from, Value $to, callable $body) use ($b, $i64, $c, $blk): void { + $x = $b->alloca($i64); + $b->store($from, $x); + $head = $blk($name.'_head'); + $bodyB = $blk($name.'_body'); + $out = $blk($name.'_out'); + $b->branch($head); + $b->positionAtEnd($head); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($x), $to), $bodyB, $out); + $b->positionAtEnd($bodyB); + $xv = $b->load($x); + $body($xv); + $b->store($b->add($xv, $c(1)), $x); + $b->branch($head); + $b->positionAtEnd($out); + }; + + $n = $b->intCast($count, $i64); + // Map signed order to unsigned order (flip the sign bit); descending also inverts. + $flip = $reverse ? $c(\PHP_INT_MAX) : $c(\PHP_INT_MIN); + $loop('enc', $c(0), $n, static function (Value $i) use ($b, $keys, $flip): void { + $p = $b->gep($keys, $i); + $b->store($b->xor($b->load($p), $flip), $p); + }); + $tmp = $b->pointerCast($b->call($malloc, $b->mul($n, $c(8))), $i64p); + $counts = $b->pointerCast($b->call($malloc, $c(256 * 8)), $i64p); + $src = $b->alloca($i64p); + $dst = $b->alloca($i64p); + $b->store($keys, $src); + $b->store($tmp, $dst); + LibcExtern::ensureMemsetDecl($context); + $memset = $context->lookupFunction('memset'); + + for ($pass = 0; $pass < 8; ++$pass) { + $shift = $c(8 * $pass); + $s = $b->load($src); + $b->call($memset, $b->pointerCast($counts, $i8p), $context->getTypeFromString('int32')->constInt(0, false), $c(256 * 8)); + $loop('cnt'.$pass, $c(0), $n, static function (Value $i) use ($b, $s, $shift, $counts, $c): void { + $byte = $b->bitwiseAnd($b->lShr($b->load($b->gep($s, $i)), $shift), $c(255)); + $p = $b->gep($counts, $byte); + $b->store($b->add($b->load($p), $c(1)), $p); + }); + // Skip a pass whose byte is the same for every key. + $first = $b->bitwiseAnd($b->lShr($b->load($b->gep($s, $c(0))), $shift), $c(255)); + $doPass = $blk('do'.$pass); + $skip = $blk('skip'.$pass); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($b->gep($counts, $first)), $n), $skip, $doPass); + $b->positionAtEnd($doPass); + // exclusive prefix sums + $sum = $b->alloca($i64); + $b->store($c(0), $sum); + $loop('pre'.$pass, $c(0), $c(256), static function (Value $k) use ($b, $counts, $sum): void { + $p = $b->gep($counts, $k); + $v = $b->load($p); + $b->store($b->load($sum), $p); + $b->store($b->add($b->load($sum), $v), $sum); + }); + $d = $b->load($dst); + $loop('scat'.$pass, $c(0), $n, static function (Value $i) use ($b, $s, $d, $shift, $counts, $c): void { + $key = $b->load($b->gep($s, $i)); + $byte = $b->bitwiseAnd($b->lShr($key, $shift), $c(255)); + $p = $b->gep($counts, $byte); + $pos = $b->load($p); + $b->store($key, $b->gep($d, $pos)); + $b->store($b->add($pos, $c(1)), $p); + }); + $b->store($d, $src); + $b->store($s, $dst); + $b->branch($skip); + $b->positionAtEnd($skip); + } + + $result = $b->load($src); + $copy = $blk('copy'); + $after = $blk('after'); + $b->branchIf($b->icmp(Builder::INT_EQ, $result, $keys), $after, $copy); + $b->positionAtEnd($copy); + $b->call($memcpy, $b->pointerCast($keys, $i8p), $b->pointerCast($result, $i8p), $b->mul($n, $c(8))); + $b->branch($after); + $b->positionAtEnd($after); + $loop('dec', $c(0), $n, static function (Value $i) use ($b, $keys, $flip): void { + $p = $b->gep($keys, $i); + $b->store($b->xor($b->load($p), $flip), $p); + }); + $b->call($free, $b->pointerCast($tmp, $i8p)); + $b->call($free, $b->pointerCast($counts, $i8p)); + } +} diff --git a/lib/JIT/MergeSortLlvm.php b/lib/JIT/MergeSortLlvm.php new file mode 100644 index 00000000000..0e6a8a8775d --- /dev/null +++ b/lib/JIT/MergeSortLlvm.php @@ -0,0 +1,190 @@ + b` (i1) for element pointers; + * may append blocks, must leave the builder + * in the block where the i1 is available + */ + public static function emit(Context $context, Value $base, Value $count, callable $outOfOrder, string $tag): void + { + $b = $context->builder; + $fn = BasicBlockHelper::parentFunction($context); + $sizeT = $context->getTypeFromString('size_t'); + $i8p = $context->getTypeFromString('int8*'); + $elemPtrTy = $base->typeOf(); + $zero = $sizeT->constInt(0, false); + $one = $sizeT->constInt(1, false); + $two = $sizeT->constInt(2, false); + + $elemSize = $b->ptrToInt( + $b->gep($elemPtrTy->constNull(), $one), + $sizeT + ); + $bytes = $b->mul($count, $elemSize); + + $done = $fn->appendBasicBlock($tag.'_msort_done'); + $work = $fn->appendBasicBlock($tag.'_msort_work'); + $b->branchIf($b->icmp(Builder::INT_ULT, $count, $two), $done, $work); + + $b->positionAtEnd($work); + $scratch = $b->pointerCast($b->call($context->lookupFunction('__mm__malloc'), $bytes), $elemPtrTy); + $srcSlot = $b->alloca($elemPtrTy); + $dstSlot = $b->alloca($elemPtrTy); + $widthSlot = $b->alloca($sizeT); + $loSlot = $b->alloca($sizeT); + $iSlot = $b->alloca($sizeT); + $jSlot = $b->alloca($sizeT); + $kSlot = $b->alloca($sizeT); + $b->store($base, $srcSlot); + $b->store($scratch, $dstSlot); + $b->store($one, $widthSlot); + + $passHead = $fn->appendBasicBlock($tag.'_msort_pass_head'); + $passBody = $fn->appendBasicBlock($tag.'_msort_pass_body'); + $finish = $fn->appendBasicBlock($tag.'_msort_finish'); + $b->branch($passHead); + + // for (width = 1; width < n; width *= 2) + $b->positionAtEnd($passHead); + $width = $b->load($widthSlot); + $b->branchIf($b->icmp(Builder::INT_ULT, $width, $count), $passBody, $finish); + + $b->positionAtEnd($passBody); + $b->store($zero, $loSlot); + $runHead = $fn->appendBasicBlock($tag.'_msort_run_head'); + $runBody = $fn->appendBasicBlock($tag.'_msort_run_body'); + $passEnd = $fn->appendBasicBlock($tag.'_msort_pass_end'); + $b->branch($runHead); + + // for (lo = 0; lo < n; lo += 2*width) + $b->positionAtEnd($runHead); + $lo = $b->load($loSlot); + $b->branchIf($b->icmp(Builder::INT_ULT, $lo, $count), $runBody, $passEnd); + + $b->positionAtEnd($runBody); + $widthR = $b->load($widthSlot); + $mid = self::umin($context, $b->add($lo, $widthR), $count); + $hi = self::umin($context, $b->add($lo, $b->mul($widthR, $two)), $count); + $src = $b->load($srcSlot); + $dst = $b->load($dstSlot); + $b->store($lo, $iSlot); + $b->store($mid, $jSlot); + $b->store($lo, $kSlot); + $mergeHead = $fn->appendBasicBlock($tag.'_msort_merge_head'); + $mergeBoth = $fn->appendBasicBlock($tag.'_msort_merge_both'); + $takeLeft = $fn->appendBasicBlock($tag.'_msort_take_left'); + $takeRight = $fn->appendBasicBlock($tag.'_msort_take_right'); + $leftTail = $fn->appendBasicBlock($tag.'_msort_left_tail'); + $b->branch($mergeHead); + + // while (i < mid && j < hi) + $b->positionAtEnd($mergeHead); + $i = $b->load($iSlot); + $j = $b->load($jSlot); + $bothLeft = $b->bitwiseAnd( + $b->icmp(Builder::INT_ULT, $i, $mid), + $b->icmp(Builder::INT_ULT, $j, $hi) + ); + $b->branchIf($bothLeft, $mergeBoth, $leftTail); + + $b->positionAtEnd($mergeBoth); + $gt = $outOfOrder($b->inBoundsGep($src, $i), $b->inBoundsGep($src, $j)); + $b->branchIf($gt, $takeRight, $takeLeft); + + $b->positionAtEnd($takeLeft); + $iL = $b->load($iSlot); + $kL = $b->load($kSlot); + $b->store($b->load($b->inBoundsGep($src, $iL)), $b->inBoundsGep($dst, $kL)); + $b->store($b->add($iL, $one), $iSlot); + $b->store($b->add($kL, $one), $kSlot); + $b->branch($mergeHead); + + $b->positionAtEnd($takeRight); + $jR = $b->load($jSlot); + $kR = $b->load($kSlot); + $b->store($b->load($b->inBoundsGep($src, $jR)), $b->inBoundsGep($dst, $kR)); + $b->store($b->add($jR, $one), $jSlot); + $b->store($b->add($kR, $one), $kSlot); + $b->branch($mergeHead); + + // Tails: copy src[i..mid) then src[j..hi) — at most one is non-empty. + $b->positionAtEnd($leftTail); + $iT = $b->load($iSlot); + $jT = $b->load($jSlot); + $kT = $b->load($kSlot); + $memcpy = self::memcpyFn($context); + $leftLen = $b->sub($mid, $iT); + $b->call( + $memcpy, + $b->pointerCast($b->inBoundsGep($dst, $kT), $i8p), + $b->pointerCast($b->inBoundsGep($src, $iT), $i8p), + $b->mul($leftLen, $elemSize) + ); + $k2 = $b->add($kT, $leftLen); + $b->call( + $memcpy, + $b->pointerCast($b->inBoundsGep($dst, $k2), $i8p), + $b->pointerCast($b->inBoundsGep($src, $jT), $i8p), + $b->mul($b->sub($hi, $jT), $elemSize) + ); + $b->store($hi, $loSlot); + $b->branch($runHead); + + $b->positionAtEnd($passEnd); + $s = $b->load($srcSlot); + $d = $b->load($dstSlot); + $b->store($d, $srcSlot); + $b->store($s, $dstSlot); + $b->store($b->mul($b->load($widthSlot), $two), $widthSlot); + $b->branch($passHead); + + // Result lives in src; copy back if that is the scratch buffer. + $b->positionAtEnd($finish); + $result = $b->load($srcSlot); + $copyBack = $fn->appendBasicBlock($tag.'_msort_copy_back'); + $freeBlock = $fn->appendBasicBlock($tag.'_msort_free'); + $b->branchIf($b->icmp(Builder::INT_EQ, $result, $base), $freeBlock, $copyBack); + $b->positionAtEnd($copyBack); + $b->call($memcpy, $b->pointerCast($base, $i8p), $b->pointerCast($result, $i8p), $bytes); + $b->branch($freeBlock); + $b->positionAtEnd($freeBlock); + $b->call($context->lookupFunction('__mm__free'), $b->pointerCast($scratch, $i8p)); + $b->branch($done); + + $b->positionAtEnd($done); + } + + private static function umin(Context $context, Value $a, Value $b): Value + { + return $context->builder->select($context->builder->icmp(Builder::INT_ULT, $a, $b), $a, $b); + } + + private static function memcpyFn(Context $context): Value\Function_ + { + LibcExtern::ensureMemcpyDecl($context); + + return $context->lookupFunction('memcpy'); + } +} diff --git a/lib/JIT/ZendSortLlvm.php b/lib/JIT/ZendSortLlvm.php new file mode 100644 index 00000000000..1d7e360ed71 --- /dev/null +++ b/lib/JIT/ZendSortLlvm.php @@ -0,0 +1,538 @@ +emitSort($values, $count, $compare, $extra); + } + + private function __construct(Context $context, string $tag) + { + $this->context = $context; + $this->b = $context->builder; + $this->fn = BasicBlockHelper::parentFunction($context); + $this->tag = $tag.'_zsort'; + $this->i64 = $context->getTypeFromString('int64'); + } + + private function block(string $name): \PHPLLVM\BasicBlock + { + return $this->fn->appendBasicBlock($this->tag.'_'.$name); + } + + private function c(int $v): Value + { + return $this->i64->constInt($v, $v < 0); + } + + private function slot(?Value $init = null): Value + { + $s = $this->b->alloca($this->i64); + if (null !== $init) { + $this->b->store($init, $s); + } + + return $s; + } + + private function emitSort(Value $values, Value $countIn, callable $compare, ?Value $extra): void + { + $b = $this->b; + $i8p = $this->context->getTypeFromString('int8*'); + $this->values = $values; + $this->extra = null === $extra ? $i8p->constNull() : $b->pointerCast($extra, $i8p); + $this->cmpFn = $this->emitComparator($values->typeOf(), $compare); + $i64 = $this->i64; + $count = $b->intCast($countIn, $i64); + $done = $this->block('done'); + $work = $this->block('work'); + $b->branchIf($b->icmp(Builder::INT_SLT, $count, $this->c(2)), $done, $work); + + $b->positionAtEnd($work); + $i64p = $this->context->getTypeFromString('int64*'); + $malloc = $this->context->lookupFunction('__mm__malloc'); + $free = $this->context->lookupFunction('__mm__free'); + $this->perm = $b->pointerCast($b->call($malloc, $b->mul($count, $this->c(8))), $i64p); + $base = $this->slot($this->c(0)); + $nmemb = $this->slot($count); + $stack = $b->pointerCast($b->call($malloc, $this->c(2 * 8 * 128)), $i64p); + $sp = $this->slot($this->c(0)); + $i = $this->slot(); + $j = $this->slot(); + $k = $this->slot(); + $pivot = $this->slot(); + + // perm[x] = x + $this->loop('init', $this->c(0), $count, function (Value $x): void { + $this->b->store($x, $this->b->gep($this->perm, $x)); + }); + + $loopTop = $this->block('loop_top'); + $b->branch($loopTop); + + // zend_sort main loop + $b->positionAtEnd($loopTop); + $small = $this->block('small'); + $partition = $this->block('partition'); + $pop = $this->block('pop'); + $b->branchIf($b->icmp(Builder::INT_SLE, $b->load($nmemb), $this->c(16)), $small, $partition); + + $b->positionAtEnd($small); + $this->insertSort($b->load($base), $b->load($nmemb), $i, $j, $k); + $b->branch($pop); + + $b->positionAtEnd($partition); + $start = $b->load($base); + $n = $b->load($nmemb); + $end = $b->add($start, $n); + $offset = $b->lShr($n, $this->c(1)); + $piv = $b->add($start, $offset); + $big = $this->block('median5'); + $med3 = $this->block('median3'); + $afterMed = $this->block('after_median'); + $b->branchIf($b->icmp(Builder::INT_NE, $b->lShr($n, $this->c(10)), $this->c(0)), $big, $med3); + $b->positionAtEnd($big); + $delta = $b->lShr($offset, $this->c(1)); + $this->sort5($start, $b->add($start, $delta), $piv, $b->add($piv, $delta), $b->sub($end, $this->c(1))); + $b->branch($afterMed); + $b->positionAtEnd($med3); + $this->sort3($start, $piv, $b->sub($end, $this->c(1))); + $b->branch($afterMed); + + $b->positionAtEnd($afterMed); + $start = $b->load($base); + $end = $b->add($start, $b->load($nmemb)); + $piv = $b->add($start, $b->lShr($b->load($nmemb), $this->c(1))); + $this->swap($b->add($start, $this->c(1)), $piv); + $b->store($b->add($start, $this->c(1)), $pivot); + $b->store($b->add($start, $this->c(2)), $i); + $b->store($b->sub($end, $this->c(1)), $j); + $scanI = $this->block('scan_i'); + $scanIStep = $this->block('scan_i_step'); + $afterI = $this->block('after_i'); + $scanJ = $this->block('scan_j'); + $scanJStep = $this->block('scan_j_step'); + $afterJ = $this->block('after_j'); + $partDone = $this->block('part_done'); + $b->branch($scanI); + + // while (cmp(pivot, i) > 0) { i++; if (i == j) goto done; } + $b->positionAtEnd($scanI); + $this->gtThen($b->load($pivot), $b->load($i), $scanIStep, $afterI); + $b->positionAtEnd($scanIStep); + $b->store($b->add($b->load($i), $this->c(1)), $i); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($i), $b->load($j)), $partDone, $scanI); + + // j--; if (j == i) goto done; + $b->positionAtEnd($afterI); + $b->store($b->sub($b->load($j), $this->c(1)), $j); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($j), $b->load($i)), $partDone, $scanJ); + + // while (cmp(j, pivot) > 0) { j--; if (j == i) goto done; } + $b->positionAtEnd($scanJ); + $this->gtThen($b->load($j), $b->load($pivot), $scanJStep, $afterJ); + $b->positionAtEnd($scanJStep); + $b->store($b->sub($b->load($j), $this->c(1)), $j); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($j), $b->load($i)), $partDone, $scanJ); + + // swp(i, j); i++; if (i == j) goto done; + $b->positionAtEnd($afterJ); + $this->swap($b->load($i), $b->load($j)); + $b->store($b->add($b->load($i), $this->c(1)), $i); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($i), $b->load($j)), $partDone, $scanI); + + // swp(pivot, i - 1); recurse on the smaller side first, loop on the larger. + $b->positionAtEnd($partDone); + $iv = $b->load($i); + $this->swap($b->load($pivot), $b->sub($iv, $this->c(1))); + $start = $b->load($base); + $end = $b->add($start, $b->load($nmemb)); + $leftN = $b->sub($b->sub($iv, $start), $this->c(1)); + $rightN = $b->sub($end, $iv); + $leftFirst = $b->icmp(Builder::INT_SLT, $b->sub($b->sub($iv, $this->c(1)), $start), $rightN); + // push the deferred part, continue with the one php-src recurses into + $deferBase = $b->select($leftFirst, $iv, $start); + $deferN = $b->select($leftFirst, $rightN, $leftN); + $nowBase = $b->select($leftFirst, $start, $iv); + $nowN = $b->select($leftFirst, $leftN, $rightN); + $top = $b->load($sp); + $b->store($deferBase, $b->gep($stack, $b->mul($top, $this->c(2)))); + $b->store($deferN, $b->gep($stack, $b->add($b->mul($top, $this->c(2)), $this->c(1)))); + $b->store($b->add($top, $this->c(1)), $sp); + $b->store($nowBase, $base); + $b->store($nowN, $nmemb); + $b->branch($loopTop); + + $b->positionAtEnd($pop); + $finish = $this->block('finish'); + $popOne = $this->block('pop_one'); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($sp), $this->c(0)), $finish, $popOne); + $b->positionAtEnd($popOne); + $top = $b->sub($b->load($sp), $this->c(1)); + $b->store($top, $sp); + $b->store($b->load($b->gep($stack, $b->mul($top, $this->c(2)))), $base); + $b->store($b->load($b->gep($stack, $b->add($b->mul($top, $this->c(2)), $this->c(1)))), $nmemb); + $b->branch($loopTop); + + // Apply the permutation through a scratch copy. + $b->positionAtEnd($finish); + $elemSize = $b->ptrToInt($b->gep($values->typeOf()->constNull(), $this->c(1)), $i64); + $bytes = $b->mul($count, $elemSize); + $scratch = $b->pointerCast($b->call($malloc, $bytes), $values->typeOf()); + LibcExtern::ensureMemcpyDecl($this->context); + $memcpy = $this->context->lookupFunction('memcpy'); + $this->loop('gather', $this->c(0), $count, function (Value $x) use ($values, $scratch, $memcpy, $elemSize, $i8p): void { + $src = $this->b->gep($values, $this->b->load($this->b->gep($this->perm, $x))); + $this->b->call( + $memcpy, + $this->b->pointerCast($this->b->gep($scratch, $x), $i8p), + $this->b->pointerCast($src, $i8p), + $elemSize + ); + }); + $b->call($memcpy, $b->pointerCast($values, $i8p), $b->pointerCast($scratch, $i8p), $bytes); + $b->call($free, $b->pointerCast($scratch, $i8p)); + $b->call($free, $b->pointerCast($this->perm, $i8p)); + $b->call($free, $b->pointerCast($stack, $i8p)); + $b->branch($done); + + $b->positionAtEnd($done); + } + + /** Branch to $then if cmp(x, y) > 0 (with tiebreak), else to $else. */ + private function gtThen(Value $x, Value $y, \PHPLLVM\BasicBlock $then, \PHPLLVM\BasicBlock $else): void + { + $this->b->branchIf($this->gt($x, $y), $then, $else); + } + + /** i1: cmp(values[perm[x]], values[perm[y]]) > 0, ties by original position. */ + private function gt(Value $x, Value $y): Value + { + $b = $this->b; + $ix = $b->load($b->gep($this->perm, $x)); + $iy = $b->load($b->gep($this->perm, $y)); + $res = $b->call($this->cmpFn, $b->gep($this->values, $ix), $b->gep($this->values, $iy), $this->extra); + + return $b->bitwiseOr( + $b->icmp(Builder::INT_SGT, $res, $this->c(0)), + $b->bitwiseAnd($b->icmp(Builder::INT_EQ, $res, $this->c(0)), $b->icmp(Builder::INT_SGT, $ix, $iy)) + ); + } + + /** `internal i64 _cmp(T* a, T* b, i8* extra)` around the caller's compare emitter. */ + private function emitComparator(\PHPLLVM\Type $elemPtrTy, callable $compare): Value\Function_ + { + static $seq = 0; + $name = '__phpc_zsort_cmp_'.$this->tag.'_'.(++$seq); + $fn = $this->context->module->addFunction($name, $this->context->context->functionType( + $this->i64, + false, + $elemPtrTy, + $elemPtrTy, + $this->context->getTypeFromString('int8*') + )); + $this->context->llvm->lib->LLVMSetLinkage($fn->value, \llvm\llvm::LLVMInternalLinkage); + $saved = BasicBlockHelper::tryGetInsertBlock($this->context); + $this->b->positionAtEnd($fn->appendBasicBlock('entry')); + $res = $compare($fn->getParam(0), $fn->getParam(1), $fn->getParam(2)); + $this->b->returnValue($this->b->intCast($res, $this->i64)); + if (null !== $saved) { + $this->b->positionAtEnd($saved); + } + + return $fn; + } + + private function swap(Value $x, Value $y): void + { + $b = $this->b; + $px = $b->gep($this->perm, $x); + $py = $b->gep($this->perm, $y); + $vx = $b->load($px); + $vy = $b->load($py); + $b->store($vy, $px); + $b->store($vx, $py); + } + + /** if (gt(x, y)) swp(x, y) */ + private function sort2(Value $x, Value $y): void + { + $sw = $this->block('s2_swap'); + $out = $this->block('s2_out'); + $this->gtThen($x, $y, $sw, $out); + $this->b->positionAtEnd($sw); + $this->swap($x, $y); + $this->b->branch($out); + $this->b->positionAtEnd($out); + } + + private function sort3(Value $a, Value $bb, Value $c): void + { + $b = $this->b; + $out = $this->block('s3_out'); + $abGt = $this->block('s3_ab_gt'); + $abLe = $this->block('s3_ab_le'); + $this->gtThen($a, $bb, $abGt, $abLe); + + // !(a > b): if !(b > c) return; swp(b,c); if (a > b) swp(a,b) + $b->positionAtEnd($abLe); + $bcGt = $this->block('s3_bc_gt'); + $this->gtThen($bb, $c, $bcGt, $out); + $b->positionAtEnd($bcGt); + $this->swap($bb, $c); + $this->sort2($a, $bb); + $b->branch($out); + + // a > b: if !(c > b) { swp(a,c); return; } swp(a,b); if (b > c) swp(b,c) + $b->positionAtEnd($abGt); + $cbGt = $this->block('s3_cb_gt'); + $cbLe = $this->block('s3_cb_le'); + $this->gtThen($c, $bb, $cbGt, $cbLe); + $b->positionAtEnd($cbLe); + $this->swap($a, $c); + $b->branch($out); + $b->positionAtEnd($cbGt); + $this->swap($a, $bb); + $this->sort2($bb, $c); + $b->branch($out); + + $b->positionAtEnd($out); + } + + /** zend_sort_4 / zend_sort_5 tail: bubble the new last element down while out of order. */ + private function sinkLast(array $pos): void + { + $b = $this->b; + $out = $this->block('sink_out'); + for ($m = \count($pos) - 1; $m >= 1; --$m) { + $sw = $this->block('sink_swap'); + $this->gtThen($pos[$m - 1], $pos[$m], $sw, $out); + $b->positionAtEnd($sw); + $this->swap($pos[$m - 1], $pos[$m]); + } + $b->branch($out); + $b->positionAtEnd($out); + } + + private function sort4(Value $a, Value $bb, Value $c, Value $d): void + { + $this->sort3($a, $bb, $c); + $this->sinkLast([$a, $bb, $c, $d]); + } + + private function sort5(Value $a, Value $bb, Value $c, Value $d, Value $e): void + { + $this->sort4($a, $bb, $c, $d); + $this->sinkLast([$a, $bb, $c, $d, $e]); + } + + /** zend_insert_sort(base, n) */ + private function insertSort(Value $start, Value $n, Value $i, Value $j, Value $k): void + { + $b = $this->b; + $out = $this->block('ins_out'); + $general = $this->block('ins_general'); + $cases = []; + foreach ([2, 3, 4, 5] as $size) { + $cases[$size] = $this->block('ins_n'.$size); + } + $sw = $b->branchSwitch($n, $general, 6); + $sw->addCase($this->c(0), $out); + $sw->addCase($this->c(1), $out); + foreach ($cases as $size => $blk) { + $sw->addCase($this->c($size), $blk); + } + $p = fn (int $o): Value => $b->add($start, $this->c($o)); + $b->positionAtEnd($cases[2]); + $this->sort2($p(0), $p(1)); + $b->branch($out); + $b->positionAtEnd($cases[3]); + $this->sort3($p(0), $p(1), $p(2)); + $b->branch($out); + $b->positionAtEnd($cases[4]); + $this->sort4($p(0), $p(1), $p(2), $p(3)); + $b->branch($out); + $b->positionAtEnd($cases[5]); + $this->sort5($p(0), $p(1), $p(2), $p(3), $p(4)); + $b->branch($out); + + $b->positionAtEnd($general); + $end = $b->add($start, $n); + $sentry = $b->add($start, $this->c(6)); + + // first pass: i in [start+1, sentry), linear probe + $b->store($b->add($start, $this->c(1)), $i); + $h1 = $this->block('ins1_head'); + $body1 = $this->block('ins1_body'); + $next1 = $this->block('ins1_next'); + $probe1 = $this->block('ins1_probe'); + $probeStep1 = $this->block('ins1_probe_step'); + $placed1 = $this->block('ins1_placed'); + $pass2 = $this->block('ins2_init'); + $b->branch($h1); + $b->positionAtEnd($h1); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($i), $sentry), $body1, $pass2); + $b->positionAtEnd($body1); + $b->store($b->sub($b->load($i), $this->c(1)), $j); + $this->gtThen($b->load($j), $b->load($i), $probe1, $next1); + // while (j != start) { j--; if (!(cmp(j,i) > 0)) { j++; break; } } + $b->positionAtEnd($probe1); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($j), $start), $placed1, $probeStep1); + $b->positionAtEnd($probeStep1); + $b->store($b->sub($b->load($j), $this->c(1)), $j); + $stop1 = $this->block('ins1_stop'); + $this->gtThen($b->load($j), $b->load($i), $probe1, $stop1); + $b->positionAtEnd($stop1); + $b->store($b->add($b->load($j), $this->c(1)), $j); + $b->branch($placed1); + $b->positionAtEnd($placed1); + $this->rotateDown($i, $j, $k); + $b->branch($next1); + $b->positionAtEnd($next1); + $b->store($b->add($b->load($i), $this->c(1)), $i); + $b->branch($h1); + + // second pass: i in [sentry, end), 2-step probe + $b->positionAtEnd($pass2); + $b->store($sentry, $i); + $h2 = $this->block('ins2_head'); + $body2 = $this->block('ins2_body'); + $next2 = $this->block('ins2_next'); + $probe2 = $this->block('ins2_probe'); + $placed2 = $this->block('ins2_placed'); + $b->branch($h2); + $b->positionAtEnd($h2); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($i), $end), $body2, $out); + $b->positionAtEnd($body2); + $b->store($b->sub($b->load($i), $this->c(1)), $j); + $this->gtThen($b->load($j), $b->load($i), $probe2, $next2); + + // do { j -= 2; ... } while (1) + $b->positionAtEnd($probe2); + $b->store($b->sub($b->load($j), $this->c(2)), $j); + $le = $this->block('ins2_le'); + $chkStart = $this->block('ins2_chk_start'); + $this->gtThen($b->load($j), $b->load($i), $chkStart, $le); + // !(cmp(j,i) > 0): j++; if (!(cmp(j,i) > 0)) j++; break; + $b->positionAtEnd($le); + $b->store($b->add($b->load($j), $this->c(1)), $j); + $bump = $this->block('ins2_bump'); + $this->gtThen($b->load($j), $b->load($i), $placed2, $bump); + $b->positionAtEnd($bump); + $b->store($b->add($b->load($j), $this->c(1)), $j); + $b->branch($placed2); + // if (j == start) break; + $b->positionAtEnd($chkStart); + $chkStart1 = $this->block('ins2_chk_start1'); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($j), $start), $placed2, $chkStart1); + // if (j == start + 1) { j--; if (cmp(i, j) > 0) j++; break; } + $b->positionAtEnd($chkStart1); + $atOne = $this->block('ins2_at_one'); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($j), $b->add($start, $this->c(1))), $atOne, $probe2); + $b->positionAtEnd($atOne); + $b->store($b->sub($b->load($j), $this->c(1)), $j); + $inc = $this->block('ins2_at_one_inc'); + $this->gtThen($b->load($i), $b->load($j), $inc, $placed2); + $b->positionAtEnd($inc); + $b->store($b->add($b->load($j), $this->c(1)), $j); + $b->branch($placed2); + + $b->positionAtEnd($placed2); + $this->rotateDown($i, $j, $k); + $b->branch($next2); + $b->positionAtEnd($next2); + $b->store($b->add($b->load($i), $this->c(1)), $i); + $b->branch($h2); + + $b->positionAtEnd($out); + } + + /** for (k = i; k > j; k--) swp(k, k - 1); */ + private function rotateDown(Value $i, Value $j, Value $k): void + { + $b = $this->b; + $b->store($b->load($i), $k); + $head = $this->block('rot_head'); + $body = $this->block('rot_body'); + $out = $this->block('rot_out'); + $b->branch($head); + $b->positionAtEnd($head); + $b->branchIf($b->icmp(Builder::INT_SGT, $b->load($k), $b->load($j)), $body, $out); + $b->positionAtEnd($body); + $kv = $b->load($k); + $this->swap($kv, $b->sub($kv, $this->c(1))); + $b->store($b->sub($kv, $this->c(1)), $k); + $b->branch($head); + $b->positionAtEnd($out); + } + + /** for (x = from; x < to; x++) $body(x) */ + private function loop(string $name, Value $from, Value $to, callable $body): void + { + $b = $this->b; + $x = $this->slot($from); + $head = $this->block($name.'_head'); + $bodyBlock = $this->block($name.'_body'); + $out = $this->block($name.'_out'); + $b->branch($head); + $b->positionAtEnd($head); + $b->branchIf($b->icmp(Builder::INT_SLT, $b->load($x), $to), $bodyBlock, $out); + $b->positionAtEnd($bodyBlock); + $xv = $b->load($x); + $body($xv); + $b->store($b->add($xv, $this->c(1)), $x); + $b->branch($head); + $b->positionAtEnd($out); + } +} diff --git a/test/differential/cases/numeric_string_arith_parity.php b/test/differential/cases/numeric_string_arith_parity.php new file mode 100644 index 00000000000..289356aad9b --- /dev/null +++ b/test/differential/cases/numeric_string_arith_parity.php @@ -0,0 +1,6 @@ +getMessage(),"\n";} diff --git a/test/differential/cases/operand_typeerror_runtime_names.php b/test/differential/cases/operand_typeerror_runtime_names.php new file mode 100644 index 00000000000..8d2781706ec --- /dev/null +++ b/test/differential/cases/operand_typeerror_runtime_names.php @@ -0,0 +1,6 @@ +getMessage(), "\n"; } +} diff --git a/test/differential/cases/pow_int_overflow_parity.php b/test/differential/cases/pow_int_overflow_parity.php new file mode 100644 index 00000000000..b836aba6359 --- /dev/null +++ b/test/differential/cases/pow_int_overflow_parity.php @@ -0,0 +1,7 @@ +