Skip to content
Open
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
14 changes: 13 additions & 1 deletion src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2413,9 +2413,21 @@ protected function resolveCalledFunctionDef(NodeAbstract $expr): ?FunctionDef

protected function parseEcho(mixed $v): string
{
// PHP prints each operand before it evaluates the next one. A nested
// call inside an operand may be materialized into a pre-statement
// temporary; keep those lines next to the operand they belong to,
// otherwise they all run (and may throw) before the first operand
// has been printed.
$lines = [];
foreach ($v->exprs as $expr) {
$lines[] = 'php::echo(' . $this->parseExprToString($expr) . ');';
$beforeCount = count($this->context->beforeStmtLines);
$afterCount = count($this->context->afterStmtLines);
$value = $this->parseExprToString($expr);
$before = array_slice($this->context->beforeStmtLines, $beforeCount);
$after = array_slice($this->context->afterStmtLines, $afterCount);
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeCount);
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterCount);
array_push($lines, ...$before, ...['php::echo(' . $value . ');'], ...$after);
}

return implode("\n" . $this->getIndent(), $lines);
Expand Down
12 changes: 11 additions & 1 deletion src/Parser/ArrayExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,16 @@ private function parseArrayMixed(Expr\Array_ $node): string

$items = $node->items;
foreach ($items as $item) {
// PHP evaluates a key before its value. A side-effecting key must
// be materialized first: the value may hoist a nested call into
// a pre-statement temporary, which would otherwise run before an
// inline key expression.
$orderedKey = null;
if ($item->key && !$item->unpack && $this->shouldMaterializeOrderedOperand($item->key)) {
$this->assertExprCanBeUsedAsValue($item->key, 'array key');
$this->assertNotNativeObjectArrayKey($item->key);
$orderedKey = $this->parseOrderedOperand($item->key, false, true);
}
$this->assertExprCanBeUsedAsValue($item->value, $item->unpack ? 'array unpack value' : 'array value');
if ($item->byRef) {
if ($item->unpack) {
Expand All @@ -314,7 +324,7 @@ private function parseArrayMixed(Expr\Array_ $node): string
$this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');';
} elseif ($item->key) {
$this->assertExprCanBeUsedAsValue($item->key, 'array key');
$key = $this->parseArrayKey($item->key);
$key = $orderedKey ?? $this->parseArrayKey($item->key);
$method = $item->byRef ? 'set' : 'setValue';
$this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.' . $method . '(' . $key . ', ' . $value . ');';
} else {
Expand Down
19 changes: 19 additions & 0 deletions tests/compiler/array/array-key-evaluation-order.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
An array literal evaluates a side-effecting key before its value, also when the value nests a call
--FILE--
<?php
function f(string $s): string { echo "[$s]"; return $s; }

function main(): void
{
$a = [f("k1") => strtoupper(f("v1")), f("k2") => f("v2")];
echo "\n", json_encode($a), "\n";
$b = [f("x") => str_repeat(f("y"), 2), ...[f("z")]];
echo "\n", json_encode($b), "\n";
}
?>
--EXPECT--
[k1][v1][k2][v2]
{"k1":"V1","k2":"v2"}
[x][y][z]
{"x":"yy","0":"z"}
27 changes: 27 additions & 0 deletions tests/compiler/basic/echo-operand-order.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
echo prints each operand before evaluating the next, also when an operand nests a call
--FILE--
<?php
function f(string $s): string { echo "[f:$s]"; return $s; }
function boom(): string { throw new RuntimeException('boom'); }

function main(): void
{
echo "A", var_export(f("1"), true), "\n";
echo "B", strtoupper(f("2")), "\n";
echo "C", str_repeat(f("3"), 2), "\n";
$g = fn() => f("4");
echo "D", var_export($g(), true), "\n";
try {
echo "E-", var_export(boom(), true), "\n";
} catch (RuntimeException $e) {
echo "| caught ", $e->getMessage(), "\n";
}
}
?>
--EXPECT--
A[f:1]'1'
B[f:2]2
C[f:3]33
D[f:4]'4'
E-| caught boom
Loading