From b7d2673a81b9d89213ebf1e5bba6cbae53b5bbcd Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 4 Jul 2026 22:38:11 +0000 Subject: [PATCH] Compiler: fix stream_context_create([]) after @mkdir misbind (#16205) Hoisted empty-array literals in post-@ END blocks were aliased to the suppressed call's return slot (same class of bug as null literals in #15916). Skip suppress-slot binding when a trailing Array_ feeds the dead-temp arg. Co-authored-by: Cursor --- lib/Compiler.php | 52 +++++++++++++++++- ...ream_context_create_empty_after_mkdir.phpt | 10 ++++ ..._gap_stream_context_create_after_mkdir.php | 6 +++ .../ErrorSuppressInlineReturnSlotTest.php | 53 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 test/compliance/cases/stdlib/stream_context_create_empty_after_mkdir.phpt create mode 100644 test/repro/maintainer_gap_stream_context_create_after_mkdir.php diff --git a/lib/Compiler.php b/lib/Compiler.php index 1d82617c447..0f87240339a 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -8192,7 +8192,10 @@ private function inheritErrorSuppressExpressionSlots(Block $suppressCompiled, Bl ) { continue; } - if ($this->errorSuppressEndBlockCallArgHasTrailingHoistedScalarProducer($endCompiled, $endChild, (int) $argIndex)) { + if ( + $this->errorSuppressEndBlockCallArgHasTrailingHoistedScalarProducer($endCompiled, $endChild, (int) $argIndex) + || $this->errorSuppressEndBlockCallArgHasTrailingHoistedArrayProducer($endCompiled, $endChild, (int) $argIndex) + ) { continue; } // First dead temp in the outer call is the @ inner value (#15916, #10302). @@ -28693,10 +28696,55 @@ private function errorSuppressEndBlockInnerResultSlotForCallArg( if ($this->errorSuppressEndBlockCallArgHasTrailingHoistedScalarProducer($block, $cfgCallOp, $argIndex)) { return null; } + if ($this->errorSuppressEndBlockCallArgHasTrailingHoistedArrayProducer($block, $cfgCallOp, $argIndex)) { + return null; + } return $this->errorSuppressEndBlockInnerResultSlot($block); } + /** + * Trailing hoisted Array_ before a post-@ call feeds this dead-temp arg (#16205). + */ + private function errorSuppressEndBlockCallArgHasTrailingHoistedArrayProducer( + Block $block, + Op $cfgCallOp, + int $argIndex + ): bool { + if (null === $block->orig || !property_exists($cfgCallOp, 'args') || !\is_array($cfgCallOp->args)) { + return false; + } + $callArg = $cfgCallOp->args[$argIndex] ?? null; + if (!$callArg instanceof Operand || $this->isEmbeddedCallLiteralArg($callArg)) { + return false; + } + if (!$this->callArgIsDeadInlineTemporary($callArg)) { + return false; + } + $arrayProducer = $this->inlineArrayProducerImmediatelyBeforeCfgCall($cfgCallOp, $block); + if (!$arrayProducer instanceof Op\Expr\Array_) { + return false; + } + if ( + null !== $arrayProducer->result + && $this->operandsReferToSameVariable($arrayProducer->result, $callArg) + ) { + return true; + } + $nonEmbeddedArgIndices = []; + foreach ($cfgCallOp->args as $i => $candidate) { + if (!$this->isEmbeddedCallLiteralArg($candidate)) { + $nonEmbeddedArgIndices[] = (int) $i; + } + } + $producerOrdinal = array_search($argIndex, $nonEmbeddedArgIndices, true); + if (false === $producerOrdinal) { + return false; + } + + return 0 === $producerOrdinal; + } + /** * Trailing hoisted true/false/null before a post-@ call feeds this dead-temp arg (#15916). * @@ -28779,7 +28827,7 @@ private function errorSuppressEndBlockCallArgHasTrailingHoistedScalarProducer( /** * True when an outer call in a post-@ block consumes the suppressed inner expression (#10336, #15916). * - * Standalone `@mkdir(); stream_context_create(null)` must not wire hoisted null to mkdir's return slot. + * Standalone `@mkdir(); stream_context_create(null|[])` must not wire hoisted literal to mkdir's return slot. */ private function callInErrorSuppressEndBlockUsesInnerResultAsArg(Block $block, Op $cfgCallOp): bool { diff --git a/test/compliance/cases/stdlib/stream_context_create_empty_after_mkdir.phpt b/test/compliance/cases/stdlib/stream_context_create_empty_after_mkdir.phpt new file mode 100644 index 00000000000..b31e4effcad --- /dev/null +++ b/test/compliance/cases/stdlib/stream_context_create_empty_after_mkdir.phpt @@ -0,0 +1,10 @@ +--TEST-- +stream_context_create([]) after @mkdir — empty literal stays array (#16205, ext/standard/streams.c) +--FILE-- +parseAndCompile($code, 'suppress_then_hoisted_empty_array.php'); + + $suppressReturnSlot = null; + $streamContextSendSlot = null; + $walk = static function (Block $b) use (&$walk, &$suppressReturnSlot, &$streamContextSendSlot): void { + $inSuppress = null !== $b->orig && $b->orig instanceof \PHPCfg\ErrorSuppressBlock; + $inEnd = null !== $b->orig && 1 === \count($b->orig->parents) && $b->orig->parents[0] instanceof \PHPCfg\ErrorSuppressBlock; + $pendingInit = false; + foreach ($b->opCodes as $op) { + if (OpCode::TYPE_FUNCCALL_INIT === $op->type) { + $pendingInit = true; + continue; + } + if ($pendingInit && OpCode::TYPE_FUNCCALL_EXEC_RETURN === $op->type) { + if ($inSuppress) { + $suppressReturnSlot = $op->arg1; + } + $pendingInit = false; + continue; + } + if ($pendingInit && OpCode::TYPE_FUNCCALL_EXEC_NORETURN === $op->type) { + $pendingInit = false; + continue; + } + if (OpCode::TYPE_ARG_SEND === $op->type && $inEnd && null === $streamContextSendSlot) { + $streamContextSendSlot = $op->arg1; + } + foreach ([$op->block1, $op->block2, $op->block3] as $sub) { + if ($sub instanceof Block) { + $walk($sub); + } + } + } + }; + $walk($block); + + self::assertNotNull($suppressReturnSlot, 'suppress mkdir return slot'); + self::assertNotNull($streamContextSendSlot, 'stream_context_create arg send slot'); + self::assertNotSame( + $suppressReturnSlot, + $streamContextSendSlot, + 'hoisted [] must not alias unrelated @mkdir return slot' + ); + } }