diff --git a/src/CompilerBase.php b/src/CompilerBase.php index db84d361..78a23a9d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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); diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 27a63f29..b45e7016 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -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) { @@ -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 { diff --git a/tests/compiler/array/array-key-evaluation-order.phpt b/tests/compiler/array/array-key-evaluation-order.phpt new file mode 100755 index 00000000..c057ee30 --- /dev/null +++ b/tests/compiler/array/array-key-evaluation-order.phpt @@ -0,0 +1,19 @@ +--TEST-- +An array literal evaluates a side-effecting key before its value, also when the value nests a call +--FILE-- + 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"} diff --git a/tests/compiler/basic/echo-operand-order.phpt b/tests/compiler/basic/echo-operand-order.phpt new file mode 100755 index 00000000..36486c33 --- /dev/null +++ b/tests/compiler/basic/echo-operand-order.phpt @@ -0,0 +1,27 @@ +--TEST-- +echo prints each operand before evaluating the next, also when an operand nests a call +--FILE-- + 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