feat(compiler): 支持PHP变长参数函数特性

- 实现变长参数语法解析和编译支持
- 添加变长参数必须位于最后位置的验证逻辑
- 支持变长参数不能通过引用传递的限制检查
- 实现变长参数默认值作为空数组的处理机制
- 添加对变长参数函数调用时参数数量的检查功能
- 支持除法运算符返回类型推断的特殊情况处理
- 优化函数调用参数解析中的变长参数展开逻辑
- 添加多个变长参数函数测试用例覆盖不同场景
pull/1/head
韩天峰 5 months ago
parent 096a92a62e
commit 07200b9e12
  1. 45
      src/Php/CompilerBase.php
  2. 5
      src/Php/Entity/FunctionDef.php
  3. 2
      src/Php/Translator.php
  4. 40
      tests/aot/variadic/calculator.phpt
  5. 56
      tests/aot/variadic/misc.phpt
  6. 26
      tests/aot/variadic/multiply.phpt
  7. 39
      tests/aot/variadic/sum.phpt

@ -999,8 +999,12 @@ class CompilerBase extends \PhpAot\Core\Translator
$propertyDef = new PropertyDef($name, $param->flags, $type, $default, $nullable);
$this->classDef->properties[$name] = $propertyDef;
}
if ($param->variadic and $i !== $last) {
$this->fatalError($param, 'Variadic parameters must be the last parameter');
if ($param->variadic) {
if ($i !== $last) {
$this->fatalError($param, 'Variadic parameters must be the last parameter');
} elseif ($param->byRef) {
$this->fatalError($param, 'Variadic parameters cannot be passed by reference');
}
}
$name = $this->parseIdentifier($param->var);
$type = $this->parseParameterType($param, $name);
@ -1022,6 +1026,11 @@ class CompilerBase extends \PhpAot\Core\Translator
$defaultValueCount++;
$argInfo->default = $this->parseParamDefaultValue($param->default);
$argInfo->defaultValue = $param->default;
} elseif ($param->variadic) {
// 变长参数可以视为空数组默认值
$defaultValueCount++;
$argInfo->default = '{}';
$argInfo->defaultValue = new Node\Expr\Array_();
}
$functionDef->argInfoList[] = $argInfo;
}
@ -1595,6 +1604,17 @@ class CompilerBase extends \PhpAot\Core\Translator
}
}
protected function checkNativeCallArgs(CallLike $expr, FunctionDef $funcDef, array $args, string $name): void
{
$argc = count($args);
$type = str_contains($name, '::') ? 'Method' : 'Function';
if ($argc < $funcDef->argCountRequired) {
$this->fatalError($expr, $type . ' `' . $name . '()` requires ' . $funcDef->argCountRequired . ' arguments, ' . $argc . ' given');
} elseif (!$funcDef->hasVariadicArg() and count($expr->args) > count($funcDef->argInfoList)) {
$this->fatalError($expr, $type . ' `' . $name . '()` accepts ' . count($funcDef->argInfoList) . ' arguments, ' . $argc . ' given');
}
}
protected function getNativeMethod(CallLike $expr, string $class, string $method): string|false
{
if (!$this->hasNativeClass($class)) {
@ -1635,11 +1655,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return false;
}
if (count($expr->args) < $methodDef->functionDef->argCountRequired) {
$this->fatalError($expr, 'Method `' . $classDef->getNamespacedName() . '::' . $method . '()` requires ' . $methodDef->functionDef->argCountRequired . ' arguments, ' . count($expr->args) . ' given');
} elseif (count($expr->args) > count($methodDef->functionDef->argInfoList)) {
$this->fatalError($expr, 'Method `' . $classDef->getNamespacedName() . '::' . $method . '()` accepts ' . count($methodDef->functionDef->argInfoList) . ' arguments, ' . count($expr->args) . ' given');
}
$this->checkNativeCallArgs($expr, $methodDef->functionDef, $expr->args, $classDef->getNamespacedName() . '::' . $method);
return $this->getNativeName($method, $classDef->namespace, $classDef->name);
}
@ -1697,6 +1713,14 @@ class CompilerBase extends \PhpAot\Core\Translator
return self::TYPE_FLOAT;
}
if ($leftType === self::TYPE_INT || $rightType === self::TYPE_INT) {
// 除法存在特殊性,若未能整除,会返回浮点数,其他则一律视为整数
if ($exprType === 'Expr_BinaryOp_Div') {
if ($leftType === self::TYPE_INT && $rightType === self::TYPE_INT) {
return self::TYPE_INT;
} else {
return self::TYPE_VAR;
}
}
return self::TYPE_INT;
}
break;
@ -2205,6 +2229,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$nativeFn = $this->findNativeFunction($name);
if ($nativeFn) {
$expr->setAttribute('nativeCall', $nativeFn);
$this->checkNativeCallArgs($expr, $this->getNativeFunction($nativeFn), $expr->args, $name);
try {
return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
} catch (PlaceHolder) {
@ -2272,6 +2297,11 @@ class CompilerBase extends \PhpAot\Core\Translator
ksort($args);
}
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
if (count($args) === 0 and count($functionDef->argInfoList) === 1 and $functionDef->argInfoList[0]->variadic) {
return '{}';
}
foreach ($args as $i => $arg) {
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
if ($argInfo->variadic) {
@ -2290,7 +2320,6 @@ class CompilerBase extends \PhpAot\Core\Translator
foreach ($argsSlice as $item) {
if ($item->unpack) {
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArg($item) . ');';
break;
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->parseArg($item) . ');';
}

@ -35,4 +35,9 @@ class FunctionDef
{
return $this->namespace ? $this->namespace . '\\' . $this->name : $this->name;
}
public function hasVariadicArg(): bool
{
return $this->argInfoList && $this->argInfoList[count($this->argInfoList) - 1]->variadic;
}
}

@ -420,7 +420,7 @@ class Translator extends Preprocessor
if ($argInfoList) {
foreach ($argInfoList as $argInfo) {
if ($argInfo->variadic) {
$arg = self::TYPE_ARRAY . ' ' . $argInfo->name;
$arg = self::TYPE_ARRAY . ' ' . $argInfo->name . ' = {}';
} else {
$arg = $argInfo->type . ' ' . $argInfo->name;
if ($argInfo->default) {

@ -0,0 +1,40 @@
--TEST--
Variadic Functions - Variable number of arguments with ...
--SKIPIF--
--FILE--
<?php
// Test variadic in class methods
class Calculator {
public function add(...$values): int {
return array_sum($values);
}
public function average(...$values): float {
if (count($values) === 0) {
return 0.0;
}
return array_sum($values) / count($values);
}
public static function max(...$values): mixed {
if (empty($values)) {
return null;
}
return max($values);
}
}
function main() {
// Test variadic in class methods
$calc = new Calculator();
var_dump($calc->add(1, 2, 3, 4));
var_dump($calc->average(13, 14, 17, 26));
var_dump(Calculator::max(5, 15, 10, 20));
}
?>
--EXPECT--
int(10)
float(17.5)
int(20)

@ -0,0 +1,56 @@
--TEST--
Variadic Functions - Variable number of arguments with ...
--SKIPIF--
--FILE--
<?php
// Test combining variadic with type hints
function concat_strings(string ...$strings): string {
return implode('', $strings);
}
// Test variadic returning array
function filter_positive(int ...$numbers): array {
return array_filter($numbers, fn($n) => $n > 0);
}
// Test variadic with reference (should fail gracefully)
function test_by_reference(...$params) {
foreach ($params as &$param) {
$param *= 2;
}
return $params;
}
function main() {
// Test combining variadic with type hints
var_dump(concat_strings("Hello", " ", "World", "!"));
var_dump(concat_strings("PHP", "AOT"));
// Test variadic returning array
var_dump(filter_positive(1, -2, 3, -4, 5));
// Test variadic with reference
$values = [1, 2, 3];
var_dump(test_by_reference(...$values));
}
?>
--EXPECT--
string(12) "Hello World!"
string(6) "PHPAOT"
array(3) {
[0]=>
int(1)
[2]=>
int(3)
[4]=>
int(5)
}
array(3) {
[0]=>
int(2)
[1]=>
int(4)
[2]=>
int(6)
}

@ -0,0 +1,26 @@
--TEST--
sum
--FILE--
<?php
// Test variadic with required parameters
function multiply($multiplier, ...$numbers): int {
$result = 1;
foreach ($numbers as $num) {
$result *= $num;
}
return $result * $multiplier;
}
function main()
{
// Test variadic with required parameters
var_dump(multiply(2, 3, 4)); // 2 * 3 * 4 = 24
var_dump(multiply(10, 5)); // 10 * 5 = 50
var_dump(multiply(7)); // 7 (no additional numbers)
}
?>
--EXPECT--
int(24)
int(50)
int(7)

@ -0,0 +1,39 @@
--TEST--
sum
--FILE--
<?php
// Test basic variadic function
function sum(...$numbers): int {
return array_sum($numbers);
}
function test_unpacking() {
$numbers = [1, 2, 3, 4];
var_dump(sum(...$numbers));
$args = [10, 20, 30, 40, 50];
var_dump(sum(...$args));
// Unpack multiple arrays
$arr1 = [1, 2];
$arr2 = [3, 4];
var_dump(sum(...$arr1, ...$arr2));
}
function main()
{
var_dump(sum(1, 2, 3, 4, 5));
var_dump(sum(10, 20, 30));
var_dump(sum());
// Test unpacking arrays into variadic functions
test_unpacking();
}
?>
--EXPECT--
int(15)
int(60)
int(0)
int(10)
int(150)
int(10)
Loading…
Cancel
Save