feat(php): 实现PHP函数调用中的命名参数和参数解包功能

- 添加对命名参数语法的支持,包括参数验证和解析逻辑
- 实现参数解包功能,支持...操作符展开数组参数
- 添加hasNamedCallArg和hasUnpackBeforeNamedArg辅助方法进行参数检查
- 实现genCallUserFuncArray方法用于处理复杂参数场景
- 添加对参数顺序的验证,防止解包后使用命名参数的情况
- 更新parseCallArgs方法以支持分离命名参数和位置参数
- 添加相关测试用例验证新功能的正确性
pull/5/head
韩天峰 2 months ago
parent 0bfa2869c5
commit 00f79ecdc1
  1. 5
      phpunit/src/FunctionTest.php
  2. 186
      src/Php/CompilerBase.php
  3. 49
      tests/aot/functions/unpack-before-named.phpt

@ -37,4 +37,9 @@ class FunctionTest extends \BaseTest
$this->exec('Named argument `replace` is missing default value', 'internal-call-missing-required-named-arg.php'); $this->exec('Named argument `replace` is missing default value', 'internal-call-missing-required-named-arg.php');
} }
public function testUnpackAfterNamedArgument()
{
$this->exec('Cannot use argument unpacking after named arguments', 'unpack-after-named-arg.php');
}
} }

