diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 7a8dc31c..3529a95d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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); diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index a9b6e344..5a36c7ec 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -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 diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index c347e319..2e9c5fb9 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -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 diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 93f9b526..d6e0ec47 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -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); } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index ec09b828..08ca8dbe 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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); diff --git a/tests/compiler/callable/call-argument-eval-order.phpt b/tests/compiler/callable/call-argument-eval-order.phpt index d7ed656b..71528a06 100644 --- a/tests/compiler/callable/call-argument-eval-order.phpt +++ b/tests/compiler/callable/call-argument-eval-order.phpt @@ -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" diff --git a/tests/compiler/functions/named-args-native.phpt b/tests/compiler/functions/named-args-native.phpt index 4e22465a..c3356675 100644 --- a/tests/compiler/functions/named-args-native.phpt +++ b/tests/compiler/functions/named-args-native.phpt @@ -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) + } +} diff --git a/tests/compiler/stdlib/native-arg-eval-order.phpt b/tests/compiler/stdlib/native-arg-eval-order.phpt index ac72de80..50b3628d 100644 --- a/tests/compiler/stdlib/native-arg-eval-order.phpt +++ b/tests/compiler/stdlib/native-arg-eval-order.phpt @@ -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 = ''; 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) ""