fix(parser): ensure correct argument evaluation order for native calls

- Updated BinaryOpTrait to use C++17 braced-list evaluation order for concat operations
- Modified CallArgumentGenerator to evaluate arguments in PHP source order before ABI arrangement
- Added proper handling of variadic arguments and named parameters in evaluation sequence
- Fixed array_key_exists parameter evaluation order to match PHP semantics
- Updated method call receiver evaluation to prevent statement hoisting issues
- Added comprehensive tests for argument evaluation order in various call scenarios
- Ensured captured beforeStmtLines are properly ordered relative to call expressions
pull/47/head
韩天峰 2 weeks ago
parent 8d4801ebd2
commit b1b40d6a09
  1. 3
      src/CompilerBase.php
  2. 114
      src/Generator/CallArgumentGenerator.php
  3. 6
      src/Optimizer/FuncCallOptimizer.php
  4. 10
      src/Parser/BinaryOpTrait.php
  5. 6
      src/Parser/MethodCallTrait.php
  6. 17
      tests/compiler/callable/call-argument-eval-order.phpt
  7. 44
      tests/compiler/functions/named-args-native.phpt
  8. 8
      tests/compiler/stdlib/native-arg-eval-order.phpt

@ -3376,6 +3376,9 @@ class CompilerBase implements PropertyAccessContext
if (!$part instanceof Node\InterpolatedStringPart) {
$this->assertExprCanBeUsedAsValue($part, 'string interpolation value');
}
// Although C++17 orders the braced-list elements, materializing an
// expression prevents captured statements from a later part from
// being hoisted ahead of an earlier Call.
$list[] = $part instanceof Node\InterpolatedStringPart
? $this->parseExpr($part)
: $this->parseOrderedOperand($part, false);

