diff --git a/docs/capabilities.md b/docs/capabilities.md index 76ea665d7..3ef29c708 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -44,7 +44,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `header` | yes | yes | yes | standard | AOT PHPT | | `hexdec` | yes | yes | yes | standard | | | `htmlspecialchars` | yes | yes | yes | standard | AOT PHPT | -| `implode` | yes | yes | yes | standard | doc: VM only; AOT PHPT | +| `implode` | yes | yes | yes | standard | doc: VM only; JIT PHPT; AOT PHPT | | `in_array` | yes | yes | yes | standard | doc: VM only; JIT PHPT | | `intdiv` | yes | yes | yes | standard | | | `intval` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | @@ -86,6 +86,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `scandir` | yes | yes | yes | standard | | | `sin` | yes | yes | yes | standard | | | `sizeof` | yes | yes | yes | standard | | +| `sort` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `sqrt` | yes | yes | yes | standard | | | `str_contains` | yes | yes | yes | standard | | | `str_ends_with` | yes | yes | yes | standard | AOT PHPT | diff --git a/ext/standard/Module.php b/ext/standard/Module.php index 624256adf..f72da203c 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -79,6 +79,7 @@ public function getFunctions(): array new array_push(), new array_pop(), new array_shift(), + new sort_(), new array_values(), new array_keys(), new array_merge(), diff --git a/ext/standard/sort_.php b/ext/standard/sort_.php new file mode 100644 index 000000000..6f71cbb5a --- /dev/null +++ b/ext/standard/sort_.php @@ -0,0 +1,88 @@ +calledArgs)) { + throw new \LogicException('sort() requires exactly one argument'); + } + $array = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_ARRAY !== $array->type) { + throw new \LogicException('sort() argument must be an array in this compiler build'); + } + $ht = $array->toArray(); + if ($ht->getNumElements() < 2) { + if (null !== $frame->returnVar) { + $frame->returnVar->bool(true); + } + + return; + } + $values = []; + foreach ($ht->iterate(true) as $value) { + $copy = new Variable(); + $copy->copyFrom($value); + $values[] = $copy; + } + \usort($values, static function (Variable $a, Variable $b): int { + $a = $a->resolveIndirect(); + $b = $b->resolveIndirect(); + if (Variable::TYPE_STRING === $a->type && Variable::TYPE_STRING === $b->type) { + return VmString::strcmp($a->toString(), $b->toString()); + } + if (Variable::TYPE_INTEGER === $a->type && Variable::TYPE_INTEGER === $b->type) { + return $a->toInt() <=> $b->toInt(); + } + + throw new \LogicException( + 'sort() only supports homogeneous string or integer arrays in this compiler build' + ); + }); + $ht->replacePackedValues($values); + if (null !== $frame->returnVar) { + $frame->returnVar->bool(true); + } + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + if (1 !== \count($args)) { + throw new \LogicException('sort() requires exactly one argument'); + } + ArrayBuiltinHelper::sortPacked($context, $args[0]); + + return $context->getTypeFromString('int1')->constInt(1, false); + } +} diff --git a/lib/JIT/ArrayBuiltinHelper.php b/lib/JIT/ArrayBuiltinHelper.php index a88445acd..450adc134 100644 --- a/lib/JIT/ArrayBuiltinHelper.php +++ b/lib/JIT/ArrayBuiltinHelper.php @@ -431,6 +431,17 @@ private static function looseEqual(Context $context, Variable $left, Variable $r return $context->constantFromBool(false); } + public static function sortPacked(Context $context, Variable $array): void + { + if (self::isNativeArray($array->type)) { + throw new \LogicException( + 'sort() cannot compile fixed-size literal arrays in JIT/AOT yet; use bin/vm.php or bin/serve.php, or build the list with [] append' + ); + } + $ht = self::loadHashTable($context, $array); + $context->builder->call($context->lookupFunction('__hashtable__sortPacked'), $ht); + } + private static function sameTypeEqual(Context $context, Variable $left, Variable $right): Value { switch ($left->type) { diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php index eb5378706..2a61ad821 100755 --- a/lib/JIT/Builtin/Type/HashTable.php +++ b/lib/JIT/Builtin/Type/HashTable.php @@ -72,6 +72,7 @@ public function register(): void $this->registerFn('__hashtable__setStringKeyLong', 'void', ['__hashtable__*', '__string__*', 'int64']); $this->registerFn('__hashtable__offsetIsSetStringKey', 'int1', ['__hashtable__*', '__string__*']); $this->registerFn('__hashtable__readStringKeyValue', '__value__*', ['__hashtable__*', '__string__*']); + $this->registerFn('__hashtable__sortPacked', 'void', ['__hashtable__*']); $this->pointer = $this->context->getTypeFromString('__hashtable__*'); } @@ -106,6 +107,7 @@ public function implement(): void $this->implementSetStringKeyString(); $this->implementOffsetIsSetStringKey(); $this->implementReadStringKeyValue(); + $this->implementSortPacked(); } private function ensureLibcStringCompare(): void @@ -726,6 +728,222 @@ private function lookupStringKeyValue( return $this->context->builder->load($resultSlot); } + private function implementSortPacked(): void + { + $fn = $this->context->lookupFunction('__hashtable__sortPacked'); + $main = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($main); + $ht = $fn->getParam(0); + $map = $this->context->structFieldMap['__hashtable__']; + $sizeT = $this->context->getTypeFromString('size_t'); + $two = $sizeT->constInt(2, false); + $num = $this->context->builder->load($this->context->builder->structGep($ht, $map['nextFreeElement'])); + $tooSmall = $this->context->builder->icmp(Builder::INT_ULT, $num, $two); + $done = $fn->appendBasicBlock('sort_done'); + $work = $fn->appendBasicBlock('sort_work'); + $this->context->builder->branchIf($tooSmall, $done, $work); + + $this->context->builder->positionAtEnd($work); + $zero = $sizeT->constInt(0, false); + $firstEntry = $this->listEntryAt($ht, $map, $zero); + $valueMap = $this->context->structFieldMap['__value__']; + $firstType = $this->context->builder->load( + $this->context->builder->structGep($firstEntry, $valueMap['type']) + ); + $i8 = $this->context->getTypeFromString('int8'); + $stringTag = $i8->constInt(Variable::TYPE_STRING, false); + $longTag = $i8->constInt(Variable::TYPE_NATIVE_LONG, false); + $isString = $this->context->builder->icmp(Builder::INT_EQ, $firstType, $stringTag); + $sortStrings = $fn->appendBasicBlock('sort_strings'); + $sortLongs = $fn->appendBasicBlock('sort_longs'); + $this->context->builder->branchIf($isString, $sortStrings, $sortLongs); + + $this->context->builder->positionAtEnd($sortStrings); + $this->emitBubbleSortStrings($fn, $ht, $map, $num); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($sortLongs); + $this->emitBubbleSortLongs($fn, $ht, $map, $num); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($done); + $this->context->builder->returnVoid(); + } + + private function emitBubbleSortStrings( + PHPLLVM\LLVMAbstract\Value\Function_ $fn, + PHPLLVM\Value $ht, + array $map, + PHPLLVM\Value $num + ): void { + $sizeT = $this->context->getTypeFromString('size_t'); + $one = $sizeT->constInt(1, false); + $zero = $sizeT->constInt(0, false); + $outerSlot = $this->context->builder->alloca($sizeT, 1, 'sort_outer'); + $this->context->builder->store($zero, $outerSlot); + $outerHead = $fn->appendBasicBlock('sort_str_outer_head'); + $outerBody = $fn->appendBasicBlock('sort_str_outer_body'); + $outerDone = $fn->appendBasicBlock('sort_str_outer_done'); + $this->context->builder->branch($outerHead); + + $this->context->builder->positionAtEnd($outerHead); + $outer = $this->context->builder->load($outerSlot); + $outerEnd = $this->context->builder->sub($num, $one); + $outerAtEnd = $this->context->builder->icmp(Builder::INT_SGE, $outer, $outerEnd); + $this->context->builder->branchIf($outerAtEnd, $outerDone, $outerBody); + + $this->context->builder->positionAtEnd($outerBody); + $innerSlot = $this->context->builder->alloca($sizeT, 1, 'sort_inner'); + $this->context->builder->store($zero, $innerSlot); + $limit = $this->context->builder->sub($num, $outer); + $limit = $this->context->builder->sub($limit, $one); + $innerHead = $fn->appendBasicBlock('sort_str_inner_head'); + $innerBody = $fn->appendBasicBlock('sort_str_inner_body'); + $innerDone = $fn->appendBasicBlock('sort_str_inner_done'); + $this->context->builder->branch($innerHead); + + $this->context->builder->positionAtEnd($innerHead); + $inner = $this->context->builder->load($innerSlot); + $innerAtEnd = $this->context->builder->icmp(Builder::INT_SGE, $inner, $limit); + $this->context->builder->branchIf($innerAtEnd, $innerDone, $innerBody); + + $this->context->builder->positionAtEnd($innerBody); + $nextInner = $this->context->builder->addNoSignedWrap($inner, $one); + $strA = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readStringAt'), + $ht, + $inner + ); + $strB = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readStringAt'), + $ht, + $nextInner + ); + $cmp = $this->context->builder->call( + $this->context->lookupFunction('strcmp'), + $this->stringDataPtr($strA), + $this->stringDataPtr($strB) + ); + $i32 = $this->context->getTypeFromString('int32'); + $needsSwap = $this->context->builder->icmp(Builder::INT_SGT, $cmp, $i32->constInt(0, false)); + $swapBlock = $fn->appendBasicBlock('sort_str_swap'); + $noSwap = $fn->appendBasicBlock('sort_str_no_swap'); + $afterSwap = $fn->appendBasicBlock('sort_str_after_swap'); + $this->context->builder->branchIf($needsSwap, $swapBlock, $noSwap); + + $this->context->builder->positionAtEnd($swapBlock); + $entryA = $this->listEntryAt($ht, $map, $inner); + $entryB = $this->listEntryAt($ht, $map, $nextInner); + $this->context->builder->call($this->context->lookupFunction('__value__writeString'), $entryA, $strB); + $this->context->builder->call($this->context->lookupFunction('__value__writeString'), $entryB, $strA); + $this->context->builder->branch($afterSwap); + + $this->context->builder->positionAtEnd($noSwap); + $this->context->builder->branch($afterSwap); + + $this->context->builder->positionAtEnd($afterSwap); + $this->context->builder->store($nextInner, $innerSlot); + $this->context->builder->branch($innerHead); + + $this->context->builder->positionAtEnd($innerDone); + $this->context->builder->store( + $this->context->builder->addNoSignedWrap($outer, $one), + $outerSlot + ); + $this->context->builder->branch($outerHead); + + $this->context->builder->positionAtEnd($outerDone); + } + + private function emitBubbleSortLongs( + PHPLLVM\LLVMAbstract\Value\Function_ $fn, + PHPLLVM\Value $ht, + array $map, + PHPLLVM\Value $num + ): void { + $sizeT = $this->context->getTypeFromString('size_t'); + $one = $sizeT->constInt(1, false); + $zero = $sizeT->constInt(0, false); + $outerSlot = $this->context->builder->alloca($sizeT, 1, 'sort_long_outer'); + $this->context->builder->store($zero, $outerSlot); + $outerHead = $fn->appendBasicBlock('sort_long_outer_head'); + $outerBody = $fn->appendBasicBlock('sort_long_outer_body'); + $outerDone = $fn->appendBasicBlock('sort_long_outer_done'); + $this->context->builder->branch($outerHead); + + $this->context->builder->positionAtEnd($outerHead); + $outer = $this->context->builder->load($outerSlot); + $outerEnd = $this->context->builder->sub($num, $one); + $outerAtEnd = $this->context->builder->icmp(Builder::INT_SGE, $outer, $outerEnd); + $this->context->builder->branchIf($outerAtEnd, $outerDone, $outerBody); + + $this->context->builder->positionAtEnd($outerBody); + $innerSlot = $this->context->builder->alloca($sizeT, 1, 'sort_long_inner'); + $this->context->builder->store($zero, $innerSlot); + $limit = $this->context->builder->sub($num, $outer); + $limit = $this->context->builder->sub($limit, $one); + $innerHead = $fn->appendBasicBlock('sort_long_inner_head'); + $innerBody = $fn->appendBasicBlock('sort_long_inner_body'); + $innerDone = $fn->appendBasicBlock('sort_long_inner_done'); + $this->context->builder->branch($innerHead); + + $this->context->builder->positionAtEnd($innerHead); + $inner = $this->context->builder->load($innerSlot); + $innerAtEnd = $this->context->builder->icmp(Builder::INT_SGE, $inner, $limit); + $this->context->builder->branchIf($innerAtEnd, $innerDone, $innerBody); + + $this->context->builder->positionAtEnd($innerBody); + $nextInner = $this->context->builder->addNoSignedWrap($inner, $one); + $longA = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readLongAt'), + $ht, + $inner + ); + $longB = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readLongAt'), + $ht, + $nextInner + ); + $needsSwap = $this->context->builder->icmp(Builder::INT_SGT, $longA, $longB); + $swapBlock = $fn->appendBasicBlock('sort_long_swap'); + $noSwap = $fn->appendBasicBlock('sort_long_no_swap'); + $afterSwap = $fn->appendBasicBlock('sort_long_after_swap'); + $this->context->builder->branchIf($needsSwap, $swapBlock, $noSwap); + + $this->context->builder->positionAtEnd($swapBlock); + $entryA = $this->listEntryAt($ht, $map, $inner); + $entryB = $this->listEntryAt($ht, $map, $nextInner); + $this->context->builder->call($this->context->lookupFunction('__value__writeLong'), $entryA, $longB); + $this->context->builder->call($this->context->lookupFunction('__value__writeLong'), $entryB, $longA); + $this->context->builder->branch($afterSwap); + + $this->context->builder->positionAtEnd($noSwap); + $this->context->builder->branch($afterSwap); + + $this->context->builder->positionAtEnd($afterSwap); + $this->context->builder->store($nextInner, $innerSlot); + $this->context->builder->branch($innerHead); + + $this->context->builder->positionAtEnd($innerDone); + $this->context->builder->store( + $this->context->builder->addNoSignedWrap($outer, $one), + $outerSlot + ); + $this->context->builder->branch($outerHead); + + $this->context->builder->positionAtEnd($outerDone); + } + + /** + * @param array $map + */ + private function listEntryAt(PHPLLVM\Value $ht, array $map, PHPLLVM\Value $index): PHPLLVM\Value + { + $values = $this->context->builder->load($this->context->builder->structGep($ht, $map['values'])); + + return $this->context->builder->inBoundsGep($values, $index); + } + private function stringDataPtr(PHPLLVM\Value $str): PHPLLVM\Value { $map = $this->context->structFieldMap['__string__']; diff --git a/lib/VM/HashTable.php b/lib/VM/HashTable.php index 2998f78af..40dd2b889 100755 --- a/lib/VM/HashTable.php +++ b/lib/VM/HashTable.php @@ -198,6 +198,33 @@ public function shiftFirst(): ?Variable return $result; } + /** + * Replace packed list values in place (indices 0..n-1, no holes). + * + * @param list $values + */ + public function replacePackedValues(array $values): void + { + $this->assertConsistent(); + if (!$this->isWithoutHoles()) { + throw new \LogicException('replacePackedValues() only supports packed list arrays without holes'); + } + if (\count($values) !== $this->numElements) { + throw new \LogicException('replacePackedValues() value count must match array length'); + } + $this->refcount->assertSeparated(); + for ($i = 0; $i < $this->numUsed; ++$i) { + $bucket = $this->buckets->read($i); + if ($bucket->value->isUndefined()) { + continue; + } + $bucket->value->copyFrom($values[$i]); + $bucket->hash = $i; + $bucket->key = null; + } + $this->rehash(); + } + /** * Copy all defined values into a new packed list array. */ diff --git a/test/compliance/cases/stdlib/sort.phpt b/test/compliance/cases/stdlib/sort.phpt new file mode 100644 index 000000000..e41ef54b1 --- /dev/null +++ b/test/compliance/cases/stdlib/sort.phpt @@ -0,0 +1,9 @@ +--TEST-- +stdlib sort() on string list arrays +--FILE-- +string($s); + $ht->append($var); + } + $sorted = ['a', 'b', 'c']; + $values = []; + foreach ($sorted as $s) { + $var = new Variable(); + $var->string($s); + $values[] = $var; + } + $ht->replacePackedValues($values); + $this->assertSame('a', $ht->findIndex(0)->toString()); + $this->assertSame('b', $ht->findIndex(1)->toString()); + $this->assertSame('c', $ht->findIndex(2)->toString()); + } +}