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
74 changes: 70 additions & 4 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,10 @@ protected function compileOps(array $ops, Block $block): void {
// Lowered by compileExpr Empty_ via TYPE_ISSET + TYPE_BOOLEAN_NOT (#3298, #4586).
break;
} elseif (
$child instanceof Op\Expr\ArrayDimFetch
(
$child instanceof Op\Expr\ArrayDimFetch
|| $this->isListSpreadAssignOp($child)
)
&& $this->isListDestructGroupStart($ops, $i)
) {
[$block, $i] = $this->compileListDestructGroup($ops, $i, $block);
Expand Down Expand Up @@ -1138,13 +1141,23 @@ private function isKeyedListDestructDimFetch(array $ops, int $index): bool
return $assign->expr === $fetch->result;
}

private function isListSpreadAssignOp(Op $op): bool
{
return $op instanceof Op\Expr\Assign
&& null !== $op->listSpreadRhs
&& null !== $op->listSpreadFromIndex;
}

/**
* php-cfg lowers `list($a, …) = $rhs` to integer-key dim fetches (#4298).
*
* @param Op[] $ops
*/
private function isListDestructGroupStart(array $ops, int $index): bool
{
if ($this->isListSpreadAssignOp($ops[$index])) {
return !$this->isListDestructSpreadTail($ops, $index);
}
if (!$this->isPlainListDestructDimFetch($ops, $index)) {
return false;
}
Expand All @@ -1171,6 +1184,27 @@ private function isListDestructGroupStart(array $ops, int $index): bool
return true;
}

/**
* Spread arm at the end of `[$a, ...$rest] = $rhs` — not a separate group start (#4835).
*
* @param Op[] $ops
*/
private function isListDestructSpreadTail(array $ops, int $index): bool
{
if (!$this->isListSpreadAssignOp($ops[$index])) {
return false;
}
if ($index < 1) {
return false;
}
$p = $index - 1;
if ($ops[$p] instanceof Op\Expr\Assign || $ops[$p] instanceof Op\Expr\AssignRef) {
--$p;
}

return $p >= 0 && $this->isPlainListDestructDimFetch($ops, $p);
}

/**
* @param Op[] $ops
*/
Expand Down Expand Up @@ -1221,13 +1255,36 @@ private function isListDestructDimFetchConsumer(array $ops, int $index): bool
private function listDestructGroupEndIndex(array $ops, int $start): int
{
$i = $start;
if ($this->isListSpreadAssignOp($ops[$i])) {
return $i;
}
while ($i < count($ops) && $this->isPlainListDestructDimFetch($ops, $i)) {
$i = $this->listDestructOpEndIndex($ops, $i);
}
if ($i < count($ops) && $this->isListSpreadAssignOp($ops[$i])) {
return $i;
}

return $i - 1;
}

/**
* @param Op[] $ops
*/
private function listDestructRhsOperand(array $ops, int $start): Operand
{
if ($this->isListSpreadAssignOp($ops[$start])) {
/** @var Op\Expr\Assign $spread */
$spread = $ops[$start];

return $spread->listSpreadRhs;
}
/** @var Op\Expr\ArrayDimFetch $firstFetch */
$firstFetch = $ops[$start];

return $firstFetch->var;
}

/**
* @param Op[] $ops
*/
Expand Down Expand Up @@ -1262,13 +1319,12 @@ private function listDestructOpEndIndex(array $ops, int $index): int
private function compileListDestructGroup(array $ops, int $start, Block $block): array
{
$end = $this->listDestructGroupEndIndex($ops, $start);
/** @var Op\Expr\ArrayDimFetch $firstFetch */
$firstFetch = $ops[$start];
$rhs = $this->listDestructRhsOperand($ops, $start);

$checkOp = new OpCode(
OpCode::TYPE_LIST_UNPACK_CHECK,
null,
$this->compileOperand($firstFetch->var, $block, true),
$this->compileOperand($rhs, $block, true),
);
$block->addOpCode($checkOp);

Expand Down Expand Up @@ -3612,6 +3668,16 @@ protected function compileExpr(Op\Expr $expr, Block $block): array {
$this->compileOperand($expr->expr, $block, true)
)];
case Op\Expr\Assign::class:
if (null !== $expr->listSpreadRhs && null !== $expr->listSpreadFromIndex) {
$fromIndex = new Operand\Literal($expr->listSpreadFromIndex);

return [new OpCode(
OpCode::TYPE_LIST_SPREAD_ASSIGN,
$this->compileOperand($expr->var, $block, false),
$this->compileOperand($expr->listSpreadRhs, $block, true),
$this->compileOperand($fromIndex, $block, true),
)];
}
$staticPropertyFetch = $this->unwrapStaticPropertyFetch($expr->var);
if (null !== $staticPropertyFetch) {
$fetchSlot = $this->compileOperand($staticPropertyFetch->result, $block, false);
Expand Down
2 changes: 2 additions & 0 deletions lib/OpCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class OpCode {
const TYPE_FUNCTION_STATIC_INIT_STORE = 122;
/** PHP 8.3+ `$needle in $haystack` strict contains (#4682). arg2=needle, arg3=haystack. */
const TYPE_IN = 123;
/** `[$a, ...$rest] = $list` tail: arg1=dest, arg2=source array, arg3=from-index constant slot (#4835). */
const TYPE_LIST_SPREAD_ASSIGN = 124;

public int $type;
public ?int $arg1;
Expand Down
2 changes: 2 additions & 0 deletions lib/OpCodeNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ function opcode_type_name(int $type): string
return 'TYPE_FUNCTION_STATIC_INIT_STORE';
case 123:
return 'TYPE_IN';
case 124:
return 'TYPE_LIST_SPREAD_ASSIGN';
default:
return 'unknown opcode';
}
Expand Down
29 changes: 29 additions & 0 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,35 @@ private function runFramesInner(): int
}
}
break;
case OpCode::TYPE_LIST_SPREAD_ASSIGN:
$dest = $frame->scope[$op->arg1];
$src = $frame->scope[$op->arg2]->resolveIndirect();
if (Variable::TYPE_ARRAY !== $src->type) {
if (null !== $op->block1) {
$frame = $this->frameForBranch($frame, $op->block1);
goto restart;
}
break;
}
if (!isset($frame->block->constants[$op->arg3])) {
throw new \LogicException('list spread assign requires compile-time offset');
}
$offset = $frame->block->constants[$op->arg3]->toInt();
$ht = $src->toArray();
if (!\PHPCompiler\ext\standard\VmArray::isList($ht)) {
$catchFrame = $this->dispatchVmTypeError(
new \TypeError('Cannot unpack array with string keys'),
$frame
);
if (null !== $catchFrame) {
$frame = $catchFrame;
goto restart;
}
break;
}
$tail = $ht->sliceCopy($offset, null);
$dest->array($tail);
break;
case OpCode::TYPE_ARRAY_DIM_FETCH:
case OpCode::TYPE_ARRAY_DIM_FETCH_WRITE:
$arg1 = $frame->scope[$op->arg1];
Expand Down
51 changes: 51 additions & 0 deletions patches/php-cfg-list-spread.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/Assign.php
+++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/Assign.php
@@ -20,6 +20,12 @@ class Assign extends Expr
public $expr;

