diff --git a/lib/Compiler.php b/lib/Compiler.php index d43b54a531b..f5f9ec7d88b 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -12117,6 +12117,22 @@ private function precedingInlineCallArgProducersBeforeCfgOp(array $cfgChildren, array_unshift($producers, $child); break; } + if ( + ($child instanceof Op\Expr\FuncCall || $child instanceof Op\Expr\NsFuncCall) + && property_exists($callOp, 'args') + && is_array($callOp->args) + && count($callOp->args) >= 2 + && $this->isNestedCallArgProducerSeparatedByConsumerLiteralPreludes( + $child, + $callOp, + $i, + $callIndex, + $cfgChildren + ) + ) { + array_unshift($producers, $child); + break; + } if ($child instanceof Op\Expr\Array_) { $next = $cfgChildren[$i + 1] ?? null; // php-cfg `var_export(array_keys([...]), true)` — Array_ feeds sibling FuncCall arg (#10373). @@ -12182,6 +12198,15 @@ private function isNestedCallArgProducerForConsumer( if ($this->isAdjacentNestedFuncCallProducer($producer, $consumer, $producerIndex, $consumerIndex)) { return true; } + if ($this->isNestedCallArgProducerSeparatedByConsumerLiteralPreludes( + $producer, + $consumer, + $producerIndex, + $consumerIndex, + $cfgChildren + )) { + return true; + } if ($producerIndex + 2 !== $consumerIndex) { return false; } @@ -12209,6 +12234,65 @@ private function isNestedCallArgProducerForConsumer( || $this->operandsReferToSameVariable($consumer->name, $callee->result); } + /** + * php-cfg `var_export(g(), true)` hoists trailing literal ConstFetch between nested calls (#10495). + * + * @param list $cfgChildren + */ + private function isNestedCallArgProducerSeparatedByConsumerLiteralPreludes( + Op\Expr $producer, + Op $consumer, + int $producerIndex, + int $consumerIndex, + array $cfgChildren + ): bool { + if ($producerIndex >= $consumerIndex - 1) { + return false; + } + if ( + !$producer instanceof Op\Expr\FuncCall + && !$producer instanceof Op\Expr\NsFuncCall + && !$producer instanceof Op\Expr\StaticCall + && !$producer instanceof Op\Expr\MethodCall + ) { + return false; + } + if ( + !$consumer instanceof Op\Expr\FuncCall + && !$consumer instanceof Op\Expr\NsFuncCall + && !$consumer instanceof Op\Expr\MethodCall + && !$consumer instanceof Op\Expr\StaticCall + && !$consumer instanceof Op\Expr\New_ + ) { + return false; + } + if (!property_exists($consumer, 'args') || !is_array($consumer->args) || count($consumer->args) < 2) { + return false; + } + $firstArg = $consumer->args[0] ?? null; + if (!$firstArg instanceof Operand\Temporary) { + return false; + } + for ($j = $producerIndex + 1; $j < $consumerIndex; ++$j) { + $mid = $cfgChildren[$j] ?? null; + if ($mid instanceof Op\Expr\ConstFetch || $mid instanceof Op\Expr\ClassConstFetch) { + continue; + } + if ( + $mid instanceof Op\Expr\FuncCall + || $mid instanceof Op\Expr\NsFuncCall + || $mid instanceof Op\Expr\StaticCall + || $mid instanceof Op\Expr\MethodCall + ) { + return false; + } + + return false; + } + + return true; + } + /** * php-cfg `var_dump($g(), $g())` hoists sibling FuncCall producers with dead arg temps (#9463). * diff --git a/test/compliance/cases/stdlib/nested_string_builtin_arg.phpt b/test/compliance/cases/stdlib/nested_string_builtin_arg.phpt new file mode 100644 index 00000000000..71078f4df1b --- /dev/null +++ b/test/compliance/cases/stdlib/nested_string_builtin_arg.phpt @@ -0,0 +1,15 @@ +--TEST-- +stdlib nested scalar-return builtin in call argument (#10495, Zend/zend_execute.c) +--FILE-- +parseAndCompile($code, 'nested_string_builtin_call_arg.php'); + + $debugTypeReturnSlot = null; + $varExportSends = []; + $fcallOrdinal = 0; + foreach ($block->opCodes as $op) { + if (OpCode::TYPE_FUNCCALL_INIT === $op->type) { + ++$fcallOrdinal; + if (2 === $fcallOrdinal) { + $varExportSends = []; + } + } + if (1 === $fcallOrdinal && OpCode::TYPE_FUNCCALL_EXEC_RETURN === $op->type) { + $debugTypeReturnSlot = $op->arg1; + } + if (2 === $fcallOrdinal && OpCode::TYPE_ARG_SEND === $op->type) { + $varExportSends[] = $op->arg1; + } + } + + self::assertNotNull($debugTypeReturnSlot); + self::assertCount(2, $varExportSends); + self::assertSame($debugTypeReturnSlot, $varExportSends[0], 'arg sends='.json_encode($varExportSends)); + + ob_start(); + $runtime->run($block); + $out = ob_get_clean(); + self::assertStringContainsString("'null'", $out); + self::assertStringContainsString("'NULL'", $out); + self::assertStringContainsString("'stdClass'", $out); + } }