diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 8acc192b51..bc5ff413b1 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 | 8592 | -| Phase A inventory files (M2 ratio SSOT) | 8592 | +| PHP files on vm.php path | 8593 | +| Phase A inventory files (M2 ratio SSOT) | 8593 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 26785 | +| Source constructs flagged (warnings) | 26789 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -7602,10 +7602,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/UnserializeObjectDecodeLlvm.php` | 0 | 1 | | `lib/JIT/UnsetHelper.php` | 0 | 1 | | `lib/JIT/UnsetHelperLlvm.php` | 0 | 4 | +| `lib/JIT/UserCallableResolver.php` | 0 | 1 | | `lib/JIT/UserScriptAotEnv.php` | 0 | 1 | | `lib/JIT/UsortCallbackPolicy.php` | 0 | 1 | | `lib/JIT/UsortKeyedLlvm.php` | 0 | 1 | -| `lib/JIT/UsortPackedLlvm.php` | 0 | 2 | +| `lib/JIT/UsortPackedLlvm.php` | 0 | 5 | | `lib/JIT/ValueBoxDimWrite.php` | 0 | 1 | | `lib/JIT/ValueEchoHelper.php` | 0 | 2 | | `lib/JIT/ValueSortKeyedLlvm.php` | 0 | 2 | @@ -51908,7 +51909,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/ArrayMapRuntime.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 405) +- new JITVariable (line 409) - 26 class method(s) ### `lib/JIT/Builtin/ArrayMergeRecursiveRuntime.php` @@ -62415,6 +62416,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - 13 class method(s) - 2 closure(s) +### `lib/JIT/UserCallableResolver.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) + ### `lib/JIT/UserScriptAotEnv.php` **Warnings** (review for bootstrap subset): @@ -62433,8 +62439,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/UsortPackedLlvm.php` **Warnings** (review for bootstrap subset): -- new NestedClosureInvoke (line 126) -- 4 class method(s) +- new Variable (line 43) +- new NestedClosureInvoke (line 46) +- new Variable (line 105) +- 9 class method(s) +- 5 closure(s) ### `lib/JIT/ValueBoxDimWrite.php` @@ -62629,9 +62638,9 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/ZendSortLlvm.php` **Warnings** (review for bootstrap subset): -- new self (line 57) +- new self (line 64) - 17 class method(s) -- 3 closure(s) +- 4 closure(s) ### `lib/JIT/ZipExtensionHooks.php` diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index 7a9ffedba8..452098acf6 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -7694,6 +7694,7 @@ "lib/JIT/UnserializeObjectDecodeLlvm.php", "lib/JIT/UnsetHelper.php", "lib/JIT/UnsetHelperLlvm.php", + "lib/JIT/UserCallableResolver.php", "lib/JIT/UserScriptAotEnv.php", "lib/JIT/UsortCallbackPolicy.php", "lib/JIT/UsortKeyedLlvm.php", @@ -8826,9 +8827,9 @@ "test/bootstrap-aot/lib_opcode/main.php" ], "totals": { - "inventory_files": 8592, + "inventory_files": 8593, "excluded": 0, - "eligible": 8592, + "eligible": 8593, "aot_lint_targets": 113, "aot_link_targets": 100, "aot_link_lib_targets": 1 diff --git a/lib/JIT/Builtin/ArrayMapRuntime.php b/lib/JIT/Builtin/ArrayMapRuntime.php index 865b71831c..1b8f83c2ef 100644 --- a/lib/JIT/Builtin/ArrayMapRuntime.php +++ b/lib/JIT/Builtin/ArrayMapRuntime.php @@ -111,6 +111,10 @@ public static function mapSingle(Context $context, JITVariable $callback, JITVar $callback->compileTimeString )) { $name = $callback->compileTimeString; + $userFn = \PHPCompiler\JIT\UserCallableResolver::userFunctionCall($context, $callback); + if (null !== $userFn) { + return ArrayMapLlvm::mapStaticMethod($context, $ht, $userFn); + } if (null !== $name) { return ArrayMapLlvm::mapBuiltin($context, $ht, $name); } diff --git a/lib/JIT/Builtin/UsortRuntime.php b/lib/JIT/Builtin/UsortRuntime.php index 483a55f29b..4cf4770ac1 100644 --- a/lib/JIT/Builtin/UsortRuntime.php +++ b/lib/JIT/Builtin/UsortRuntime.php @@ -47,6 +47,13 @@ final class UsortRuntime public static function usortPacked(Context $context, JITVariable $array, JITVariable $callback): Value { + $userFn = \PHPCompiler\JIT\UserCallableResolver::userFunctionCall($context, $callback); + if (null !== $userFn && !ArrayBuiltinHelper::isNativeArray($array->type)) { + $ht = HashTableHelper::separateContainerForWrite($context, $array); + \PHPCompiler\JIT\UsortPackedLlvm::sortPackedWithFunction($context, $ht, $userFn); + + return $context->getTypeFromString('int1')->constInt(1, false); + } UsortCallbackPolicy::assertJitLowerable($context, $callback); if (UsortCallbackPolicy::isClosureJitLowerable($callback)) { self::sortPackedWithClosure($context, $array, $callback); diff --git a/lib/JIT/UserCallableResolver.php b/lib/JIT/UserCallableResolver.php new file mode 100644 index 0000000000..7c4c6f17f8 --- /dev/null +++ b/lib/JIT/UserCallableResolver.php @@ -0,0 +1,35 @@ +compileTimeString; + if (!\is_string($name) || '' === $name || str_contains($name, '::')) { + return null; + } + $lc = strtolower(ltrim($name, '\\')); + if (null !== \PHPCompiler\ext\standard\BuiltinRegistry::resolve($lc)) { + return null; + } + if (!$context->functionIsRegistered($lc)) { + return null; + } + $call = $context->resolveFunctionProxy($lc); + + return $call instanceof Call\Native ? $call : null; + } +} diff --git a/lib/JIT/UsortPackedLlvm.php b/lib/JIT/UsortPackedLlvm.php index 9244ecfb71..6677e25285 100644 --- a/lib/JIT/UsortPackedLlvm.php +++ b/lib/JIT/UsortPackedLlvm.php @@ -4,203 +4,265 @@ namespace PHPCompiler\JIT; +use PHPCompiler\JIT\Builtin\NumericStringLlvm; use PHPCompiler\JIT\Call\NestedClosureInvoke; use PHPLLVM\Builder; -use PHPLLVM\Type; use PHPLLVM\Value; /** - * Pure LLVM usort() for thin standalone AOT Closure comparators (#26954 / #27217 peer). + * usort() on a packed list with a user comparator, php-src exact. * - * NestedJIT of {@see \PHPCompiler\ext\standard\UsortJitHelper} never invokes the comparator - * under thin AOT (variadic VmClosureInvoke::invokeVariable). Bubble-sort packed slots in place - * via {@see NestedClosureInvoke} — same class of fix as {@see ArrayWalkLlvm} (#27632). + * ext/standard/array.c php_usort -> zend_array_sort(arr, php_array_user_compare, renumber) + * -> zend_sort ({@see ZendSortLlvm}) with php_array_user_compare_unstable: + * the comparator's return goes through zval_get_long and ZEND_NORMALIZE_BOOL (so 0.5 is 0); + * a bool return raises the "Returning bool from comparison function is deprecated" + * E_DEPRECATED once per call, and `false` retries with swapped operands + * (-ZEND_NORMALIZE_BOOL(zval_get_long(cmp(b, a)))); ties fall back to the original + * position (RETURN_STABLE_SORT). * - * php-src: ext/standard/array.c — php_array_usort + * The comparator may be a Closure (passed to the comparator function as its object + * pointer, dispatched by its runtime target) or a user function resolved at compile time. + * (Was: bubble sort capped at n² passes, double results by sign, no bool handling.) */ final class UsortPackedLlvm { + private const DEPRECATION = 'usort(): Returning bool from comparison function is deprecated, ' + .'return an integer less than, equal to, or greater than zero'; + public static function sortPackedWithClosure(Context $context, Value $ht, Variable $closure): void { NestedClosureInvokeLlvm::ensureLinked($context); BasicBlockHelper::ensureOpenInsertBlock($context, 'usort_packed_llvm_cont'); - self::emitSortBody($context, $ht, $closure); + $objPtr = self::closureObjectPtr($context, $closure); + $closureCall = $closure->closureCall; + $objTy = $context->getTypeFromString('__object__*'); + self::emitSort( + $context, + $ht, + static function (Context $ctx, Value $extra, Variable $a, Variable $b) use ($closureCall, $objTy): Value { + $rebound = new Variable($ctx, Variable::TYPE_OBJECT, Variable::KIND_VALUE, $ctx->builder->pointerCast($extra, $objTy)); + $rebound->closureCall = $closureCall; + + return (new NestedClosureInvoke())->call($ctx, $rebound, $a, $b); + }, + $objPtr, + true + ); + } + + /** usort() with a user function resolved at compile time (`usort($a, 'cmp')`). */ + public static function sortPackedWithFunction(Context $context, Value $ht, Call $function): void + { + BasicBlockHelper::ensureOpenInsertBlock($context, 'usort_packed_fn_cont'); + self::emitSort( + $context, + $ht, + static fn (Context $ctx, Value $extra, Variable $a, Variable $b): Value => JitValueBox::coerceToValuePtrForStore( + $ctx, + $function->call($ctx, $a, $b) + ), + null, + false + ); } - private static function emitSortBody(Context $context, Value $ht, Variable $closure): void + /** + * @param callable(Context, Value, Variable, Variable): Value $invoke returns the comparator + * result as __value__*; runs inside the comparator function + */ + private static function emitSort(Context $context, Value $ht, callable $invoke, ?Value $extra, bool $inline): void { + $b = $context->builder; $map = $context->structFieldMap['__hashtable__']; - $sizeT = $context->getTypeFromString('size_t'); - $i1 = $context->getTypeFromString('int1'); - $i64 = $context->getTypeFromString('int64'); - $valueType = $context->getTypeFromString('__value__'); - $zero = $sizeT->constInt(0, false); - $one = $sizeT->constInt(1, false); - - $n = $context->builder->load($context->builder->structGep($ht, $map['numElements'])); - $done = BasicBlockHelper::append($context, 'usort_packed_done'); - $work = BasicBlockHelper::append($context, 'usort_packed_work'); - $tooSmall = $context->builder->icmp( - Builder::INT_ULT, + $i8 = $context->getTypeFromString('int8'); + $flag = self::deprecationFlag($context); + $b->store($i8->constInt(0, false), $flag); + $values = $b->load($b->structGep($ht, $map['values'])); + $n = $b->load($b->structGep($ht, $map['numElements'])); + ZendSortLlvm::emit( + $context, + $values, $n, - $sizeT->constInt(2, false) - ); - $context->builder->branchIf($tooSmall, $done, $work); - - $context->builder->positionAtEnd($work); - $maxPasses = $context->builder->mul($n, $n); - $passCountSlot = BasicBlockHelper::entryAlloca($context, $sizeT); - $context->builder->store($zero, $passCountSlot); - $swappedSlot = BasicBlockHelper::entryAlloca($context, $i1); - $context->builder->store($i1->constInt(1, false), $swappedSlot); - - $passHead = BasicBlockHelper::append($context, 'usort_packed_pass_head'); - $passBody = BasicBlockHelper::append($context, 'usort_packed_pass_body'); - $passExit = BasicBlockHelper::append($context, 'usort_packed_pass_exit'); - $context->builder->branch($passHead); - - $context->builder->positionAtEnd($passHead); - $didSwap = $context->builder->load($swappedSlot); - $passCount = $context->builder->load($passCountSlot); - $overCap = $context->builder->icmp(Builder::INT_UGE, $passCount, $maxPasses); - $cont = $context->builder->and( - $didSwap, - $context->builder->icmp(Builder::INT_EQ, $overCap, $i1->constInt(0, false)) + static fn (Value $x, Value $y, Value $extraIn): Value => self::emitCompare($context, $invoke, $extraIn, $x, $y, $flag), + 'usort', + $extra, + $inline ); - $context->builder->branchIf($cont, $passBody, $done); + } - $context->builder->positionAtEnd($passBody); - $context->builder->store( - $context->builder->addNoSignedWrap($passCount, $one), - $passCountSlot - ); - $context->builder->store($i1->constInt(0, false), $swappedSlot); - $limit = $context->builder->sub($n, $one); - $iSlot = BasicBlockHelper::entryAlloca($context, $sizeT); - $context->builder->store($zero, $iSlot); - $walkHead = BasicBlockHelper::append($context, 'usort_packed_walk_head'); - $walkBody = BasicBlockHelper::append($context, 'usort_packed_walk_body'); - $context->builder->branch($walkHead); - - $context->builder->positionAtEnd($walkHead); - $i = $context->builder->load($iSlot); - $atEnd = $context->builder->icmp(Builder::INT_UGE, $i, $limit); - $context->builder->branchIf($atEnd, $passExit, $walkBody); - - $context->builder->positionAtEnd($walkBody); - $j = $context->builder->addNoSignedWrap($i, $one); - $leftBox = HashTableReadLlvm::readIndexedToValueBox($context, $ht, $i); - $rightBox = HashTableReadLlvm::readIndexedToValueBox($context, $ht, $j); - $cmpLong = self::compareWithClosure($context, $closure, $leftBox, $rightBox); - $needsSwap = $context->builder->icmp( - Builder::INT_SGT, - $cmpLong, - $i64->constInt(0, false) + /** php_array_user_compare_unstable, inside the comparator function. */ + private static function emitCompare(Context $context, callable $invoke, Value $extra, Value $x, Value $y, Value $flag): Value + { + $b = $context->builder; + $fn = BasicBlockHelper::parentFunction($context); + $i8 = $context->getTypeFromString('int8'); + $i64 = $context->getTypeFromString('int64'); + $out = BasicBlockHelper::entryAlloca($context, $i64); + $argBox = static function (Value $elem) use ($context): Variable { + $slot = JitValueBox::alloc($context); + JitValueBox::copyFromPointer($context, $slot, $elem); + + return new Variable($context, Variable::TYPE_VALUE, Variable::KIND_VARIABLE, $slot); + }; + $ret = $invoke($context, $extra, $argBox($x), $argBox($y)); + $kind = $b->and( + $b->load($b->structGep($ret, $context->structFieldMap['__value__']['type'])), + $i8->constInt(0x7f, false) ); - $swapBlock = BasicBlockHelper::append($context, 'usort_packed_swap'); - $advance = BasicBlockHelper::append($context, 'usort_packed_advance'); - $context->builder->branchIf($needsSwap, $swapBlock, $advance); + $isBool = $b->icmp(Builder::INT_EQ, $kind, $i8->constInt(Variable::TYPE_NATIVE_BOOL, false)); + $boolPath = $fn->appendBasicBlock('usort_cmp_bool'); + $plain = $fn->appendBasicBlock('usort_cmp_plain'); + $done = $fn->appendBasicBlock('usort_cmp_done'); + $b->branchIf($isBool, $boolPath, $plain); - $context->builder->positionAtEnd($swapBlock); - self::swapPackedEntriesAt($context, $ht, $i, $j, $valueType); - $context->builder->store($i1->constInt(1, false), $swappedSlot); - $context->builder->branch($advance); + $b->positionAtEnd($plain); + $b->store(self::normalize($context, self::zvalGetLong($context, $ret)), $out); + $b->branch($done); - $context->builder->positionAtEnd($advance); - $context->builder->store($context->builder->addNoSignedWrap($i, $one), $iSlot); - $context->builder->branch($walkHead); + $b->positionAtEnd($boolPath); + $warn = $fn->appendBasicBlock('usort_cmp_deprecate'); + $afterWarn = $fn->appendBasicBlock('usort_cmp_after_deprecate'); + $b->branchIf($b->icmp(Builder::INT_EQ, $b->load($flag), $i8->constInt(0, false)), $warn, $afterWarn); + $b->positionAtEnd($warn); + $b->store($i8->constInt(1, false), $flag); + self::emitDeprecation($context); + $b->branch($afterWarn); + $b->positionAtEnd($afterWarn); + $isFalse = $b->icmp( + Builder::INT_EQ, + $b->load($b->pointerCast( + $b->structGep($ret, $context->structFieldMap['__value__']['value']), + $context->getTypeFromString('int8*') + )), + $i8->constInt(0, false) + ); + $retry = $fn->appendBasicBlock('usort_cmp_retry_swapped'); + $b->branchIf($isFalse, $retry, $plain); + $b->positionAtEnd($retry); + $ret2 = $invoke($context, $extra, $argBox($y), $argBox($x)); + $b->store($b->sub($i64->constInt(0, false), self::normalize($context, self::zvalGetLong($context, $ret2))), $out); + $b->branch($done); - $context->builder->positionAtEnd($passExit); - $context->builder->branch($passHead); + $b->positionAtEnd($done); - $context->builder->positionAtEnd($done); + return $b->load($out); } - /** @return Value int64 compare sign for usort swap test */ - private static function compareWithClosure( - Context $context, - Variable $closure, - Variable $left, - Variable $right - ): Value { - $resultPtr = (new NestedClosureInvoke())->call($context, $closure, $left, $right); - $i64 = $context->getTypeFromString('int64'); + /** zval_get_long for a returned box: bool, int, float (truncated; 0 if out of range), numeric string, else 0. */ + private static function zvalGetLong(Context $context, Value $box): Value + { + $b = $context->builder; $i8 = $context->getTypeFromString('int8'); + $i64 = $context->getTypeFromString('int64'); $f64 = $context->getTypeFromString('double'); - $valueMap = $context->structFieldMap['__value__']; - $kind = $context->builder->and( - $context->builder->load($context->builder->structGep($resultPtr, $valueMap['type'])), - $i8->constInt(0x7f, false) + $fn = BasicBlockHelper::parentFunction($context); + $map = $context->structFieldMap['__value__']; + $kind = $b->and($b->load($b->structGep($box, $map['type'])), $i8->constInt(0x7f, false)); + $out = BasicBlockHelper::entryAlloca($context, $i64); + $payload8 = $b->load($b->pointerCast($b->structGep($box, $map['value']), $context->getTypeFromString('int8*'))); + $long = $b->call($context->lookupFunction('__value__readLong'), $box); + $dbl = $b->call($context->lookupFunction('__value__readDouble'), $box); + $dblFits = $b->and( + $b->fcmp(Builder::REAL_OGE, $dbl, $f64->constReal(-9223372036854775808.0)), + $b->fcmp(Builder::REAL_OLT, $dbl, $f64->constReal(9223372036854775808.0)) ); - $fn = $context->builder->getInsertBlock()->getParent(); - $longBb = $fn->appendBasicBlock('usort_cmp_long'); - $dblCheck = $fn->appendBasicBlock('usort_cmp_dbl_check'); - $dblBody = $fn->appendBasicBlock('usort_cmp_dbl_body'); - $zeroBb = $fn->appendBasicBlock('usort_cmp_zero'); - $join = $fn->appendBasicBlock('usort_cmp_join'); - $resultSlot = BasicBlockHelper::entryAlloca($context, $i64); - $zero = $i64->constInt(0, false); - - $isLong = $context->builder->icmp( - Builder::INT_EQ, - $kind, - $i8->constInt(Variable::TYPE_NATIVE_LONG & 0x7f, false) + $dblLong = $b->select($dblFits, $b->fpToSi($dbl, $i64), $i64->constInt(0, false)); + $isKind = static fn (int $t): Value => $b->icmp(Builder::INT_EQ, $kind, $i8->constInt($t, false)); + $scalar = $b->select( + $isKind(Variable::TYPE_NATIVE_LONG), + $long, + $b->select( + $isKind(Variable::TYPE_NATIVE_DOUBLE), + $dblLong, + $b->select( + $isKind(Variable::TYPE_NATIVE_BOOL), + $b->zExt($b->icmp(Builder::INT_NE, $payload8, $i8->constInt(0, false)), $i64), + $i64->constInt(0, false) + ) + ) ); - $isDouble = $context->builder->icmp( - Builder::INT_EQ, - $kind, - $i8->constInt(Variable::TYPE_NATIVE_DOUBLE & 0x7f, false) + $b->store($scalar, $out); + $str = $fn->appendBasicBlock('zgl_string'); + $done = $fn->appendBasicBlock('zgl_done'); + $b->branchIf($isKind(Variable::TYPE_STRING & 0x7f), $str, $done); + $b->positionAtEnd($str); + $num = JitValueBox::pointer($context, JitValueBox::alloc($context)); + $status = NumericStringLlvm::call($context, $b->call($context->lookupFunction('__value__readString'), $box), $num); + $numKind = $b->load($b->structGep($num, $map['type'])); + $numDbl = $b->call($context->lookupFunction('__value__readDouble'), $num); + $numFits = $b->and( + $b->fcmp(Builder::REAL_OGE, $numDbl, $f64->constReal(-9223372036854775808.0)), + $b->fcmp(Builder::REAL_OLT, $numDbl, $f64->constReal(9223372036854775808.0)) ); - $context->builder->branchIf($isLong, $longBb, $dblCheck); - - $context->builder->positionAtEnd($longBb); - $context->builder->store( - $context->builder->call($context->lookupFunction('__value__readLong'), $resultPtr), - $resultSlot + $numLong = $b->select( + $b->icmp(Builder::INT_EQ, $numKind, $i8->constInt(Variable::TYPE_NATIVE_LONG, false)), + $b->call($context->lookupFunction('__value__readLong'), $num), + $b->select($numFits, $b->fpToSi($numDbl, $i64), $i64->constInt(0, false)) ); - $context->builder->branch($join); - - $context->builder->positionAtEnd($dblCheck); - $context->builder->branchIf($isDouble, $dblBody, $zeroBb); - - $context->builder->positionAtEnd($dblBody); - $dblVal = $context->builder->call($context->lookupFunction('__value__readDouble'), $resultPtr); - $zeroD = $f64->constReal(0.0); - $pos = $context->builder->fcmp(Builder::REAL_OGT, $dblVal, $zeroD); - $neg = $context->builder->fcmp(Builder::REAL_OLT, $dblVal, $zeroD); - $context->builder->store( - $context->builder->select( - $pos, - $i64->constInt(1, false), - $context->builder->select($neg, $i64->constInt(-1, true), $zero) + $b->store( + $b->select( + $b->icmp(Builder::INT_EQ, $status, $context->getTypeFromString('int32')->constInt(NumericStringLlvm::NON_NUMERIC, false)), + $i64->constInt(0, false), + $numLong ), - $resultSlot + $out ); - $context->builder->branch($join); + $b->branch($done); + $b->positionAtEnd($done); - $context->builder->positionAtEnd($zeroBb); - $context->builder->store($zero, $resultSlot); - $context->builder->branch($join); + return $b->load($out); + } - $context->builder->positionAtEnd($join); + private static function normalize(Context $context, Value $v): Value + { + $b = $context->builder; + $i64 = $context->getTypeFromString('int64'); + $zero = $i64->constInt(0, false); - return $context->builder->load($resultSlot); + return $b->sub($b->zExt($b->icmp(Builder::INT_SGT, $v, $zero), $i64), $b->zExt($b->icmp(Builder::INT_SLT, $v, $zero), $i64)); } - private static function swapPackedEntriesAt( - Context $context, - Value $ht, - Value $idxA, - Value $idxB, - Type $valueType - ): void { - $map = $context->structFieldMap['__hashtable__']; - $values = $context->builder->load($context->builder->structGep($ht, $map['values'])); - $entryA = $context->builder->inBoundsGep($values, $idxA); - $entryB = $context->builder->inBoundsGep($values, $idxB); - $tmp = BasicBlockHelper::entryAlloca($context, $valueType); - $context->builder->store($context->builder->load($entryA), $tmp); - $context->builder->store($context->builder->load($entryB), $entryA); - $context->builder->store($context->builder->load($tmp), $entryB); + private static function closureObjectPtr(Context $context, Variable $closure): Value + { + $i8p = $context->getTypeFromString('int8*'); + if (Variable::TYPE_OBJECT === $closure->type) { + return $context->builder->pointerCast($context->helper->loadValue($closure), $i8p); + } + + return $context->builder->pointerCast( + $context->builder->call( + $context->lookupFunction('__value__readObject'), + JitValueBox::valuePtrFromVariable($context, $closure) + ), + $i8p + ); + } + + private static function deprecationFlag(Context $context): Value + { + $name = '__phpc_usort_bool_deprecation_shown'; + $g = $context->module->getNamedGlobal($name); + if (null === $g) { + $i8 = $context->getTypeFromString('int8'); + $g = $context->module->addGlobal($i8, $name); + $g->setInitializer($i8->constInt(0, false)); + $context->llvm->lib->LLVMSetLinkage($g->value, \llvm\llvm::LLVMInternalLinkage); + } + + return $g; + } + + private static function emitDeprecation(Context $context): void + { + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $context->builder->call( + $context->lookupFunction('__compiler_trigger_error'), + $context->builder->pointerCast($context->constantFromString(self::DEPRECATION), $i8p), + $context->getTypeFromString('size_t')->constInt(\strlen(self::DEPRECATION), false), + $i32->constInt(\PHPCompiler\VM\ErrorReporter::E_DEPRECATED, false), + $context->builder->pointerCast($context->constantFromString($context->jitAotEntryScriptPath), $i8p), + $i32->constInt(max(0, $context->callSiteLine), false) + ); } } diff --git a/lib/JIT/ZendSortLlvm.php b/lib/JIT/ZendSortLlvm.php index 1d7e360ed7..c645093b24 100644 --- a/lib/JIT/ZendSortLlvm.php +++ b/lib/JIT/ZendSortLlvm.php @@ -35,7 +35,9 @@ final class ZendSortLlvm private $i64; private Value $perm; private Value $values; - private Value\Function_ $cmpFn; + private ?Value\Function_ $cmpFn = null; + /** @var callable|null inline comparator (compare emitted at each comparison site) */ + private $inlineCompare = null; private Value $extra; /** @@ -45,6 +47,10 @@ final class ZendSortLlvm * function, the three-way compare of element pointers a, b (with the caller's * $extra pointer as i8*) as an integer whose sign matters * @param Value|null $extra any pointer the comparator needs from the caller's frame + * @param bool $inline emit the comparator at every comparison site in the caller's + * function instead of a separate function — required when it references the + * caller's frame (closures with captured variables); scratch slots it needs must + * be entry allocas */ public static function emit( Context $context, @@ -52,9 +58,10 @@ public static function emit( Value $count, callable $compare, string $tag, - ?Value $extra = null + ?Value $extra = null, + bool $inline = false ): void { - (new self($context, $tag))->emitSort($values, $count, $compare, $extra); + (new self($context, $tag))->emitSort($values, $count, $compare, $extra, $inline); } private function __construct(Context $context, string $tag) @@ -86,13 +93,17 @@ private function slot(?Value $init = null): Value return $s; } - private function emitSort(Value $values, Value $countIn, callable $compare, ?Value $extra): void + private function emitSort(Value $values, Value $countIn, callable $compare, ?Value $extra, bool $inline): 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); + if ($inline) { + $this->inlineCompare = $compare; + } else { + $this->cmpFn = $this->emitComparator($values->typeOf(), $compare); + } $i64 = $this->i64; $count = $b->intCast($countIn, $i64); $done = $this->block('done'); @@ -262,7 +273,9 @@ 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); + $res = null !== $this->cmpFn + ? $b->call($this->cmpFn, $b->gep($this->values, $ix), $b->gep($this->values, $iy), $this->extra) + : $b->intCast(($this->inlineCompare)($b->gep($this->values, $ix), $b->gep($this->values, $iy), $this->extra), $this->i64); return $b->bitwiseOr( $b->icmp(Builder::INT_SGT, $res, $this->c(0)), @@ -284,9 +297,13 @@ private function emitComparator(\PHPLLVM\Type $elemPtrTy, callable $compare): Va )); $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)); + // Scope lowering to the comparator: lazily materialized helpers (first use of + // __value__copy etc.) restore the insert point relative to loweringLlvmFunction. + BasicBlockHelper::scopeLoweringToFunction($this->context, $fn, $name, function () use ($fn, $compare): void { + $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); } diff --git a/test/differential/cases/usort_zend_parity.php b/test/differential/cases/usort_zend_parity.php new file mode 100644 index 0000000000..9349bae634 --- /dev/null +++ b/test/differential/cases/usort_zend_parity.php @@ -0,0 +1,10 @@ + strlen($b); } +function dbl($x) { return $x * 2; } +$w = ["ccc", "a", "bb", "dddd", "e"]; usort($w, 'bylen'); echo implode(',', $w), "\n"; +$w = ["ccc", "a", "bb", "dddd", "e"]; usort($w, fn($x, $y) => strlen($y) - strlen($x)); echo implode(',', $w), "\n"; +$f = [3, 1, 2, 5, 4]; usort($f, fn($a, $b) => ($a - $b) / 10); echo implode(',', $f), "\n"; +$big = []; for ($i = 0; $i < 500; ++$i) { $big[] = ($i * 31) % 97; } usort($big, fn($a, $b) => $a <=> $b); echo $big[0], ' ', $big[250], ' ', $big[499], "\n"; +$o = []; for ($i = 0; $i < 30; ++$i) { $o[] = ['k' => $i % 7, 'id' => $i]; } usort($o, fn($a, $b) => $a['k'] <=> $b['k']); echo implode(',', array_map(fn($r) => $r['id'], $o)), "\n"; +echo implode(',', array_map('dbl', [1, 2, 3])), "\n"; diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index 3854c849bc..4b3be782c7 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -8697,6 +8697,7 @@ require_once __DIR__.'/../../../lib/JIT/IntRadixSortLlvm.php'; require_once __DIR__.'/../../../lib/JIT/MergeSortLlvm.php'; require_once __DIR__.'/../../../lib/JIT/ZendSortLlvm.php'; +require_once __DIR__.'/../../../lib/JIT/UserCallableResolver.php'; // VM -r smoke: bootstrap-selfhost-lib-spine-vm-smoke.sh (#1846). // VM driver execute: bootstrap-selfhost-vm-driver-execute-probe.sh (#2201).