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

- 在ClosureGenerator中实现对nullable、union和intersection类型的返回值检查
- 为闭包参数添加运行时类型验证和错误提示
- 扩展CPP_RESERVED_NAMES数组,添加更多C++保留关键字
- 在FunctionContext中增加闭包返回类型检查相关属性
- 实现闭包参数和返回值的类型检查生成器方法
- 修复变量名转义逻辑,确保与PHPX关键字冲突的变量名被正确处理
pull/5/head
韩天峰 2 months ago
parent 90f9da0368
commit 91f7085662
  1. 45
      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 ($v->expr === null) {
if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) { if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) {
return 'return;'; 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) { } elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
$tmpVar = $this->genTmpVarName(); $tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR); $this->addLocalVar($tmpVar, self::TYPE_VAR);
@ -1679,7 +1686,13 @@ class CompilerBase extends \PhpAot\Core\Translator
$exprCode = $this->convertExprType($expr, $returnType, $type); $exprCode = $this->convertExprType($expr, $returnType, $type);
// Union/nullable return type: always use tmpVar for runtime check // 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(); $tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR); $this->addLocalVar($tmpVar, self::TYPE_VAR);
$code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL; $code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL;
@ -5845,6 +5858,14 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function genReturnCode(): string 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) { if ($this->functionDef->returnType === self::TYPE_VOID) {
return ''; return '';
} }
@ -5903,9 +5924,25 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($this->isCallExpr($expr->expr)) { if ($this->isCallExpr($expr->expr)) {
$nativeCall = $expr->expr->getAttribute('nativeCall'); $nativeCall = $expr->expr->getAttribute('nativeCall');
if ($nativeCall and $this->getFunction($nativeCall)->returnType === self::TYPE_VOID) { 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 . ';'; 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 . ';'; return $beforeCode . PHP_EOL . 'return ' . $code . ';';
}; };
@ -5917,7 +5954,11 @@ class CompilerBase extends \PhpAot\Core\Translator
$cb = function () use ($expr) { $cb = function () use ($expr) {
$fnCode = $this->parseStmts($expr->stmts); $fnCode = $this->parseStmts($expr->stmts);
if (!$this->isReturnStmtInLastLine($expr->stmts)) { if (!$this->isReturnStmtInLastLine($expr->stmts)) {
$fnCode .= 'return ' . self::VALUE_NULL . ';' . PHP_EOL; if ($this->context->closureReturnTypeCheck) {
$fnCode .= $this->genReturnCode() . PHP_EOL;
} else {
$fnCode .= 'return ' . self::VALUE_NULL . ';' . PHP_EOL;
}
} }
return $fnCode; return $fnCode;
}; };

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

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

@ -8,7 +8,12 @@
namespace PhpAot\Php\Generator; namespace PhpAot\Php\Generator;
use PhpAot\Php\ArgInfo;
use PhpAot\Php\Context\FunctionContext; use PhpAot\Php\Context\FunctionContext;
use PhpParser\Node;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
trait ClosureGenerator trait ClosureGenerator
@ -36,6 +41,13 @@ trait ClosureGenerator
$this->context = new FunctionContext(); $this->context = new FunctionContext();
$this->context->inClosure = true; $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++; $this->indentLevel++;
$requiredArgCount = 0; $requiredArgCount = 0;
@ -71,6 +83,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);
continue; continue;
} }
$argExpr = $param->default === null $argExpr = $param->default === null
@ -78,6 +91,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);
} }
foreach ($uses as $i => $useItem) { foreach ($uses as $i => $useItem) {
@ -149,4 +163,26 @@ trait ClosureGenerator
return $code; 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 protected function genUnionParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string
{ {
$fnName = $this->getTypeCheckCallableName(); $fnName = $this->getTypeCheckCallableName();
$paramName = $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 . '), '
. '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(' . $this->genCharPtr($argInfo->typeStr, true) . '), '
. 'php::Str(", "), ' . 'php::Str(", "), '
. $valueExpr . '.typeStr(), ' . $valueExpr . '.typeStr(), '
@ -310,4 +311,118 @@ trait TypeCheckGenerator
return $code; 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 protected function escapeVarName(string $name): string
{ {
if (in_array($name, Constants::CPP_RESERVED_NAMES)) {
return '_php__var__' . $name;
}
if ($name === 'this') { if ($name === 'this') {
return 'this_'; return 'this_';
} }
if (in_array($name, Constants::CPP_RESERVED_NAMES, true)) {
return '_php__var__' . $name;
}
return $name; return $name;
} }

Loading…
Cancel
Save