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
84 changes: 84 additions & 0 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<Op> $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).
*
Expand Down
15 changes: 15 additions & 0 deletions test/compliance/cases/stdlib/nested_string_builtin_arg.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
stdlib nested scalar-return builtin in call argument (#10495, Zend/zend_execute.c)
--FILE--
<?php
declare(strict_types=1);

echo var_export(get_debug_type(null), true), "\n";
echo var_export(gettype(null), true), "\n";
echo var_export(json_encode(null), true), "\n";
echo var_export(get_class(new stdClass()), true), "\n";
--EXPECT--
'null'
'NULL'
'null'
'stdClass'
15 changes: 15 additions & 0 deletions test/repro/nested_string_builtin_call_arg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

/**
* Maintainer repro: nested scalar-return builtin in call argument (#10495).
*/

echo var_export(get_debug_type(null), true), "\n";
echo var_export(gettype(null), true), "\n";
echo var_export(json_encode(null), true), "\n";
echo var_export(get_class(new stdClass()), true), "\n";

$x = get_debug_type(null);
echo var_export($x, true), "\n";
46 changes: 46 additions & 0 deletions test/unit/InlineCallArgProducerSlotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1009,4 +1009,50 @@ public function testVarExportArrayPadNegativeLengthUsesNestedFuncCallProducerSlo
self::assertStringContainsString('0', $out);
self::assertStringContainsString('1', $out);
}

/** Issue #10495 — var_export(get_debug_type(null), true) wires nested scalar-return FuncCall producer. */
public function testVarExportNestedScalarBuiltinUsesFuncCallProducerSlot(): void
{
$code = <<<'PHP'
<?php
echo var_export(get_debug_type(null), true);
echo "\n";
echo var_export(gettype(null), true);
echo "\n";
echo var_export(json_encode(null), true);
echo "\n";
echo var_export(get_class(new stdClass()), true);
PHP;
$runtime = new Runtime();
$block = $runtime->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);
}
}