Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions ext/standard/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
88 changes: 88 additions & 0 deletions ext/standard/sort_.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

/**
* This file is part of PHP-Compiler, a PHP CFG Compiler for PHP code
*
* @copyright 2015 Anthony Ferrara. All rights reserved
* @license MIT See LICENSE at the root of the project for more info
*/

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\ArrayBuiltinHelper;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPCompiler\VM\Variable;
use PHPLLVM\Value;

/**
* sort() for homogeneous packed string or integer arrays (subset of PHP).
*
* VM: full support. JIT/AOT: dynamic hashtable arrays only (not fixed native literals).
*/
final class sort_ extends Internal
{
public function __construct()
{
parent::__construct('sort');
}

public function execute(Frame $frame): void
{
if (1 !== \count($frame->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);
}
}
11 changes: 11 additions & 0 deletions lib/JIT/ArrayBuiltinHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
218 changes: 218 additions & 0 deletions lib/JIT/Builtin/Type/HashTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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__*');
}
Expand Down Expand Up @@ -106,6 +107,7 @@ public function implement(): void
$this->implementSetStringKeyString();
$this->implementOffsetIsSetStringKey();
$this->implementReadStringKeyValue();
$this->implementSortPacked();
}

private function ensureLibcStringCompare(): void
Expand Down Expand Up @@ -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<string, int> $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__'];
Expand Down
Loading
Loading