diff --git a/phpunit/src/FunctionTest.php b/phpunit/src/FunctionTest.php index d04e7e4b..2209d56f 100644 --- a/phpunit/src/FunctionTest.php +++ b/phpunit/src/FunctionTest.php @@ -27,4 +27,14 @@ class FunctionTest extends \BaseTest $this->exec('Named argument `value` overwrites previous argument', 'native-call-named-overwrites-positional.php'); } + public function testInternalCallUnknownNamedArgument() + { + $this->exec('Unknown named argument `foo`', 'internal-call-unknown-named-arg.php'); + } + + public function testInternalCallMissingRequiredNamedArgument() + { + $this->exec('Named argument `replace` is missing default value', 'internal-call-missing-required-named-arg.php'); + } + } diff --git a/src/Php/Analysis/SsaBuilder.php b/src/Php/Analysis/SsaBuilder.php index abf5bb23..46ea8a1c 100644 --- a/src/Php/Analysis/SsaBuilder.php +++ b/src/Php/Analysis/SsaBuilder.php @@ -206,8 +206,9 @@ class SsaBuilder private array $argInfoList = [] ) { foreach ($this->argInfoList as $argInfo) { - $this->params[] = $argInfo->name; - $this->paramByRef[$argInfo->name] = $argInfo->byRef ?? false; + $paramName = $argInfo->phpName ?: $argInfo->name; + $this->params[] = $paramName; + $this->paramByRef[$paramName] = $argInfo->byRef ?? false; } } diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 70902959..93843742 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -15,6 +15,7 @@ use PhpParser\NodeAbstract; class ArgInfo { public string $name; + public string $phpName = ''; public string $type; public string $default = ''; public ?ArrayInitPlan $arrayInitPlan = null; diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 733299df..af79a610 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -1873,7 +1873,7 @@ class CompilerBase extends \PhpAot\Core\Translator { $argNameIndex = []; foreach ($functionDef->argInfoList as $k => $argInfo) { - $argNameIndex[$argInfo->name] = $k; + $argNameIndex[$argInfo->phpName ?: $this->unescapeVarName($argInfo->name)] = $k; } return $argNameIndex; } @@ -2841,6 +2841,7 @@ class CompilerBase extends \PhpAot\Core\Translator if (!$ref) { return; } + $this->validateInternalNamedCallArgs($ref, $expr->args); $minArgs = $ref->getNumberOfRequiredParameters(); $maxArgs = $ref->getNumberOfParameters(); $actualArgCount = count($expr->args); @@ -2852,6 +2853,77 @@ class CompilerBase extends \PhpAot\Core\Translator } } + protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void + { + $hasNamedArg = false; + $seenNamedArgs = []; + $providedArgIndexes = []; + $argNameIndex = []; + $requiredArgIndexes = []; + $variadicArgIndex = null; + + foreach ($ref->getParameters() as $i => $param) { + $argNameIndex[$param->getName()] = $i; + if (!$param->isOptional() && !$param->isVariadic()) { + $requiredArgIndexes[$i] = $param->getName(); + } + if ($param->isVariadic()) { + $variadicArgIndex = $i; + } + } + + foreach ($callArgs as $i => $arg) { + if ($this->isPlaceholderExpr($arg)) { + continue; + } + if ($arg->name === null) { + if ($hasNamedArg) { + $this->fatalError($arg, 'Cannot use positional argument after named argument'); + } + $providedArgIndexes[$i] = true; + continue; + } + if (!$this->isIdExpr($arg->name)) { + $this->fatalError($arg, 'Named argument must be a string'); + } + + $argName = $arg->name->name; + if (isset($seenNamedArgs[$argName])) { + $this->fatalError($arg, "Duplicate named argument `{$argName}`"); + } + if (!array_key_exists($argName, $argNameIndex)) { + if ($variadicArgIndex === null) { + $this->fatalError($arg, "Unknown named argument `{$argName}`"); + } + $seenNamedArgs[$argName] = true; + $hasNamedArg = true; + continue; + } + + $argIndex = $argNameIndex[$argName]; + if ($variadicArgIndex !== null && $argIndex === $variadicArgIndex) { + $seenNamedArgs[$argName] = true; + $hasNamedArg = true; + continue; + } + if (isset($providedArgIndexes[$argIndex])) { + $this->fatalError($arg, "Named argument `{$argName}` overwrites previous argument"); + } + + $seenNamedArgs[$argName] = true; + $providedArgIndexes[$argIndex] = true; + $hasNamedArg = true; + } + + if ($hasNamedArg) { + foreach ($requiredArgIndexes as $index => $name) { + if (!isset($providedArgIndexes[$index])) { + $this->fatalError($callArgs[array_key_last($callArgs)] ?? null, "Named argument `{$name}` is missing default value"); + } + } + } + } + protected function parseFuncCall(Expr\FuncCall $expr): string { if ($this->isVarExpr($expr->name)) { @@ -2954,7 +3026,8 @@ class CompilerBase extends \PhpAot\Core\Translator break; } } - $this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argInfo->name . '` is missing default value'); + $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + $this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argName . '` is missing default value'); } $args[$k] = new Node\Arg($argInfo->defaultValue); } @@ -3891,7 +3964,8 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isTypedObject($object)) { $class = $this->getObjectType($object); if ($class and $argInfo->class and !$this->isInheritedFrom($class, $argInfo->class)) { - $this->fatalError($arg, "Argument `{$argInfo->name}` must be an instance of `{$argInfo->class}`, `{$class}` given"); + $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + $this->fatalError($arg, "Argument `{$argName}` must be an instance of `{$argInfo->class}`, `{$class}` given"); } } } diff --git a/src/Php/Generator/ClosureGenerator.php b/src/Php/Generator/ClosureGenerator.php index 9ee09f9a..401a9523 100644 --- a/src/Php/Generator/ClosureGenerator.php +++ b/src/Php/Generator/ClosureGenerator.php @@ -75,6 +75,7 @@ trait ClosureGenerator $this->fatalError($expr, 'Closure cannot use reference parameter'); } $var = $this->parseIdentifier($param->var); + $phpName = is_string($param->var->name) ? $param->var->name : $this->unescapeVarName($var); if ($param->variadic) { $code .= $this->getIndent() . self::TYPE_ARRAY . ' ' . $var . ';' . PHP_EOL; $code .= $this->getIndent() . 'for (uint32_t i = ' . $i . '; i < php::getCallArgNum(); i++) {' . PHP_EOL; @@ -83,7 +84,7 @@ trait ClosureGenerator $this->indentLevel--; $code .= $this->getIndent() . '}' . PHP_EOL; $this->addArgument($var, self::TYPE_ARRAY); - $code .= $this->genClosureParamTypeCheck($param, $var, $i, true); + $code .= $this->genClosureParamTypeCheck($param, $var, $phpName, $i, true); continue; } $argExpr = $param->default === null @@ -91,7 +92,7 @@ trait ClosureGenerator : 'php::getCallArg(' . $i . ', ' . $this->parseParamDefaultValue($param->default) . ')'; $code .= $this->getIndent() . 'auto ' . $var . ' = ' . $argExpr . ';' . PHP_EOL; $this->addArgument($var, self::TYPE_VAR); - $code .= $this->genClosureParamTypeCheck($param, $var, $i, false); + $code .= $this->genClosureParamTypeCheck($param, $var, $phpName, $i, false); } foreach ($uses as $i => $useItem) { @@ -164,7 +165,7 @@ trait ClosureGenerator return $code; } - private function genClosureParamTypeCheck(Node\Param $param, string $var, int $index, bool $variadic): string + private function genClosureParamTypeCheck(Node\Param $param, string $var, string $phpName, int $index, bool $variadic): string { if (!$param->type instanceof NullableType && !$param->type instanceof UnionType && !$param->type instanceof IntersectionType) { return ''; @@ -177,6 +178,7 @@ trait ClosureGenerator $argInfo = new ArgInfo(); $argInfo->name = $var; + $argInfo->phpName = $phpName; $argInfo->type = self::TYPE_VAR; $argInfo->variadic = $variadic; $argInfo->typeCheck = $typeInfo['check']; diff --git a/src/Php/Generator/PropertyPromotion.php b/src/Php/Generator/PropertyPromotion.php index 1ef954de..edea815e 100644 --- a/src/Php/Generator/PropertyPromotion.php +++ b/src/Php/Generator/PropertyPromotion.php @@ -15,7 +15,8 @@ trait PropertyPromotion protected function genPropertyPromotion(ArgInfo $argInfo): string { $code = ''; - $code .= 'this_.setProperty(' . $this->genCharPtr($argInfo->name) . ', ' . $argInfo->name . ')'; + $propertyName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + $code .= 'this_.setProperty(' . $this->genCharPtr($propertyName) . ', ' . $argInfo->name . ')'; $code .= ";\n"; return $code; } diff --git a/src/Php/Generator/TypeCheckGenerator.php b/src/Php/Generator/TypeCheckGenerator.php index 990ad37f..c0ee0699 100644 --- a/src/Php/Generator/TypeCheckGenerator.php +++ b/src/Php/Generator/TypeCheckGenerator.php @@ -265,7 +265,7 @@ trait TypeCheckGenerator protected function genUnionParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string { $fnName = $this->getTypeCheckCallableName(); - $paramName = $this->unescapeVarName($argInfo->name); + $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); return 'php::concat({' . 'php::Str(' . $this->genCharPtr($fnName . '(): Argument #', true) . '), ' . 'php::toString(' . $argNoExpr . '), ' @@ -381,7 +381,7 @@ trait TypeCheckGenerator protected function genClosureParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string { - $paramName = $this->unescapeVarName($argInfo->name); + $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); return 'php::concat({' . 'php::Str(' . $this->genCharPtr('{closure}(): Argument #', true) . '), ' . 'php::toString(' . $argNoExpr . '), ' diff --git a/src/Php/Optimizer/FuncCallOptimizer.php b/src/Php/Optimizer/FuncCallOptimizer.php index f0b9a08d..9a82847c 100644 --- a/src/Php/Optimizer/FuncCallOptimizer.php +++ b/src/Php/Optimizer/FuncCallOptimizer.php @@ -195,6 +195,12 @@ trait FuncCallOptimizer protected function parseFuncCallWithOptimizer(string $name, Node\Expr\FuncCall $expr): string|false { + foreach ($expr->args as $arg) { + if ($arg instanceof Node\Arg && $arg->name !== null) { + return false; + } + } + $config = $this->getFuncCallConfig()[$name] ?? null; if ($config === null) { return false; diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index feff1478..13097bb8 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -249,21 +249,25 @@ class Preprocessor extends CompilerBase $last = array_key_last($params); foreach ($params as $i => $param) { + if (!is_string($param->var->name)) { + $this->fatalError($param, 'Parameter name must be a string'); + } + $phpName = $param->var->name; + $name = $this->escapeVarName($phpName); // .stub 存根定义 C++ Native 函数,必须设置函数的参数类型 if ($this->stubFile and !$param->type) { - throw new \RuntimeException('No type for ' . $this->parseIdentifier($param->var)); + throw new \RuntimeException('No type for ' . $phpName); } // 构造方法属性定义语法(Constructor Property Promotion) if ($param->isPromoted()) { if (!$this->classDef or !$this->methodDef or $this->methodDef->name !== '__construct') { $this->fatalError($param, 'Promoted properties are not supported'); } - $name = $this->parseIdentifier($param->var); $nullable = $param->type instanceof NullableType; // Promoted property defaults belong to the constructor parameter, // not to the property default table. The property itself must stay // uninitialized until __construct assigns it. - $this->addClassProperty($name, $param->flags, $param->type, null, $nullable, $param); + $this->addClassProperty($phpName, $param->flags, $param->type, null, $nullable, $param); } if ($param->variadic) { if ($i !== $last) { @@ -272,13 +276,13 @@ class Preprocessor extends CompilerBase $this->fatalError($param, 'Variadic parameters cannot be passed by reference'); } } - $name = $this->parseIdentifier($param->var); if ($this->method and $name === 'this_') { $this->fatalError($param, 'Cannot use `$this` as parameter of class method'); } $argInfo = new ArgInfo(); $type = $this->parseParameterType($param, $argInfo, $name); $argInfo->name = $name; + $argInfo->phpName = $phpName; $argInfo->type = $type; $argInfo->byRef = $param->byRef; $argInfo->variadic = $param->variadic; diff --git a/tests/aot/closure/closure-composite-type-check.phpt b/tests/aot/closure/closure-composite-type-check.phpt new file mode 100644 index 00000000..457acac4 --- /dev/null +++ b/tests/aot/closure/closure-composite-type-check.phpt @@ -0,0 +1,82 @@ +--TEST-- +Closure composite type declarations use runtime type checks +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- +getMessage(), "\n"; + } + try { + $union([]); + } catch (\TypeError $e) { + echo $e->getMessage(), "\n"; + } + try { + $variadic(1, []); + } catch (\TypeError $e) { + echo $e->getMessage(), "\n"; + } + try { + $intersection(new ClosureOnlyA()); + } catch (\TypeError $e) { + echo $e->getMessage(), "\n"; + } + try { + $returnUnion([]); + } catch (\TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +NULL +string(2) "ok" +array(2) { + [0]=> + int(1) + [1]=> + string(3) "two" +} +string(11) "ClosureBoth" +int(42) +{closure}(): Argument #1 ($value) must be of type ?int, string given +{closure}(): Argument #1 ($union) must be of type int|string, array given +{closure}(): Argument #2 ($values) must be of type int|string, array given +{closure}(): Argument #1 ($value) must be of type ClosureIA&ClosureIB, object given +{closure}(): Return value must be of type int|string, array given diff --git a/tests/aot/closure/closure-param-defaults.phpt b/tests/aot/closure/closure-param-defaults.phpt new file mode 100644 index 00000000..51eff8ec --- /dev/null +++ b/tests/aot/closure/closure-param-defaults.phpt @@ -0,0 +1,41 @@ +--TEST-- +Closure parameters handle defaults and variadic arguments +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- +getMessage()); + } +} +?> +--EXPECT-- +int(42) +array(3) { + [0]=> + int(1) + [1]=> + string(3) "two" + [2]=> + NULL +} +string(18) "ArgumentCountError" +string(74) "Too few arguments to function {closure}(), 0 passed and exactly 1 expected" diff --git a/tests/aot/functions/internal-named-args.phpt b/tests/aot/functions/internal-named-args.phpt new file mode 100644 index 00000000..6d93c617 --- /dev/null +++ b/tests/aot/functions/internal-named-args.phpt @@ -0,0 +1,16 @@ +--TEST-- +Internal function named arguments are validated before optimization +--FILE-- + +--EXPECT-- +int(3) +string(3) "xbc" +string(2) "de" diff --git a/tests/aot/named_args/007.phpt b/tests/aot/named_args/007.phpt new file mode 100644 index 00000000..705bdaff --- /dev/null +++ b/tests/aot/named_args/007.phpt @@ -0,0 +1,37 @@ +--TEST-- +Named arguments and promoted properties with C++ reserved parameter names +--FILE-- +union); + var_dump($object->class); +} +?> +--EXPECT-- +array(2) { + [0]=> + int(10) + [1]=> + int(20) +} +int(42) +string(5) "named" diff --git a/tests/aot/optimizations/reserved-param-ssa.phpt b/tests/aot/optimizations/reserved-param-ssa.phpt new file mode 100644 index 00000000..15d0ac61 --- /dev/null +++ b/tests/aot/optimizations/reserved-param-ssa.phpt @@ -0,0 +1,21 @@ +--TEST-- +SSA optimizations handle parameters with C++ reserved names +--FILE-- + +--EXPECT-- +int(12)