feat(compiler): 支持命名参数和提升属性功能

- 添加了对函数和构造函数中命名参数的支持
- 实现了提升属性(Property Promotion)功能
- 新增了对 C++ 保留关键字作为参数名的支持
- 完善了闭包类型检查和参数验证逻辑
- 增加了内部函数调用的命名参数验证
- 优化了参数类型检查和错误提示信息
- 添加了相关测试用例验证功能正确性
pull/5/head
韩天峰 2 months ago
parent 91f7085662
commit 0bfa2869c5
  1. 10
      phpunit/src/FunctionTest.php
  2. 5
      src/Php/Analysis/SsaBuilder.php
  3. 1
      src/Php/ArgInfo.php
  4. 80
      src/Php/CompilerBase.php
  5. 8
      src/Php/Generator/ClosureGenerator.php
  6. 3
      src/Php/Generator/PropertyPromotion.php
  7. 4
      src/Php/Generator/TypeCheckGenerator.php
  8. 6
      src/Php/Optimizer/FuncCallOptimizer.php
  9. 12
      src/Php/Preprocessor.php
  10. 82
      tests/aot/closure/closure-composite-type-check.phpt
  11. 41
      tests/aot/closure/closure-param-defaults.phpt
  12. 16
      tests/aot/functions/internal-named-args.phpt
  13. 37
      tests/aot/named_args/007.phpt
  14. 21
      tests/aot/optimizations/reserved-param-ssa.phpt

@ -27,4 +27,14 @@ class FunctionTest extends \BaseTest
$this->exec('Named argument `value` overwrites previous argument', 'native-call-named-overwrites-positional.php'); $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');
}
} }

@ -206,8 +206,9 @@ class SsaBuilder
private array $argInfoList = [] private array $argInfoList = []
) { ) {
foreach ($this->argInfoList as $argInfo) { foreach ($this->argInfoList as $argInfo) {
$this->params[] = $argInfo->name; $paramName = $argInfo->phpName ?: $argInfo->name;
$this->paramByRef[$argInfo->name] = $argInfo->byRef ?? false; $this->params[] = $paramName;
$this->paramByRef[$paramName] = $argInfo->byRef ?? false;
} }
} }

@ -15,6 +15,7 @@ use PhpParser\NodeAbstract;
class ArgInfo class ArgInfo
{ {
public string $name; public string $name;
public string $phpName = '';
public string $type; public string $type;
public string $default = ''; public string $default = '';
public ?ArrayInitPlan $arrayInitPlan = null; public ?ArrayInitPlan $arrayInitPlan = null;

@ -1873,7 +1873,7 @@ class CompilerBase extends \PhpAot\Core\Translator
{ {
$argNameIndex = []; $argNameIndex = [];
foreach ($functionDef->argInfoList as $k => $argInfo) { foreach ($functionDef->argInfoList as $k => $argInfo) {
$argNameIndex[$argInfo->name] = $k; $argNameIndex[$argInfo->phpName ?: $this->unescapeVarName($argInfo->name)] = $k;
} }
return $argNameIndex; return $argNameIndex;
} }
@ -2841,6 +2841,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if (!$ref) { if (!$ref) {
return; return;
} }
$this->validateInternalNamedCallArgs($ref, $expr->args);
$minArgs = $ref->getNumberOfRequiredParameters(); $minArgs = $ref->getNumberOfRequiredParameters();
$maxArgs = $ref->getNumberOfParameters(); $maxArgs = $ref->getNumberOfParameters();
$actualArgCount = count($expr->args); $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 protected function parseFuncCall(Expr\FuncCall $expr): string
{ {
if ($this->isVarExpr($expr->name)) { if ($this->isVarExpr($expr->name)) {
@ -2954,7 +3026,8 @@ class CompilerBase extends \PhpAot\Core\Translator
break; 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); $args[$k] = new Node\Arg($argInfo->defaultValue);
} }
@ -3891,7 +3964,8 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($this->isTypedObject($object)) { if ($this->isTypedObject($object)) {
$class = $this->getObjectType($object); $class = $this->getObjectType($object);
if ($class and $argInfo->class and !$this->isInheritedFrom($class, $argInfo->class)) { 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");
} }
} }
} }