@ -24,10 +24,11 @@ trait CallArgumentGenerator
{
protected function parseNativeCallArgs(array $callArgs, string $nativeFunc, int $parameterOffset = 0): string
{
$argList = [];
$functionDef = $this->getFunction($nativeFunc);
$args = [];
$variadicArgs = [];
$providedArgs = [];
$defaultArgs = [];
$sourceArgs = [];
$variadicArgCount = 0;
$hasNamedArg = false;
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
@ -43,18 +44,28 @@ trait CallArgumentGenerator
if ($k < $parameterOffset) {
$this->fatalError($arg, 'Named argument cannot target the extension receiver');
}
$args[$k] = $arg;
$providedArgs[$k] = true;
$sourceArgs[] = [$k, null, $arg];
} else {
$variadicArgs[] = [$argName, $arg];
if ($variadicArgIndex === null) {
$this->fatalError($arg, "Unknown named argument `{$argName}`");
}
$sourceArgs[] = [$variadicArgIndex, $argName, $arg];
$variadicArgCount++;
}
$hasNamedArg = true;
} elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) {
$variadicArgs[] = [null, $arg];
$sourceArgs[] = [$variadicArgIndex, null, $arg];
$variadicArgCount++;
} else {
$args[$i + $parameterOffset] = $arg;
$argIndex = $i + $parameterOffset;
$providedArgs[$argIndex] = true;
$sourceArgs[] = [$argIndex, null, $arg];
}
}
// 对 key 进行排序,确保参数顺序正确
// Fill ABI holes first, but do not sort yet. User expressions must be
// lowered in source order; sorting raw AST arguments here would also
// reorder their side effects.
if ($hasNamedArg) {
// 命名参数中间存在空洞,需要使用默认参数填充
foreach ($functionDef->argInfoList as $k => $argInfo) {
@ -64,7 +75,7 @@ trait CallArgumentGenerator
if ($variadicArgIndex !== null and $k === $variadicArgIndex) {
continue;
}
if (!isset($args[$k])) {
if (!isset($providedArgs[$k])) {
if ($argInfo->default === '') {
$errorNode = null;
foreach ($callArgs as $a) {
@ -79,61 +90,71 @@ trait CallArgumentGenerator
// Defaults are resolved in the declaration scope. Re-parsing
// the original AST here would evaluate self/parent/private
// class constants in the caller's scope instead.
$args[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k);
$defaultArgs[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k);
}
}
ksort($args);
}
if ($variadicArgIndex !== null and $variadicArgs) {
$args[$variadicArgIndex] = $this->buildNativeVariadicArg($variadicArgs, $functionDef->argInfoList[$variadicArgIndex]);
ksort($args);
}
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
if (count($args) === 0
if (count($sourceArgs) === 0
and count($functionDef->argInfoList) === $parameterOffset + 1
and $functionDef->argInfoList[$parameterOffset]->variadic) {
return '{}';
}
foreach ($args as $i => $arg) {
if (is_string($arg)) {
$argList[] = $arg;
$resolvedArgs = [];
$variadicVar = null;
$callableName = $functionDef->displayName ?: $functionDef->getNamespacedName();
// Evaluate every supplied argument in PHP source order. The resulting
// expressions/temporaries may then be rearranged safely for the native
// C++ ABI without changing observable call order.
foreach ($sourceArgs as [$argIndex, $variadicName, $arg]) {
if ($argIndex !== $variadicArgIndex) {
$argInfo = $this->getArgInfo($arg, $nativeFunc, $argIndex);
$resolvedArgs[$argIndex] = $this->getTypeConvertedArg(
$arg,
$argInfo,
$callableName,
$argIndex
);
continue;
}
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
$callableName = $functionDef->displayName ?: $functionDef->getNamespacedName();
$argList[] = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $i);
}
return implode(', ', $argList);
}
protected function buildNativeVariadicArg(array $variadicArgs, ArgInfo $argInfo): string
{
if (count($variadicArgs) === 1 and $variadicArgs[0][0] === null and $variadicArgs[0][1]->unpack) {
$arg = $variadicArgs[0][1];
if ($this->isVarExpr($arg->value)) {
// A single unpacked native array is already the ABI value. Reuse
// it directly instead of allocating and merging a temporary array.
if ($variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) {
$var = $this->parseIdentifier($arg->value);
if ($this->getVarType($var) === Type::ARRAY) {
return $var;
$resolvedArgs[$variadicArgIndex] = $var;
continue;
}
}
return $this->convertArrayExpr($this->parseExpr($arg->value));
}
$tmpVar = $this->addTmpVar(Type::ARRAY);
foreach ($variadicArgs as [$name, $arg]) {
$variadicVar ??= $this->addTmpVar(Type::ARRAY);
$argInfo = $functionDef->argInfoList[$variadicArgIndex];
if ($arg->unpack) {
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArrayArg($arg) . ');';
} elseif ($name !== null) {
$this->context->beforeStmtLines[] = $tmpVar . '.set(' . $this->getLiteralString($name) . ', ' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
$this->context->beforeStmtLines[] = $variadicVar . '.merge(' . $this->parseArrayArg($arg) . ');';
} elseif ($variadicName !== null) {
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
$this->context->beforeStmtLines[] = $variadicVar . '.set('
. $this->getLiteralString($variadicName) . ', ' . $value . ');';
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
$this->context->beforeStmtLines[] = $variadicVar . '.append(' . $value . ');';
}
}
return $tmpVar;
// Defaults have no caller-side evaluation. Add them after user
// arguments, then sort only the already-lowered values for the ABI.
foreach ($defaultArgs as $i => $defaultArg) {
$resolvedArgs[$i] = $defaultArg;
}
if ($variadicVar !== null) {
$resolvedArgs[$variadicArgIndex] = $variadicVar;
}
ksort($resolvedArgs);
return implode(', ', $resolvedArgs);
}
protected function parseNamedCallArgs(array $args, int $firstIndex, array $listArgs): string
@ -597,10 +618,11 @@ trait CallArgumentGenerator
protected function parseCallArgValue(Node\Arg $arg): string
{
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
// 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().
// C++17 evaluates php::ArgList{...} elements from left to right, but a
// later argument may emit captured beforeStmtLines while being lowered.
// Those statements are placed before the whole outer call and would
// overtake an earlier Call left inside the initializer list. Complete
// each direct Call in a temporary before lowering the next argument.
$expr = $arg->value instanceof Expr\FuncCall
|| $arg->value instanceof Expr\MethodCall
|| $arg->value instanceof Expr\StaticCall

@ -688,7 +688,11 @@ trait FuncCallOptimizer
protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c): string
{
return $this->getArg($e, 1) . '.offsetExists(' . $this->getArg($e, 0) . ')';
// The C++ receiver is PHP's second argument, but PHP still evaluates
// the key first. Resolve both in source order before rearranging them.
$key = $this->getArg($e, 0);
$array = $this->getArg($e, 1);
return $array . '.offsetExists(' . $key . ')';
}
protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string

@ -622,12 +622,10 @@ trait BinaryOpTrait
}
$type = $this->detectTypeOfExpr($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.
// C++17 evaluates the braced-list elements in order. The temporary
// is still required because lowering a later operand may append
// captured beforeStmtLines ahead of the entire concat expression;
// without it, those statements could overtake an earlier Call.
$parsed = $this->parseOrderedOperand($item, false);
$argList[] = $this->prepareConcatOperand($parsed, $type);
}

@ -296,9 +296,9 @@ trait MethodCallTrait
}
$class = '';
// 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)`.
// C++17 sequences a member-call receiver before its arguments, but
// lowering an argument may hoist captured beforeStmtLines ahead of the
// whole call. Materialize an effectful receiver before parsing args.
$object = empty($expr->args)
? $this->parseIdentifier($expr->var)
: $this->parseOrderedOperand($expr->var, false);

@ -13,6 +13,12 @@ function combine(string $left, string $right): string
return $left . $right;
}
function tracedCombine(string $label, string $left, string $right): string
{
echo $label, "\n";
return $left . $right;
}
class CallOrder
{
public function method(string $left, string $right): string
@ -32,6 +38,10 @@ function main(): void
$function = 'combine';
var_dump($function(traced('dynamic-function-left', 'i'), traced('dynamic-function-right', 'j')));
var_dump($function(
tracedCombine('nested-left-done', traced('nested-a', 'm'), traced('nested-b', 'n')),
tracedCombine('nested-right-done', traced('nested-c', 'o'), traced('nested-d', 'p')),
));
$object = new CallOrder();
var_dump($object->method(traced('method-left', 'c'), traced('method-right', 'd')));
@ -50,6 +60,13 @@ string(2) "ab"
dynamic-function-left
dynamic-function-right
string(2) "ij"
nested-a
nested-b
nested-left-done
nested-c
nested-d
nested-right-done
string(4) "mnop"
method-left
method-right
string(2) "cd"

@ -12,10 +12,30 @@ function makeUser(string $name, int $age, string $city = "Beijing", bool $vip =
];
}
function traced(string $label, mixed $value): mixed
{
echo $label, "\n";
return $value;
}
function collect(string $first = "root", ...$items): array
{
return [$first, $items];
}
function main(): void
{
var_dump(makeUser(age: 20, name: "Tom", vip: true));
var_dump(makeUser("Jane", city: "Shanghai", age: 18));
var_dump(makeUser(
age: traced('age', 20),
name: traced('name', 'Tom'),
vip: traced('vip', true),
));
var_dump(collect(
extra: traced('extra', 5),
first: traced('first', 'B'),
));
}
?>
--EXPECT--
@ -39,3 +59,27 @@ array(4) {
["vip"]=>
bool(false)
}
age
name
vip
array(4) {
["name"]=>
string(3) "Tom"
["age"]=>
int(20)
["city"]=>
string(7) "Beijing"
["vip"]=>
bool(true)
}
extra
first
array(2) {
[0]=>
string(1) "B"
[1]=>
array(1) {
["extra"]=>
int(5)
}
}

@ -5,6 +5,11 @@ Native optimized calls evaluate arguments left-to-right
var_dump(str_repeat((print "repeat-left\n") ? "x" : "x", (print "repeat-right\n") + 1));
var_dump(round((print "round-left\n") + 1.25, (print "round-right\n")));
var_dump(strcmp((print "cmp-left\n") ? "a" : "a", (print "cmp-right\n") ? "b" : "b"));
$array = ['key' => true];
var_dump(array_key_exists(
(print "exists-key\n") ? 'key' : 'key',
(print "exists-array\n") ? $array : $array,
));
$text = '<x>';
var_dump($text->replace((print "method-left\n") ? "x" : "x", (print "method-right\n") ? "y" : "y"));
?>
@ -18,6 +23,9 @@ float(2.3)
cmp-left
cmp-right
int(-1)
exists-key
exists-array
bool(true)
method-left
method-right
string(3) "<y>"

Loading…
Cancel
Save