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
215 changes: 199 additions & 16 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11700,6 +11700,11 @@ private function findUnassignedInlineArrayProducerForDeadCallArg(
return null;
}
if (1 === \count($deadArrayArgIndices)) {
$immediate = $this->inlineArrayProducerImmediatelyBeforeCfgCall($callOp, $block);
if ($immediate instanceof Op\Expr\Array_) {
return $immediate;
}

return $unassigned[\count($unassigned) - 1];
}
// Nested / sibling inline Array_ chains — flat unassigned[] index is wrong (#12729, #12730).
Expand Down Expand Up @@ -16638,7 +16643,7 @@ private function isDeferredSiblingInlineCallArgProducer(Op $op, array $ops, int
return false;
}

return ($consumerIndex - $firstSibling) >= 2;
return $this->countSiblingInlineFuncCallProducers($firstSibling, $consumerIndex, $ops) >= 2;
}

/**
Expand Down Expand Up @@ -16673,12 +16678,123 @@ private function deferredSiblingInlineCallArgConsumerIndex(Op $op, array $ops, i
if ($this->isUnaryInlineSiblingCallArgExpr($next)) {
continue;
}
if ($next instanceof Op\Expr\Array_) {
continue;
}
if ($next instanceof Op\Expr\ConstFetch || $next instanceof Op\Expr\ClassConstFetch) {
continue;
}
break;
}

return null;
}

/**
* @param list<Op> $cfgChildren
*/
private function countSiblingInlineFuncCallProducers(
int $firstSibling,
int $consumerIndex,
array $cfgChildren
): int {
$count = 0;
for ($j = $firstSibling; $j < $consumerIndex; ++$j) {
$child = $cfgChildren[$j] ?? null;
if ($child instanceof Op\Expr\FuncCall || $child instanceof Op\Expr\NsFuncCall) {
++$count;
}
}

return $count;
}

/**
* True when php-cfg hoisted sibling Array_ literals between FuncCall producers (#13778).
*
* @param list<Op> $cfgChildren
*/
private function siblingFuncCallChainHasArrayPrelude(
int $firstSibling,
int $consumerIndex,
array $cfgChildren
): bool {
for ($j = $firstSibling; $j < $consumerIndex; ++$j) {
if (($cfgChildren[$j] ?? null) instanceof Op\Expr\Array_) {
return true;
}
}

return false;
}

/**
* 0-based ordinal among hoisted sibling FuncCall producers (skips Array_/ConstFetch between calls).
*
* @param list<Op> $cfgChildren
*/
private function siblingInlineFuncCallProducerOrdinal(
int $producerIndex,
int $firstSibling,
array $cfgChildren
): int {
$ordinal = -1;
for ($j = $firstSibling; $j <= $producerIndex; ++$j) {
$child = $cfgChildren[$j] ?? null;
if ($child instanceof Op\Expr\FuncCall || $child instanceof Op\Expr\NsFuncCall) {
++$ordinal;
}
}

return $ordinal;
}

/**
* @param list<Op> $cfgChildren
*/
private function siblingInlineFuncCallProducerIndexAtOrdinal(
int $ordinal,
int $firstSibling,
int $consumerIndex,
array $cfgChildren
): ?int {
$seen = -1;
for ($j = $firstSibling; $j < $consumerIndex; ++$j) {
$child = $cfgChildren[$j] ?? null;
if ($child instanceof Op\Expr\FuncCall || $child instanceof Op\Expr\NsFuncCall) {
++$seen;
if ($seen === $ordinal) {
return $j;
}
}
}

return null;
}

/**
* php-cfg `array_keys([...])` hoists the literal Array_ stmt immediately before the call (#13778).
*/
private function inlineArrayProducerImmediatelyBeforeCfgCall(?Op $callOp, Block $block): ?Op\Expr\Array_
{
if (null === $callOp || null === $block->orig) {
return null;
}
$callIndex = null;
foreach ($block->orig->children as $i => $child) {
if ($child === $callOp) {
$callIndex = $i;
break;
}
}
if (null === $callIndex || $callIndex < 1) {
return null;
}
$prev = $block->orig->children[$callIndex - 1] ?? null;

return $prev instanceof Op\Expr\Array_ ? $prev : null;
}