@ -75,6 +75,7 @@ trait ClosureGenerator
$this->fatalError($expr, 'Closure cannot use reference parameter'); $this->fatalError($expr, 'Closure cannot use reference parameter');
} }
$var = $this->parseIdentifier($param->var); $var = $this->parseIdentifier($param->var);
$phpName = is_string($param->var->name) ? $param->var->name : $this->unescapeVarName($var);
if ($param->variadic) { if ($param->variadic) {
$code .= $this->getIndent() . self::TYPE_ARRAY . ' ' . $var . ';' . PHP_EOL; $code .= $this->getIndent() . self::TYPE_ARRAY . ' ' . $var . ';' . PHP_EOL;
$code .= $this->getIndent() . 'for (uint32_t i = ' . $i . '; i < php::getCallArgNum(); i++) {' . PHP_EOL; $code .= $this->getIndent() . 'for (uint32_t i = ' . $i . '; i < php::getCallArgNum(); i++) {' . PHP_EOL;
@ -83,7 +84,7 @@ trait ClosureGenerator
$this->indentLevel--; $this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL; $code .= $this->getIndent() . '}' . PHP_EOL;
$this->addArgument($var, self::TYPE_ARRAY); $this->addArgument($var, self::TYPE_ARRAY);
$code .= $this->genClosureParamTypeCheck($param, $var, $i, true); $code .= $this->genClosureParamTypeCheck($param, $var, $phpName, $i, true);
continue; continue;
} }
$argExpr = $param->default === null $argExpr = $param->default === null
@ -91,7 +92,7 @@ trait ClosureGenerator
: 'php::getCallArg(' . $i . ', ' . $this->parseParamDefaultValue($param->default) . ')'; : 'php::getCallArg(' . $i . ', ' . $this->parseParamDefaultValue($param->default) . ')';
$code .= $this->getIndent() . 'auto ' . $var . ' = ' . $argExpr . ';' . PHP_EOL; $code .= $this->getIndent() . 'auto ' . $var . ' = ' . $argExpr . ';' . PHP_EOL;
$this->addArgument($var, self::TYPE_VAR); $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) { foreach ($uses as $i => $useItem) {
@ -164,7 +165,7 @@ trait ClosureGenerator
return $code; 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) { if (!$param->type instanceof NullableType && !$param->type instanceof UnionType && !$param->type instanceof IntersectionType) {
return ''; return '';
@ -177,6 +178,7 @@ trait ClosureGenerator
$argInfo = new ArgInfo(); $argInfo = new ArgInfo();
$argInfo->name = $var; $argInfo->name = $var;
$argInfo->phpName = $phpName;
$argInfo->type = self::TYPE_VAR; $argInfo->type = self::TYPE_VAR;
$argInfo->variadic = $variadic; $argInfo->variadic = $variadic;
$argInfo->typeCheck = $typeInfo['check']; $argInfo->typeCheck = $typeInfo['check'];

@ -15,7 +15,8 @@ trait PropertyPromotion
protected function genPropertyPromotion(ArgInfo $argInfo): string protected function genPropertyPromotion(ArgInfo $argInfo): string
{ {
$code = ''; $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"; $code .= ";\n";
return $code; return $code;
} }

@ -265,7 +265,7 @@ trait TypeCheckGenerator
protected function genUnionParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string protected function genUnionParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string
{ {
$fnName = $this->getTypeCheckCallableName(); $fnName = $this->getTypeCheckCallableName();
$paramName = $this->unescapeVarName($argInfo->name); $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
return 'php::concat({' return 'php::concat({'
. 'php::Str(' . $this->genCharPtr($fnName . '(): Argument #', true) . '), ' . 'php::Str(' . $this->genCharPtr($fnName . '(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), ' . 'php::toString(' . $argNoExpr . '), '
@ -381,7 +381,7 @@ trait TypeCheckGenerator
protected function genClosureParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string 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({' return 'php::concat({'
. 'php::Str(' . $this->genCharPtr('{closure}(): Argument #', true) . '), ' . 'php::Str(' . $this->genCharPtr('{closure}(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), ' . 'php::toString(' . $argNoExpr . '), '

@ -195,6 +195,12 @@ trait FuncCallOptimizer
protected function parseFuncCallWithOptimizer(string $name, Node\Expr\FuncCall $expr): string|false 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; $config = $this->getFuncCallConfig()[$name] ?? null;
if ($config === null) { if ($config === null) {
return false; return false;

@ -249,21 +249,25 @@ class Preprocessor extends CompilerBase
$last = array_key_last($params); $last = array_key_last($params);
foreach ($params as $i => $param) { 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 函数,必须设置函数的参数类型 // .stub 存根定义 C++ Native 函数,必须设置函数的参数类型
if ($this->stubFile and !$param->type) { 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) // 构造方法属性定义语法(Constructor Property Promotion)
if ($param->isPromoted()) { if ($param->isPromoted()) {
if (!$this->classDef or !$this->methodDef or $this->methodDef->name !== '__construct') { if (!$this->classDef or !$this->methodDef or $this->methodDef->name !== '__construct') {
$this->fatalError($param, 'Promoted properties are not supported'); $this->fatalError($param, 'Promoted properties are not supported');
} }
$name = $this->parseIdentifier($param->var);
$nullable = $param->type instanceof NullableType; $nullable = $param->type instanceof NullableType;
// Promoted property defaults belong to the constructor parameter, // Promoted property defaults belong to the constructor parameter,
// not to the property default table. The property itself must stay // not to the property default table. The property itself must stay
// uninitialized until __construct assigns it. // 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 ($param->variadic) {
if ($i !== $last) { if ($i !== $last) {
@ -272,13 +276,13 @@ class Preprocessor extends CompilerBase
$this->fatalError($param, 'Variadic parameters cannot be passed by reference'); $this->fatalError($param, 'Variadic parameters cannot be passed by reference');
} }
} }
$name = $this->parseIdentifier($param->var);
if ($this->method and $name === 'this_') { if ($this->method and $name === 'this_') {
$this->fatalError($param, 'Cannot use `$this` as parameter of class method'); $this->fatalError($param, 'Cannot use `$this` as parameter of class method');
} }
$argInfo = new ArgInfo(); $argInfo = new ArgInfo();
$type = $this->parseParameterType($param, $argInfo, $name); $type = $this->parseParameterType($param, $argInfo, $name);
$argInfo->name = $name; $argInfo->name = $name;
$argInfo->phpName = $phpName;
$argInfo->type = $type; $argInfo->type = $type;
$argInfo->byRef = $param->byRef; $argInfo->byRef = $param->byRef;
$argInfo->variadic = $param->variadic; $argInfo->variadic = $param->variadic;

@ -0,0 +1,82 @@
--TEST--
Closure composite type declarations use runtime type checks
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
interface ClosureIA {}
interface ClosureIB {}
class ClosureBoth implements ClosureIA, ClosureIB {}
class ClosureOnlyA implements ClosureIA {}
function main(): void
{
$nullable = function (?int $value) {
var_dump($value);
};
$nullable(null);
$union = function (int|string $union) {
var_dump($union);
};
$union("ok");
$variadic = function (int|string ...$values) {
var_dump($values);
};
$variadic(1, "two");
$intersection = function (ClosureIA&ClosureIB $value) {
var_dump(get_class($value));
};
$intersection(new ClosureBoth());
$returnUnion = function ($value): int|string {
return $value;
};
var_dump($returnUnion(42));
try {
$nullable("bad");
} catch (\TypeError $e) {
echo $e->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

@ -0,0 +1,41 @@
--TEST--
Closure parameters handle defaults and variadic arguments
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
function main(): void
{
$default = function ($value = 42) {
var_dump($value);
};
$default();
$variadic = function (...$values) {
var_dump($values);
};
$variadic(1, "two", null);
$required = function (?int $value) {
var_dump($value);
};
try {
$required();
} catch (\Throwable $e) {
var_dump(get_class($e));
var_dump($e->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"

@ -0,0 +1,16 @@
--TEST--
Internal function named arguments are validated before optimization
--FILE--
<?php
function main(): void
{
var_dump(strlen(string: "abc"));
var_dump(str_replace(replace: "x", subject: "abc", search: "a"));
var_dump(substr(length: 2, string: "abcdef", offset: 3));
}
?>
--EXPECT--
int(3)
string(3) "xbc"
string(2) "de"

@ -0,0 +1,37 @@
--TEST--
Named arguments and promoted properties with C++ reserved parameter names
--FILE--
<?php
function reserved_params($union, $class = 2): array
{
return [$union, $class];
}
class ReservedPromotion
{
public function __construct(
public int $union,
public string $class = "default"
) {
}
}
function main(): void
{
var_dump(reserved_params(class: 20, union: 10));
$object = new ReservedPromotion(class: "named", union: 42);
var_dump($object->union);
var_dump($object->class);
}
?>
--EXPECT--
array(2) {
[0]=>
int(10)
[1]=>
int(20)
}
int(42)
string(5) "named"

@ -0,0 +1,21 @@
--TEST--
SSA optimizations handle parameters with C++ reserved names
--FILE--
<?php
function sum_reserved(int $union, int $class): int
{
$total = 0;
for ($i = 0; $i < $union; $i++) {
$total += $class;
}
return $total;
}
function main(): void
{
var_dump(sum_reserved(union: 4, class: 3));
}
?>
--EXPECT--
int(12)
Loading…
Cancel
Save