diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 73608a11..733299df 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.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,7 +5954,11 @@ class CompilerBase extends \PhpAot\Core\Translator $cb = function () use ($expr) { $fnCode = $this->parseStmts($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; }; diff --git a/src/Php/Constants.php b/src/Php/Constants.php index 5609f319..770a214e 100644 --- a/src/Php/Constants.php +++ b/src/Php/Constants.php @@ -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 ]; diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index e344d411..6f3c145b 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -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; } diff --git a/src/Php/Generator/ClosureGenerator.php b/src/Php/Generator/ClosureGenerator.php index c2654141..9ee09f9a 100644 --- a/src/Php/Generator/ClosureGenerator.php +++ b/src/Php/Generator/ClosureGenerator.php @@ -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); + } } diff --git a/src/Php/Generator/TypeCheckGenerator.php b/src/Php/Generator/TypeCheckGenerator.php index 242b44c0..990ad37f 100644 --- a/src/Php/Generator/TypeCheckGenerator.php +++ b/src/Php/Generator/TypeCheckGenerator.php @@ -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; + } + } diff --git a/src/Php/Generator/Utils.php b/src/Php/Generator/Utils.php index 284bcf8f..85fd3fa7 100644 --- a/src/Php/Generator/Utils.php +++ b/src/Php/Generator/Utils.php @@ -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; }