feat(php): 添加对原生函数调用中命名参数的支持

- 在 checkNativeCallArgs 中验证命名参数
- 实现 getFunctionArgNameIndex 和 getVariadicArgIndex 辅助方法
- 添加 validateNativeNamedCallArgs 方法验证命名参数规则
- 重构函数调用参数处理逻辑支持命名参数重排序
- 实现变长参数和命名参数的混合处理
- 添加构建原生变长参数的方法
- 更新参数类型转换逻辑
- 添加相关测试用例验证功能正确性
pull/1/head
韩天峰 3 months ago
parent 279fe22d82
commit fc2cc22594
  1. 10
      phpunit/code/native-call-duplicate-named-arg.php
  2. 10
      phpunit/code/native-call-named-overwrites-positional.php
  3. 10
      phpunit/code/native-call-positional-after-named.php
  4. 10
      phpunit/code/native-call-unknown-named-arg.php
  5. 20
      phpunit/src/FunctionTest.php
  6. 156
      src/Php/CompilerBase.php
  7. 4
      src/Php/Translator.php
  8. 41
      tests/aot/functions/named-args-native.phpt
  9. 52
      tests/aot/functions/named-variadic-native.phpt

@ -0,0 +1,10 @@
<?php
function target(int $value): void
{
}
function main(): void
{
target(value: 1, value: 2);
}

@ -0,0 +1,10 @@
<?php
function target(int $value): void
{
}
function main(): void
{
target(1, value: 2);
}

@ -0,0 +1,10 @@
<?php
function target(int $a, int $b): void
{
}
function main(): void
{
target(a: 1, 2);
}

@ -0,0 +1,10 @@
<?php
function target(int $value): void
{
}
function main(): void
{
target(missing: 1);
}

@ -7,4 +7,24 @@ class FunctionTest extends \BaseTest
$this->exec('The return type of the function `test` cannot be a reference type', 'function-return-ref.php');
}
public function testNativeCallUnknownNamedArgument()
{
$this->exec('Unknown named argument `missing`', 'native-call-unknown-named-arg.php');
}
public function testNativeCallDuplicateNamedArgument()
{
$this->exec('Duplicate named argument `value`', 'native-call-duplicate-named-arg.php');
}
public function testNativeCallPositionalAfterNamedArgument()
{
$this->exec('Cannot use positional argument after named argument', 'native-call-positional-after-named.php');
}
public function testNativeCallNamedArgumentOverwritesPositionalArgument()
{
$this->exec('Named argument `value` overwrites previous argument', 'native-call-named-overwrites-positional.php');
}
}

