diff --git a/lib/Compiler.php b/lib/Compiler.php index 3f8b1099d1e..83dca6ea437 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -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). @@ -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; } /** @@ -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 $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 $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 $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 $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 { @@ -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, @@ -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; } @@ -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)) { @@ -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) @@ -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, @@ -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; @@ -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; + } } } } @@ -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) { diff --git a/test/repro/maintainer_gap_array_intersect_assoc_inline_array_keys.php b/test/repro/maintainer_gap_array_intersect_assoc_inline_array_keys.php new file mode 100644 index 00000000000..91ba4da2244 --- /dev/null +++ b/test/repro/maintainer_gap_array_intersect_assoc_inline_array_keys.php @@ -0,0 +1,11 @@ + 1, 'b' => 2]), + array_keys(['a' => 9, 'c' => 3]) +); +var_export($r); +echo "\n"; +echo "ok\n"; diff --git a/test/unit/InlineCallArgProducerSlotTest.php b/test/unit/InlineCallArgProducerSlotTest.php index dee99b7e1f6..3a79a6d89a0 100644 --- a/test/unit/InlineCallArgProducerSlotTest.php +++ b/test/unit/InlineCallArgProducerSlotTest.php @@ -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 {