feat(closure): 为闭包添加类型检查功能

- 在ClosureGenerator中实现对nullable、union和intersection类型的返回值检查
- 为闭包参数添加运行时类型验证和错误提示
- 扩展CPP_RESERVED_NAMES数组,添加更多C++保留关键字
- 在FunctionContext中增加闭包返回类型检查相关属性
- 实现闭包参数和返回值的类型检查生成器方法
- 修复变量名转义逻辑,确保与PHPX关键字冲突的变量名被正确处理
pull/5/head
韩天峰 2 months ago
parent 90f9da0368
commit 91f7085662
  1. 43
      src/Php/CompilerBase.php
  2. 84
      src/Php/Constants.php
  3. 4
      src/Php/Context/FunctionContext.php
  4. 36
      src/Php/Generator/ClosureGenerator.php
  5. 117
      src/Php/Generator/TypeCheckGenerator.php
  6. 6
      src/Php/Generator/Utils.php

@ -1632,6 +1632,13 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($v->expr === null) {
if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) {
return 'return;';
} elseif ($this->context->inClosure && $this->context->closureReturnTypeCheck) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
$code = $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL;
$code .= $this->genClosureReturnCheck($tmpVar);
$code .= $this->getIndent() . 'return ' . $tmpVar . ';';
return $code;
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
@ -1679,7 +1686,13 @@ class CompilerBase extends \PhpAot\Core\Translator
$exprCode = $this->convertExprType($expr, $returnType, $type);
// Union/nullable return type: always use tmpVar for runtime check
if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
if ($this->context->inClosure && $this->context->closureReturnTypeCheck) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
$code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL;
$code .= $this->genClosureReturnCheck($tmpVar);
$this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $tmpVar . ';';
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
$code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL;
@ -5845,6 +5858,14 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function genReturnCode(): string
{
if ($this->context->inClosure && $this->context->closureReturnTypeCheck) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
$code = $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL;
$code .= $this->genClosureReturnCheck($tmpVar);
$code .= $this->getIndent() . 'return ' . $tmpVar . ';';
return $code;
}
if ($this->functionDef->returnType === self::TYPE_VOID) {
return '';
}
@ -5903,9 +5924,25 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($this->isCallExpr($expr->expr)) {
$nativeCall = $expr->expr->getAttribute('nativeCall');
if ($nativeCall and $this->getFunction($nativeCall)->returnType === self::TYPE_VOID) {
if ($this->context->closureReturnTypeCheck) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
return $beforeCode . PHP_EOL . $code . ';' . PHP_EOL
. $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL
. $this->genClosureReturnCheck($tmpVar)
. $this->getIndent() . 'return ' . $tmpVar . ';';
}
return $beforeCode . PHP_EOL . $code . ';' . PHP_EOL . 'return ' . self::VALUE_NULL . ';';
}
}
if ($this->context->closureReturnTypeCheck) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
return $beforeCode . PHP_EOL
. $tmpVar . ' = ' . $code . ';' . PHP_EOL
. $this->genClosureReturnCheck($tmpVar)
. $this->getIndent() . 'return ' . $tmpVar . ';';
}
return $beforeCode . PHP_EOL . 'return ' . $code . ';';
};
@ -5917,8 +5954,12 @@ class CompilerBase extends \PhpAot\Core\Translator
$cb = function () use ($expr) {
$fnCode = $this->parseStmts($expr->stmts);
if (!$this->isReturnStmtInLastLine($expr->stmts)) {
if ($this->context->closureReturnTypeCheck) {
$fnCode .= $this->genReturnCode() . PHP_EOL;
} else {
$fnCode .= 'return ' . self::VALUE_NULL . ';' . PHP_EOL;
}
}
return $fnCode;
};
return $this->genClosure($expr, $expr->params, $cb, $expr->uses);