+ /** `[$a, ...$rest] = $rhs` tail: full list RHS (#4835). */
+ public $listSpreadRhs = null;
+
+ /** Zero-based index of first element merged into the spread target (#4835). */
+ public $listSpreadFromIndex = null;
+
protected $writeVariables = ['var', 'result'];

public function __construct(Operand $var, Operand $expr, array $attributes = [])
--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
+++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
@@ -1508,11 +1508,27 @@
}

$attributes = $this->mapAttributes($expr);
+ $logicalIndex = 0;
foreach ($expr->items as $i => $item) {
if (null === $item) {
continue;
}

if ($item->key === null) {
- $key = new Operand\Literal($i);
+ $key = new Operand\Literal($logicalIndex);
} else {
$key = $this->readVariable($this->parseExprNode($item->key));
}

$var = $item->value;
+ if ($item->unpack) {
+ $target = $this->writeVariable($this->parseExprNode($var));
+ $assign = new Op\Expr\Assign($target, $rhs, $attributes);
+ $assign->listSpreadRhs = $rhs;
+ $assign->listSpreadFromIndex = $logicalIndex;
+ $this->block->children[] = $assign;
+
+ continue;
+ }
+
+ if ($item->key === null) {
+ ++$logicalIndex;
+ }
+
$fetch = new Op\Expr\ArrayDimFetch($rhs, $key, $attributes);
$this->block->children[] = $fetch;
if ($var instanceof Expr\List_ || $var instanceof Expr\Array_) {
5 changes: 5 additions & 0 deletions script/apply-patches.sh
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ patch_already_applied() {
grep -q 'isEmptyListExpr' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null \
&& grep -q "Cannot use empty list" "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null
;;
php-cfg-list-spread.patch)
grep -q 'listSpreadRhs' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/Assign.php" 2>/dev/null \
&& grep -q '\$item->unpack' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null
;;
php-cfg-first-class-callable.patch)
grep -q 'isFirstClassCallable' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null
;;
Expand Down Expand Up @@ -2576,6 +2580,7 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-cfg" ]]; then
apply_patch "$PATCH_DIR/php-cfg-assignop-coalesce.patch"
apply_patch "$PATCH_DIR/php-cfg-list-destruct-byref.patch"
apply_patch "$PATCH_DIR/php-cfg-empty-list-assignment.patch"
apply_patch "$PATCH_DIR/php-cfg-list-spread.patch"
apply_patch "$PATCH_DIR/php-cfg-first-class-callable.patch"
apply_patch "$PATCH_DIR/php-cfg-arrow-function.patch"
apply_patch "$PATCH_DIR/php-cfg-anonymous-class.patch"
Expand Down
4 changes: 4 additions & 0 deletions test/compliance/JITTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ public static function providePHPTests(): \Generator
if (str_contains($name, 'list_destructure_string')) {
continue;
}
// list spread `[$a, ...$rest] = $arr`: VM (#4835); MCJIT execute pending.
if (str_contains($name, 'list_destructuring_spread')) {
continue;
}
// PHP 8.3 typed class constants: VM + AOT; MCJIT execute unstable (#4511, #3592).
if (str_contains($name, 'typed_class_const')) {
continue;
Expand Down
9 changes: 9 additions & 0 deletions test/compliance/cases/language/list_destructuring_spread.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
list destructuring with spread — [$a, ...$rest] = $arr
--FILE--
<?php
$src = [1, 2, 3, 4];
[$a, ...$rest] = $src;
echo $a, ':', implode(',', $rest), "\n";
--EXPECT--
1:2,3,4
4 changes: 4 additions & 0 deletions test/repro/list_destructuring_spread_4835.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
$src = [1, 2, 3, 4];
[$a, ...$rest] = $src;
echo $a, ':', implode(',', $rest), "\n";