From ca41af9bb578223f5eef55d3c5b62ca27828dba3 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 8 Aug 2026 18:41:18 +0800 Subject: [PATCH] fix(parser): ensure left-to-right evaluation order for PHP expressions in C++ - Added parseOrderedOperand() to handle operand evaluation in correct sequence - Updated BinaryOpTrait to use ordered parsing for concat() operations - Modified CallArgumentGenerator to materialize nested calls before argument lowering - Enhanced CompilerBase to maintain evaluation order in string interpolations - Updated MethodCallTrait to evaluate receiver before method arguments - Added comprehensive test case for function/method call argument evaluation order --- src/CompilerBase.php | 4 +- src/Generator/CallArgumentGenerator.php | 11 +++- src/Optimizer/FuncCallOptimizer.php | 14 +--- src/Parser/BinaryOpTrait.php | 8 ++- src/Parser/MethodCallTrait.php | 7 +- .../callable/call-argument-eval-order.phpt | 64 +++++++++++++++++++ 6 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 tests/compiler/callable/call-argument-eval-order.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c0534d7b..7a8dc31c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3376,7 +3376,9 @@ class CompilerBase implements PropertyAccessContext if (!$part instanceof Node\InterpolatedStringPart) { $this->assertExprCanBeUsedAsValue($part, 'string interpolation value'); } - $list[] = $this->parseExpr($part); + $list[] = $part instanceof Node\InterpolatedStringPart + ? $this->parseExpr($part) + : $this->parseOrderedOperand($part, false); } return 'php::concat({' . implode(', ', $list) . '})'; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index aef054cc..a9b6e344 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -597,7 +597,16 @@ trait CallArgumentGenerator protected function parseCallArgValue(Node\Arg $arg): string { $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); - return $this->materializeCallArgValue($arg->value, $this->parseArg($arg)); + // C++17 does not define the evaluation order of function arguments. + // PHP does, so a nested call must be completed and stored before the + // next argument is lowered. Do not materialize unrelated expressions + // here: their native/reference types are handled by parseArg(). + $expr = $arg->value instanceof Expr\FuncCall + || $arg->value instanceof Expr\MethodCall + || $arg->value instanceof Expr\StaticCall + ? $this->parseOrderedArg($arg) + : $this->parseArg($arg); + return $this->materializeCallArgValue($arg->value, $expr); } protected function materializeCallArgValue(NodeAbstract $value, string $expr): string diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 5b2501a1..c347e319 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -702,21 +702,13 @@ trait FuncCallOptimizer return 'php::Decimal::round(' . $a0 . ')'; } $args = count($e->args); - // Keep PHP's left-to-right argument evaluation explicit here. This - // optimizer is itself compiled by TypePHP, and embedding several - // getArg() calls in one C++ string-concatenation expression would let - // the host C++ compiler choose a different evaluation order. - $a0 = $this->getArg($e, 0); if ($args >= 3) { - $a1 = $this->convertIntExpr($this->getArg($e, 1)); - $a2 = $this->convertIntExpr($this->getArg($e, 2)); - return 'php::fn::round(' . $a0 . ', ' . $a1 . ', ' . $a2 . ')'; + return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')'; } if ($args >= 2) { - $a1 = $this->convertIntExpr($this->getArg($e, 1)); - return 'php::fn::round(' . $a0 . ', ' . $a1 . ')'; + return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ')'; } - return 'php::fn::round(' . $a0 . ')'; + return 'php::fn::round(' . $this->getArg($e, 0) . ')'; } protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index e30f8242..93f9b526 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -622,7 +622,13 @@ trait BinaryOpTrait } $type = $this->detectTypeOfExpr($item); - $parsed = $this->parseExprAsValue($item); + // concat() is emitted as one C++ call with every PHP operand as an + // argument. C++17 does not prescribe the evaluation order of + // function arguments, whereas PHP evaluates these operands from + // left to right. In particular, materialize nested FuncCall, + // MethodCall and StaticCall expressions before assembling the + // outer concat() call. + $parsed = $this->parseOrderedOperand($item, false); $argList[] = $this->prepareConcatOperand($parsed, $type); } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 9b95aa53..ec09b828 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -296,7 +296,12 @@ trait MethodCallTrait } $class = ''; - $object = $this->parseIdentifier($expr->var); + // PHP evaluates the receiver before method arguments. Materializing an + // effectful receiver here prevents an ordered nested-call argument from + // being hoisted ahead of expressions such as `new $class(...$args)`. + $object = empty($expr->args) + ? $this->parseIdentifier($expr->var) + : $this->parseOrderedOperand($expr->var, false); if ($this->isVarExpr($expr->var)) { if (!$this->hasVar($object)) { $this->errorUndefinedVariable($expr->var); diff --git a/tests/compiler/callable/call-argument-eval-order.phpt b/tests/compiler/callable/call-argument-eval-order.phpt new file mode 100644 index 00000000..d7ed656b --- /dev/null +++ b/tests/compiler/callable/call-argument-eval-order.phpt @@ -0,0 +1,64 @@ +--TEST-- +Function, method, static and generated helper call arguments evaluate left-to-right +--FILE-- +method(traced('method-left', 'c'), traced('method-right', 'd'))); + + $method = 'method'; + var_dump($object->$method(traced('dynamic-method-left', 'k'), traced('dynamic-method-right', 'l'))); + + var_dump(CallOrder::staticMethod(traced('static-left', 'e'), traced('static-right', 'f'))); + var_dump(traced('concat-left', 'g') . traced('concat-right', 'h')); +} +?> +--EXPECT-- +function-left +function-right +string(2) "ab" +dynamic-function-left +dynamic-function-right +string(2) "ij" +method-left +method-right +string(2) "cd" +dynamic-method-left +dynamic-method-right +string(2) "kl" +static-left +static-right +string(2) "ef" +concat-left +concat-right +string(2) "gh"