@ -11,53 +11,103 @@ namespace PhpAot\Php;
class Constants
{
public const array CPP_RESERVED_NAMES = [
'alignas',
'alignof',
'and',
'and_eq',
'asm',
'auto',
'bitand',
'bitor',
'bool',
'break',
'case',
'catch',
'char',
'char8_t',
'char16_t',
'char32_t',
'class',
'struct',
'compl',
'const',
'consteval',
'constexpr',
'constinit',
'const_cast',
'continue',
'decltype',
'default',
'delete',
'do',
'double',
'dynamic_cast',
'else',
'elseif',
'enum',
'explicit',
'export',
'extends',
'extern',
'false',
'final',
'finally',
'float',
'for',
'friend',
'function',
'global',
'goto',
'if',
'bool',
'inline',
'int',
'short',
'long',
'unsigned',
'void',
'signed',
'double',
'float',
'false',
'for',
'if',
'int',
'mutable',
'namespace',
'new',
'noexcept',
'not',
'not_eq',
'nullptr',
'null',
'var',
'char',
'or',
'and',
'or_eq',
'operator',
'private',
'protected',
'public',
'register',
'reinterpret_cast',
'requires',
'return',
'short',
'signed',
'sizeof',
'static',
'pipe',
'static_assert',
'static_cast',
'struct',
'switch',
'template',
'namespace',
'this',
'thread_local',
'throw',
'true',
'try',
'typedef',
'typeid',
'typename',
'union',
'unsigned',
'using',
'var',
'virtual',
'void',
'volatile',
'wchar_t',
'while',
'xor',
'xor_eq',
'pipe',
'errno', // Linux error code
'this_', // phpx keywords
];

@ -50,6 +50,8 @@ class FunctionContext
public array $arguments = [];
public bool $inLoop = false;
public bool $inClosure = false;
public ?array $closureReturnTypeCheck = null;
public string $closureReturnTypeStr = '';
/** True if any break N (N > 1) appears in this function. */
public bool $hasMultiLevelBreak = false;
@ -92,6 +94,8 @@ class FunctionContext
$this->scopeLevel = 0;
$this->inLoop = false;
$this->inClosure = false;
$this->closureReturnTypeCheck = null;
$this->closureReturnTypeStr = '';
$this->inAssignExpr = false;
}

@ -8,7 +8,12 @@
namespace PhpAot\Php\Generator;
use PhpAot\Php\ArgInfo;
use PhpAot\Php\Context\FunctionContext;
use PhpParser\Node;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
use PhpParser\NodeAbstract;
trait ClosureGenerator
@ -36,6 +41,13 @@ trait ClosureGenerator
$this->context = new FunctionContext();
$this->context->inClosure = true;
if ($expr->returnType instanceof NullableType || $expr->returnType instanceof UnionType || $expr->returnType instanceof IntersectionType) {
$returnTypeInfo = $this->buildTypeCheckFromNode($expr->returnType);
if (!empty($returnTypeInfo['check'])) {
$this->context->closureReturnTypeCheck = $returnTypeInfo['check'];
$this->context->closureReturnTypeStr = $returnTypeInfo['typeStr'];
}
}
$this->indentLevel++;
$requiredArgCount = 0;
@ -71,6 +83,7 @@ trait ClosureGenerator
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
$this->addArgument($var, self::TYPE_ARRAY);
$code .= $this->genClosureParamTypeCheck($param, $var, $i, true);
continue;
}
$argExpr = $param->default === null
@ -78,6 +91,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);
}
foreach ($uses as $i => $useItem) {
@ -149,4 +163,26 @@ trait ClosureGenerator
return $code;
}
private function genClosureParamTypeCheck(Node\Param $param, string $var, int $index, bool $variadic): string
{
if (!$param->type instanceof NullableType && !$param->type instanceof UnionType && !$param->type instanceof IntersectionType) {
return '';
}
$typeInfo = $this->buildTypeCheckFromNode($param->type);
if (empty($typeInfo['check'])) {
return '';
}
$argInfo = new ArgInfo();
$argInfo->name = $var;
$argInfo->type = self::TYPE_VAR;
$argInfo->variadic = $variadic;
$argInfo->typeCheck = $typeInfo['check'];
$argInfo->typeStr = $typeInfo['typeStr'];
$argInfo->typeNode = $param->type;
return $this->genClosureParamCheck($argInfo, $index);
}
}

