Skip to content

fix(compiler): keep operand evaluation order in echo lists and array keys - #144

Open
Giandonn wants to merge 1 commit into
swoole:masterfrom
Giandonn:fix/echo-operand-order
Open

Giandonn wants to merge 1 commit into
swoole:masterfrom
Giandonn:fix/echo-operand-order

Conversation

@Giandonn

@Giandonn Giandonn commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #143

A call nested in an expression can be materialized into a pre-statement temporary. Two constructs emitted those temporaries before work PHP performs first.

The change

  • echo (CompilerBase::parseEcho()): it already emits one php::echo() per operand, but every operand's hoisted lines landed before the first php::echo(). The pre- and post-statement lines produced while parsing an operand are now emitted around that operand's own php::echo(), so each operand is evaluated after the previous one has been printed — including when it throws.
  • Array literal keys (ArrayExpressionTrait::parseArrayMixed()): the value was parsed before the key, so a value that is a call was materialized ahead of an inline key expression. A side-effecting key (shouldMaterializeOrderedOperand()) is now materialized with parseOrderedOperand() before the value is parsed. Literal keys are untouched (they also never reach this path with side effects in parseArray()).

Verified on a binary

PHP 8.4.26 ZTS with embed, GCC 11, Linux x64.

master this PR
8 constructs with nested side effects (echo list, call args, array values, array keys, concat, binary op, ?:, sprintf) compared with PHP 2 differ identical to PHP
new tests/compiler/basic/echo-operand-order.phpt FAIL ([f:1]A'1') PASS
new tests/compiler/array/array-key-evaluation-order.phpt FAIL ([v1][k1][v2][k2]) PASS
tests/compiler (full) — 1249 passed, 0 failed, 27 skipped
PHPUnit (full) — 2408 tests OK, 4 skipped

Found with a differential fuzzer (same session as #137 and #140).

…keys

A call nested in an expression can be materialized into a pre-statement
temporary. Two constructs emitted those temporaries before work that PHP
performs first, so side effects (output, mutations, exceptions) happened in
the wrong order.

echo: `echo a, b, c` prints each operand before evaluating the next one.
parseEcho() emits one php::echo() per operand, but every operand's hoisted
lines were emitted before the first php::echo():

    function f(string $s): string { echo "[f:$s]"; return $s; }
    echo "A", var_export(f("1"), true), "\n";
    // PHP:      A[f:1]'1'
    // compiled: [f:1]A'1'

    echo "E-", var_export(boom(), true);   // boom() throws
    // PHP prints "E-" before the exception; the binary printed nothing.

The pre- and post-statement lines produced while parsing an operand are now
emitted around that operand's own php::echo().

Array literals: PHP evaluates a key before its value. parseArrayMixed()
parsed the value first, so a value that is a call was materialized before
an inline key expression:

    [f("k1") => strtoupper(f("v1")), f("k2") => f("v2")]
    // PHP:      [k1][v1][k2][v2]
    // compiled: [v1][k1][v2][k2]

A side-effecting key is now materialized before its value is parsed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Side effects run out of order in echo lists and array literal keys

1 participant