/** Hoisted FuncCall / MethodCall / StaticCall sibling inline call-arg producers (#9463, #9351, #12421). */
private function isSiblingInlineCallProducerExpr(Op $op): bool
{
Expand Down Expand Up @@ -16725,10 +16841,26 @@ private function isSiblingMultiArgFuncCallProducer(
return false;
}
}
$distance = $consumerIndex - $producerIndex;
if ($distance < 1 || $distance > $argCount) {
$firstSibling = $this->firstSiblingInlineFuncCallProducerIndex($consumerIndex, $cfgChildren);
if (null === $firstSibling) {
return false;
}
$arrayPreludeChain = $this->siblingFuncCallChainHasArrayPrelude($firstSibling, $consumerIndex, $cfgChildren);
if ($arrayPreludeChain) {
$producerOrdinal = $this->siblingInlineFuncCallProducerOrdinal(
$producerIndex,
$firstSibling,
$cfgChildren
);
if ($producerOrdinal < 0 || $producerOrdinal >= $argCount) {
return false;
}
} else {
$distance = $consumerIndex - $producerIndex;
if ($distance < 1 || $distance > $argCount) {
return false;
}
}
$targetArgIndex = $this->siblingMultiArgFuncCallProducerTargetArgIndex(
$producerIndex,
$consumerIndex,
Expand Down Expand Up @@ -16840,6 +16972,9 @@ private function siblingMultiArgProducerWiresByDistinctDeadArgOrdinal(
if ($sib instanceof Op\Expr\ConstFetch || $sib instanceof Op\Expr\ClassConstFetch) {
continue;
}
if ($sib instanceof Op\Expr\Array_) {
continue;
}
if ($this->isUnaryInlineSiblingCallArgExpr($sib)) {
continue;
}
Expand All @@ -16850,7 +16985,9 @@ private function siblingMultiArgProducerWiresByDistinctDeadArgOrdinal(
return false;
}
}
$ordinal = $producerIndex - $firstSibling;
$ordinal = $this->siblingFuncCallChainHasArrayPrelude($firstSibling, $consumerIndex, $cfgChildren)
? $this->siblingInlineFuncCallProducerOrdinal($producerIndex, $firstSibling, $cfgChildren)
: ($producerIndex - $firstSibling);
$leadingEmbedded = 0;
foreach ($consumer->args as $callArg) {
if ($this->isEmbeddedCallLiteralArg($callArg)) {
Expand Down Expand Up @@ -16887,7 +17024,9 @@ private function siblingMultiArgFuncCallProducerTargetArgIndex(
return null;
}

$ordinal = $producerIndex - $firstSibling;
$ordinal = $this->siblingFuncCallChainHasArrayPrelude($firstSibling, $consumerIndex, $cfgChildren)
? $this->siblingInlineFuncCallProducerOrdinal($producerIndex, $firstSibling, $cfgChildren)
: ($producerIndex - $firstSibling);
$consumer = $cfgChildren[$consumerIndex] ?? null;
if (
($consumer instanceof Op\Expr\FuncCall || $consumer instanceof Op\Expr\NsFuncCall)
Expand Down Expand Up @@ -17053,11 +17192,21 @@ private function ensureDeferredSiblingInlineCallArgProducersCompiled(Block $bloc
if (null === $firstSibling) {
return;
}
$siblingCount = $callIndex - $firstSibling;
if ($siblingCount < 2) {
$arrayPreludeChain = $this->siblingFuncCallChainHasArrayPrelude(
$firstSibling,
$callIndex,
$block->orig->children
);
$siblingFuncCount = $this->countSiblingInlineFuncCallProducers(
$firstSibling,
$callIndex,
$block->orig->children
);
if ($siblingFuncCount < 2) {
return;
}
for ($argIndex = 0; $argIndex < $siblingCount; ++$argIndex) {
$loopBound = $arrayPreludeChain ? min($argCount, $siblingFuncCount) : ($callIndex - $firstSibling);
for ($argIndex = 0; $argIndex < $loopBound; ++$argIndex) {
$emitOps = [];
$slot = $this->resolveSiblingInlineCallArgProducerSlot(
$block,
Expand Down Expand Up @@ -17104,11 +17253,28 @@ private function resolveSiblingInlineCallArgProducerSlot(
if (null === $firstSibling) {
return null;
}
$siblingCount = $callIndex - $firstSibling;
if ($siblingCount < 2 || $argIndex >= $siblingCount) {
return null;
$arrayPreludeChain = $this->siblingFuncCallChainHasArrayPrelude(
$firstSibling,
$callIndex,
$block->orig->children
);
if ($arrayPreludeChain) {
$producerIndex = $this->siblingInlineFuncCallProducerIndexAtOrdinal(
$argIndex,
$firstSibling,
$callIndex,
$block->orig->children
);
if (null === $producerIndex) {
return null;
}
} else {
$siblingCount = $callIndex - $firstSibling;
if ($siblingCount < 2 || $argIndex >= $siblingCount) {
return null;
}
$producerIndex = $firstSibling + $argIndex;
}
$producerIndex = $firstSibling + $argIndex;
$producer = $block->orig->children[$producerIndex] ?? null;
if (!$this->isSiblingInlineCallProducerExpr($producer)) {
return null;
Expand Down Expand Up @@ -21453,9 +21619,23 @@ protected function compileCallArgSends(
}
// Array union arg is Plus.result, not the trailing INIT_ARRAY temp (#10490, #12763).
if (!$hasArrayUnionPlus && !$lastProducer instanceof Op\Expr\BinaryOp\Plus) {
$initArraySlot = $this->slotForRecentInitArrayCallArg($block);
if (null !== $initArraySlot) {
$valueSlot = $initArraySlot;
$immediateArray = $this->inlineArrayProducerImmediatelyBeforeCfgCall($cfgCallOp, $block);
if ($immediateArray instanceof Op\Expr\Array_) {
$immediateSlot = $block->slotForOperand($immediateArray->result);
if (null === $immediateSlot) {
foreach ($this->compileExpr($immediateArray, $block) as $op) {
$sends[] = $op;
}
$immediateSlot = $block->slotForOperand($immediateArray->result);
}
if (null !== $immediateSlot) {
$valueSlot = (string) $immediateSlot;
}
} else {
$initArraySlot = $this->slotForRecentInitArrayCallArg($block);
if (null !== $initArraySlot) {
$valueSlot = $initArraySlot;
}
}
}
}
Expand Down Expand Up @@ -22435,7 +22615,10 @@ private function siblingInlineCallArgProducerNeedsReturnSlot(?Op $cfgCallOp, Blo
continue;
}
$firstSibling = $this->firstSiblingInlineFuncCallProducerIndex($consumerIndex, $cfgChildren);
if (null === $firstSibling || $consumerIndex - $firstSibling < 2) {
if (
null === $firstSibling
|| $this->countSiblingInlineFuncCallProducers($firstSibling, $consumerIndex, $cfgChildren) < 2
) {
continue;
}
foreach ($cfgChildren as $producerIndex => $producer) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

$r = array_intersect_assoc(
array_keys(['a' => 1, 'b' => 2]),
array_keys(['a' => 9, 'c' => 3])
);
var_export($r);
echo "\n";
echo "ok\n";
38 changes: 38 additions & 0 deletions test/unit/InlineCallArgProducerSlotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1944,6 +1944,44 @@ public function testArrayMergeLeadingArrayTrailingArrayKeysRuntime(): void
self::assertStringContainsString("0 => 'b'", $out);
}

/** Issue #13778 — array_intersect_assoc(array_keys(...), array_keys(...)) wires both sibling FuncCall slots. */
public function testArrayIntersectAssocInlineArrayKeysRuntime(): void
{
$code = file_get_contents(__DIR__.'/../repro/maintainer_gap_array_intersect_assoc_inline_array_keys.php');
self::assertNotFalse($code);
$runtime = new Runtime();
$block = $runtime->parseAndCompile($code, 'array_intersect_assoc_inline_array_keys.php');

$keysReturnSlots = [];
$intersectSends = [];
$fcallOrdinal = 0;
foreach ($block->opCodes as $op) {
if (OpCode::TYPE_FUNCCALL_INIT === $op->type) {
++$fcallOrdinal;
if (3 === $fcallOrdinal) {
$intersectSends = [];
}
}
if ($fcallOrdinal <= 2 && OpCode::TYPE_FUNCCALL_EXEC_RETURN === $op->type) {
$keysReturnSlots[] = $op->arg1;
}
if (3 === $fcallOrdinal && OpCode::TYPE_ARG_SEND === $op->type) {
$intersectSends[] = $op->arg1;
}
}

self::assertCount(2, $keysReturnSlots);
self::assertCount(2, $intersectSends);
self::assertSame($keysReturnSlots[0], $intersectSends[0], 'intersect sends='.json_encode($intersectSends));
self::assertSame($keysReturnSlots[1], $intersectSends[1], 'intersect sends='.json_encode($intersectSends));

ob_start();
$runtime->run($block);
$out = ob_get_clean();
self::assertStringContainsString("0 => 'a'", $out);
self::assertStringContainsString("ok", $out);
}

/** Issue #13776 — array_combine(array_keys(...), [...]) wires FuncCall + trailing Array_ producer slots. */
public function testArrayCombineInlineArrayKeysRuntime(): void
{
Expand Down