@ -1899,6 +1899,13 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($this->isPlaceholderExpr($arg)) { if ($this->isPlaceholderExpr($arg)) {
continue; continue;
} }
if ($arg instanceof Node\Arg && $arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$providedArgIndexes[$i] = true;
continue;
}
if ($arg->name === null) { if ($arg->name === null) {
if ($hasNamedArg) { if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument'); $this->fatalError($arg, 'Cannot use positional argument after named argument');
@ -2853,6 +2860,44 @@ class CompilerBase extends \PhpAot\Core\Translator
} }
} }
protected function hasNamedCallArg(array $args): bool
{
foreach ($args as $arg) {
if ($arg instanceof Node\Arg && $arg->name !== null) {
return true;
}
}
return false;
}
protected function hasUnpackBeforeNamedArg(array $args): bool
{
$hasUnpack = false;
foreach ($args as $arg) {
if (!$arg instanceof Node\Arg) {
continue;
}
if ($arg->unpack) {
$hasUnpack = true;
} elseif ($hasUnpack && $arg->name !== null) {
return true;
}
}
return false;
}
protected function genCallUserFuncArray(string $callback, array $args, string $funcName = '', string $className = ''): string
{
return 'php::call(' . $this->getFuncPtr('call_user_func_array') . ', '
. Symbol::argList() . '{' . $callback . ', ' . $this->parseCallArgs($args, $funcName, $className, false, true) . '})';
}
protected function getFunctionCallbackExpr(string $nativeFn): string
{
$functionDef = $this->getFunction($nativeFn);
return $this->getLiteralString($functionDef->getNamespacedName());
}
protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void
{ {
$hasNamedArg = false; $hasNamedArg = false;
@ -2876,6 +2921,13 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($this->isPlaceholderExpr($arg)) { if ($this->isPlaceholderExpr($arg)) {
continue; continue;
} }
if ($arg instanceof Node\Arg && $arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$providedArgIndexes[$i] = true;
continue;
}
if ($arg->name === null) { if ($arg->name === null) {
if ($hasNamedArg) { if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument'); $this->fatalError($arg, 'Cannot use positional argument after named argument');
@ -2946,6 +2998,9 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->genPlaceHolder($this->identifierToStr($expr->name)); return $this->genPlaceHolder($this->identifierToStr($expr->name));
} }
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name); $this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
return $this->genCallUserFuncArray($this->getFunctionCallbackExpr($nativeFn), $expr->args, $name);
}
try { try {
return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')'; return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
} catch (PlaceHolder) { } catch (PlaceHolder) {
@ -2971,8 +3026,12 @@ class CompilerBase extends \PhpAot\Core\Translator
if (empty($expr->args)) { if (empty($expr->args)) {
return 'php::call(' . $fn . ')'; return 'php::call(' . $fn . ')';
} }
if ($this->hasNamedCallArg($expr->args)) {
$callback = $name !== '' ? $this->getLiteralString($name) : $fn;
return $this->genCallUserFuncArray($callback, $expr->args, $name);
}
try { try {
return 'php::call(' . $fn . ', ' . $this->parseCallArgs($expr->args, $name) . ')'; return 'php::call(' . $fn . ', ' . $this->parseCallArgs($expr->args, $name, '', $name !== '') . ')';
} catch (PlaceHolder) { } catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder); return $this->genPlaceHolder($placeHolder);
} }
@ -3135,22 +3194,88 @@ class CompilerBase extends \PhpAot\Core\Translator
return $variadicParam !== null && $variadicParam->isPassedByReference(); return $variadicParam !== null && $variadicParam->isPassedByReference();
} }
protected function parseCallArgs(array $args, string $funcName = '', string $className = ''): string protected function parseCallArgs(
array $args,
string $funcName = '',
string $className = '',
bool $separateNamedArgs = true,
bool $forceArrayArgs = false
): string
{ {
$list_args = []; $list_args = [];
$last = array_key_last($args); $arrayArgsVar = null;
$namedArgsVar = null;
$namedArgs = [];
$hasNamedArg = false;
$ensureArrayArgs = function () use (&$arrayArgsVar, &$list_args): string {
if ($arrayArgsVar === null) {
$arrayArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $list_args) . '};';
$list_args = [];
}
return $arrayArgsVar;
};
$ensureNamedArgs = function () use (&$namedArgsVar): string {
if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
}
return $namedArgsVar;
};
$addPositionalArg = function (string $value) use (&$arrayArgsVar, &$list_args): void {
if ($arrayArgsVar !== null) {
$this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');';
} else {
$list_args[] = $value;
}
};
if ($forceArrayArgs) {
$ensureArrayArgs();
}
foreach ($args as $i => $arg) { foreach ($args as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) { if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder(); throw new PlaceHolder();
} }
if ($arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$arrayArgs = $ensureArrayArgs();
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
continue;
}
if ($arg->name !== null) { if ($arg->name !== null) {
return $this->parseNamedCallArgs($args, $i, $list_args); $hasNamedArg = true;
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] = true;
if ($separateNamedArgs) {
$namedArgsArray = $ensureNamedArgs();
$this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $this->parseCallArgValue($arg) . ');';
} else {
$arrayArgs = $ensureArrayArgs();
$this->context->beforeStmtLines[] = $arrayArgs . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $this->parseCallArgValue($arg) . ');';
}
continue;
}
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
} }
$byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i); $byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i);
if ($this->isVarExpr($arg->value)) { if ($this->isVarExpr($arg->value)) {
$name = $this->parseIdentifier($arg->value); $name = $this->parseIdentifier($arg->value);
if ($byRef) { if ($byRef) {
$list_args[] = $this->parseArgRefVar($arg, $name); $addPositionalArg($this->parseArgRefVar($arg, $name));
continue; continue;
} }
if (!$this->hasVar($name)) { if (!$this->hasVar($name)) {
@ -3173,9 +3298,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($byRef) { if ($byRef) {
$ref = $this->addTmpVar(self::TYPE_REF); $ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();'; $this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
$list_args[] = '&' . $ref; $addPositionalArg('&' . $ref);
} else { } else {
$list_args[] = $globalVar; $addPositionalArg($globalVar);
} }
continue; continue;
} }
@ -3186,7 +3311,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($arg->value->dim === null) { if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression'); $this->fatalError($arg, 'Array dimension must be a constant expression');
} }
$list_args[] = $array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')'; $addPositionalArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')');
continue; continue;
} }
} elseif ($this->isFuncCallExpr($arg->value)) { } elseif ($this->isFuncCallExpr($arg->value)) {
@ -3199,12 +3324,12 @@ class CompilerBase extends \PhpAot\Core\Translator
$name = $this->parseVariable($inner); $name = $this->parseVariable($inner);
// 消除 refval() 函数调用,直接使用变量 // 消除 refval() 函数调用,直接使用变量
$arg->value = $inner; $arg->value = $inner;
$list_args[] = $this->parseArgRefVar($arg, $name); $addPositionalArg($this->parseArgRefVar($arg, $name));
continue; continue;
} }
$expr = $this->expandRefvalExpr($inner, $arg); $expr = $this->expandRefvalExpr($inner, $arg);
if ($expr !== null) { if ($expr !== null) {
$list_args[] = $expr; $addPositionalArg($expr);
continue; continue;
} }
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property'); $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
@ -3217,30 +3342,19 @@ class CompilerBase extends \PhpAot\Core\Translator
$tmpRef = $this->genTmpVarName(); $tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF); $this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';'; $this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
$list_args[] = '&' . $tmpRef; $addPositionalArg('&' . $tmpRef);
continue; continue;
} }
} }
// 变长参数展开的语法,例如:array_merge(...$arr) $value = $this->parseCallArgValue($arg);
if ($arg->unpack) { $addPositionalArg($value);
if ($i !== $last) {
$this->fatalError($arg, 'The unpack expression for variadic arguments must be the last');
}
// 如果第一个参数是数组变量,数组展开语法直接传递该变量,没必要创建临时变量
// 例如:function (array $args) { var_dump(...$args); }
if ($i === 0 and $this->isVarExpr($arg->value) and $this->getVarType($arg->value->name) === self::TYPE_ARRAY) {
return $this->parseIdentifier($arg->value);
} else {
$tmpVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $tmpVar . '{' . implode(', ', $list_args) . '};';
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArrayArg($arg) . ');';
return $tmpVar;
}
}
$list_args[] = $this->parseCallArgValue($arg);
} }
return Symbol::argList() . '{' . implode(', ', $list_args) . '}'; if ($arrayArgsVar !== null) {
return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar;
}
$callArgs = Symbol::argList() . '{' . implode(', ', $list_args) . '}';
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
} }
protected function parseCallArgValue(Node\Arg $arg): string protected function parseCallArgValue(Node\Arg $arg): string
@ -4826,6 +4940,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($nativeFunc) { if ($nativeFunc) {
$expr->setAttribute('nativeCall', $nativeFunc); $expr->setAttribute('nativeCall', $nativeFunc);
try { try {
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $funcName, $class);
}
return $this->parseNativeMethodCall($object, $nativeFunc, $expr->args); return $this->parseNativeMethodCall($object, $nativeFunc, $expr->args);
} catch (PlaceHolder) { } catch (PlaceHolder) {
return $this->genPlaceHolder($this->genArray([$object, $method])); return $this->genPlaceHolder($this->genArray([$object, $method]));
@ -4879,9 +4996,12 @@ class CompilerBase extends \PhpAot\Core\Translator
if (empty($expr->args)) { if (empty($expr->args)) {
return $object . '.call(' . $methodPtr . ')'; return $object . '.call(' . $methodPtr . ')';
} }
if ($this->hasNamedCallArg($expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $funcName, $class);
}
try { try {
$class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class; $class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class;
return $object . '.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $funcName, $class) . ')'; return $object . '.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $funcName, $class, false) . ')';
} catch (PlaceHolder) { } catch (PlaceHolder) {
return $this->genPlaceHolder($this->genArray([$object, $method])); return $this->genPlaceHolder($this->genArray([$object, $method]));
} }
@ -4969,6 +5089,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($nativeFunc) { if ($nativeFunc) {
try { try {
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
return $this->genCallUserFuncArray($this->genArray($callScope), $expr->args, $method, $class);
}
$args = $this->parseNativeCallArgs($expr->args, $nativeFunc); $args = $this->parseNativeCallArgs($expr->args, $nativeFunc);
$expr->setAttribute('nativeCall', $nativeFunc); $expr->setAttribute('nativeCall', $nativeFunc);
} catch (PlaceHolder) { } catch (PlaceHolder) {
@ -5004,6 +5127,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if (empty($expr->args)) { if (empty($expr->args)) {
return $call . '(' . $fn . ')'; return $call . '(' . $fn . ')';
} }
if ($this->hasNamedCallArg($expr->args)) {
return $this->genCallUserFuncArray($placeHolder, $expr->args);
}
try { try {
$callArgs = $this->parseCallArgs($expr->args); $callArgs = $this->parseCallArgs($expr->args);
return $call . '(' . $fn . ', ' . $callArgs . ')'; return $call . '(' . $fn . ', ' . $callArgs . ')';

@ -0,0 +1,49 @@
--TEST--
Argument unpacking can be followed by named arguments
--FILE--
<?php
function pair($a, $b): array
{
return [$a, $b];
}
class DynamicUnpackNamed
{
public function pair($a, $b): array
{
return [$a, $b];
}
}
function main(): void
{
var_dump(pair(...[1], b: 2));
$fn = 'pair';
var_dump($fn(...[3], b: 4));
$object = new DynamicUnpackNamed();
$method = 'pair';
var_dump($object->$method(...[5], b: 6));
}
?>
--EXPECT--
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
array(2) {
[0]=>
int(3)
[1]=>
int(4)
}
array(2) {
[0]=>
int(5)
[1]=>
int(6)
}
Loading…
Cancel
Save