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
pull/47/head
韩天峰 2 weeks ago
parent 833b74b344
commit ca41af9bb5
  1. 4
      src/CompilerBase.php
  2. 11
      src/Generator/CallArgumentGenerator.php
  3. 14
      src/Optimizer/FuncCallOptimizer.php
  4. 8
      src/Parser/BinaryOpTrait.php
  5. 7
      src/Parser/MethodCallTrait.php
  6. 64
      tests/compiler/callable/call-argument-eval-order.phpt

@ -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) . '})';

@ -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

@ -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

@ -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);
}

@ -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);

@ -0,0 +1,64 @@
--TEST--
Function, method, static and generated helper call arguments evaluate left-to-right
--FILE--
<?php
function traced(string $label, string $value): string
{
echo $label, "\n";
return $value;
}
function combine(string $left, string $right): string
{
return $left . $right;
}
class CallOrder
{
public function method(string $left, string $right): string
{
return $left . $right;
}
public static function staticMethod(string $left, string $right): string
{
return $left . $right;
}
}
function main(): void
{
var_dump(combine(traced('function-left', 'a'), traced('function-right', 'b')));
$function = 'combine';
var_dump($function(traced('dynamic-function-left', 'i'), traced('dynamic-function-right', 'j')));
$object = new CallOrder();
var_dump($object->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"
Loading…
Cancel
Save