@ -265,10 +265,11 @@ trait TypeCheckGenerator
protected function genUnionParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string
{
$fnName = $this->getTypeCheckCallableName();
$paramName = $this->unescapeVarName($argInfo->name);
return 'php::concat({'
. 'php::Str(' . $this->genCharPtr($fnName . '(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), '
. 'php::Str(' . $this->genCharPtr(' ($' . $argInfo->name . ') must be of type ', true) . '), '
. 'php::Str(' . $this->genCharPtr(' ($' . $paramName . ') must be of type ', true) . '), '
. 'php::Str(' . $this->genCharPtr($argInfo->typeStr, true) . '), '
. 'php::Str(", "), '
. $valueExpr . '.typeStr(), '
@ -310,4 +311,118 @@ trait TypeCheckGenerator
return $code;
}
protected function genClosureParamCheck(ArgInfo $argInfo, int $argIndex): string
{
if (empty($argInfo->typeCheck)) {
return '';
}
if ($argInfo->variadic) {
return $this->genClosureVariadicParamCheck($argInfo, $argIndex);
}
$conditions = [];
foreach ($argInfo->typeCheck as $entry) {
$cond = $this->genSingleTypeCondition($argInfo->name, $entry);
if ($cond !== '') {
$conditions[] = $cond;
}
}
if (empty($conditions)) {
return '';
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genClosureParamTypeErrorExpr($argInfo, $argInfo->name, (string) ($argIndex + 1));
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
protected function genClosureVariadicParamCheck(ArgInfo $argInfo, int $argIndex): string
{
$valueVar = $this->genTmpVarName();
$iterVar = $this->genTmpVarName();
$argNoVar = $this->genTmpVarName();
$conditions = [];
foreach ($argInfo->typeCheck as $entry) {
$cond = $this->genSingleTypeCondition($valueVar, $entry);
if ($cond !== '') {
$conditions[] = $cond;
}
}
if (empty($conditions)) {
return '';
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genClosureParamTypeErrorExpr($argInfo, $valueVar, $argNoVar);
$code = $this->getIndent() . 'for (auto ' . $iterVar . ' = ' . $argInfo->name . '.begin(); ' . $iterVar . ' != ' . $argInfo->name . '.end(); ++' . $iterVar . ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $valueVar . ' = ' . $iterVar . '.value();' . PHP_EOL;
$code .= $this->getIndent() . self::TYPE_INT . ' ' . $argNoVar . ' = ' . ($argIndex + 1) . ' + ' . $iterVar . '.index();' . PHP_EOL;
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
protected function genClosureParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string
{
$paramName = $this->unescapeVarName($argInfo->name);
return 'php::concat({'
. 'php::Str(' . $this->genCharPtr('{closure}(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), '
. 'php::Str(' . $this->genCharPtr(' ($' . $paramName . ') must be of type ', true) . '), '
. 'php::Str(' . $this->genCharPtr($argInfo->typeStr, true) . '), '
. 'php::Str(", "), '
. $valueExpr . '.typeStr(), '
. 'php::Str(" given")'
. '})';
}
protected function genClosureReturnCheck(string $varName): string
{
$typeCheck = $this->context->closureReturnTypeCheck;
if (empty($typeCheck)) {
return '';
}
$conditions = [];
foreach ($typeCheck as $entry) {
$cond = $this->genSingleTypeCondition($varName, $entry);
if ($cond !== '') {
$conditions[] = $cond;
}
}
if (empty($conditions)) {
return '';
}
$orExpr = implode(' || ', $conditions);
$typeStr = $this->context->closureReturnTypeStr;
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr('{closure}', true) . ' "(): Return value must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $varName . '.typeStr()), php::Str(" given"))';
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
}

@ -62,12 +62,12 @@ trait Utils
protected function escapeVarName(string $name): string
{
if (in_array($name, Constants::CPP_RESERVED_NAMES)) {
return '_php__var__' . $name;
}
if ($name === 'this') {
return 'this_';
}
if (in_array($name, Constants::CPP_RESERVED_NAMES, true)) {
return '_php__var__' . $name;
}
return $name;
}

Loading…
Cancel
Save