@ -1682,6 +1682,8 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function checkNativeCallArgs(CallLike $expr, FunctionDef $funcDef, array $args, string $name): void
{
$this->validateNativeNamedCallArgs($funcDef, $args);
$argc = count($args);
$type = str_contains($name, '::') ? 'Method' : 'Function';
if ($argc < $funcDef->argCountRequired) {
@ -1691,6 +1693,76 @@ class CompilerBase extends \PhpAot\Core\Translator
}
}
protected function getFunctionArgNameIndex(FunctionDef $functionDef): array
{
$argNameIndex = [];
foreach ($functionDef->argInfoList as $k => $argInfo) {
$argNameIndex[$argInfo->name] = $k;
}
return $argNameIndex;
}
protected function getVariadicArgIndex(FunctionDef $functionDef): ?int
{
$lastIndex = count($functionDef->argInfoList) - 1;
if ($lastIndex >= 0 and $functionDef->argInfoList[$lastIndex]->variadic) {
return $lastIndex;
}
return null;
}
protected function validateNativeNamedCallArgs(FunctionDef $functionDef, array $callArgs): void
{
$hasNamedArg = false;
$seenNamedArgs = [];
$providedArgIndexes = [];
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
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 and $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;
}
}
protected function getNativeMethod(CallLike $expr, string $class, string $method, bool $checkArgs = true): string|false
{
if (!$this->hasClass($class)) {
@ -2540,20 +2612,26 @@ class CompilerBase extends \PhpAot\Core\Translator
$argList = [];
$functionDef = $this->getFunction($nativeFunc);
$args = [];
$variadicArgs = [];
$hasNamedArg = false;
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
// 对命名参数进行重排
foreach ($callArgs as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
if ($arg->name) {
foreach ($functionDef->argInfoList as $k => $argInfo) {
if ($argInfo->name === $arg->name->name) {
$args[$k] = $arg;
$hasNamedArg = true;
break;
}
$argName = $arg->name->name;
$k = $argNameIndex[$argName] ?? null;
if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) {
$args[$k] = $arg;
} else {
$variadicArgs[] = [$argName, $arg];
}
$hasNamedArg = true;
} elseif ($variadicArgIndex !== null and $i >= $variadicArgIndex) {
$variadicArgs[] = [null, $arg];
} else {
$args[$i] = $arg;
}
@ -2562,6 +2640,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($hasNamedArg) {
// 命名参数中间存在空洞,需要使用默认参数填充
foreach ($functionDef->argInfoList as $k => $argInfo) {
if ($variadicArgIndex !== null and $k === $variadicArgIndex) {
continue;
}
if (!isset($args[$k])) {
if ($argInfo->defaultValue === null) {
$this->fatalError($callArgs[$i], 'Named argument `' . $argInfo->name . '` is missing default value');
@ -2572,42 +2653,52 @@ class CompilerBase extends \PhpAot\Core\Translator
ksort($args);
}
if ($variadicArgIndex !== null and $variadicArgs) {
$args[$variadicArgIndex] = $this->buildNativeVariadicArg($variadicArgs, $functionDef->argInfoList[$variadicArgIndex]);
ksort($args);
}
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
if (count($args) === 0 and count($functionDef->argInfoList) === 1 and $functionDef->argInfoList[0]->variadic) {
return '{}';
}
foreach ($args as $i => $arg) {
if (is_string($arg)) {
$argList[] = $arg;
continue;
}
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
if ($argInfo->variadic) {
$argsSlice = array_slice($args, $i);
if (count($argsSlice) === 1 and $argsSlice[0]->unpack) {
if ($this->isVarExpr($arg->value)) {
$var =$this->parseIdentifier($arg->value);
if ($this->getVarType($var) === self::TYPE_ARRAY) {
$argList[] = $var;
break;
}
}
$argList[] = $this->convertArrayExpr($this->parseExpr($arg->value));
} else {
$tmpVar = $this->addTmpVar(self::TYPE_ARRAY);
foreach ($argsSlice as $item) {
if ($item->unpack) {
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArg($item) . ');';
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->parseArg($item) . ');';
}
}
$argList[] = $tmpVar;
break;
$argList[] = $this->getTypeConvertedArg($arg, $argInfo);
}
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)) {
$var = $this->parseIdentifier($arg->value);
if ($this->getVarType($var) === self::TYPE_ARRAY) {
return $var;
}
} else {
$argList[] = $this->getTypeConvertedArg($arg, $argInfo);
}
return $this->convertArrayExpr($this->parseExpr($arg->value));
}
return implode(', ', $argList);
$tmpVar = $this->addTmpVar(self::TYPE_ARRAY);
foreach ($variadicArgs as [$name, $arg]) {
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) . ');';
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
}
}
return $tmpVar;
}
protected function parseNamedCallArgs(array $args, int $firstIndex, array $listArgs): string
@ -2623,6 +2714,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if (!$this->isIdExpr($arg->name)) {
$this->fatalError($arg, 'Named argument must be a string');
}
if (array_key_exists($arg->name->name, $namedArgs)) {
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = $this->parseArg($arg);
}

@ -2386,8 +2386,8 @@ CODE;
$this->addArgument('this_', self::TYPE_OBJECT);
}
foreach ($this->functionDef->argInfoList as $argInfo) {
$this->addArgument($argInfo->name, $argInfo->type);
if ($argInfo->class) {
$this->addArgument($argInfo->name, $argInfo->variadic ? self::TYPE_ARRAY : $argInfo->type);
if (!$argInfo->variadic and $argInfo->class) {
$this->addObject($argInfo->name, $argInfo->class);
}
}

@ -0,0 +1,41 @@
--TEST--
Native function call named arguments are reordered and validated
--FILE--
<?php
function makeUser(string $name, int $age, string $city = "Beijing", bool $vip = false): array
{
return [
"name" => $name,
"age" => $age,
"city" => $city,
"vip" => $vip,
];
}
function main(): void
{
var_dump(makeUser(age: 20, name: "Tom", vip: true));
var_dump(makeUser("Jane", city: "Shanghai", age: 18));
}
?>
--EXPECT--
array(4) {
["name"]=>
string(3) "Tom"
["age"]=>
int(20)
["city"]=>
string(7) "Beijing"
["vip"]=>
bool(true)
}
array(4) {
["name"]=>
string(4) "Jane"
["age"]=>
int(18)
["city"]=>
string(8) "Shanghai"
["vip"]=>
bool(false)
}

@ -0,0 +1,52 @@
--TEST--
Native variadic function calls accept named arguments
--FILE--
<?php
function collect(string $first = "root", ...$items): array
{
return [$first, $items];
}
function collectInts(int ...$numbers): array
{
return $numbers;
}
function main(): void
{
var_dump(collect("A", 1, 2, tail: 3, items: 4));
var_dump(collect(first: "B", extra: 5));
var_dump(collectInts(one: 1, two: 2));
}
?>
--EXPECT--
array(2) {
[0]=>
string(1) "A"
[1]=>
array(4) {
[0]=>
int(1)
[1]=>
int(2)
["tail"]=>
int(3)
["items"]=>
int(4)
}
}
array(2) {
[0]=>
string(1) "B"
[1]=>
array(1) {
["extra"]=>
int(5)
}
}
array(2) {
["one"]=>
int(1)
["two"]=>
int(2)
}
Loading…
Cancel
Save