diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 54dde567..b26bd6b7 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -9,6 +9,7 @@ namespace PhpAot\Php; use PhpParser\Node\Expr; +use PhpParser\NodeAbstract; class ArgInfo { @@ -21,4 +22,16 @@ class ArgInfo public bool $variadic = false; public bool $nullable = false; public bool $property = false; + + /** + * Each element: ['kind' => 'isInt'|'isFloat'|...|'instanceof', 'class' => ''] + * Null means no runtime type check needed. + */ + public ?array $typeCheck = null; + + /** Human-readable type string for error messages, e.g. "int|string", "?int" */ + public string $typeStr = ''; + + /** Original union/nullable AST node. Only set when typeCheck is non-null. */ + public ?NodeAbstract $typeNode = null; } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 7c99de38..16584c1b 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -1094,6 +1094,189 @@ class CompilerBase extends \PhpAot\Core\Translator } } + /** + * Build type-check descriptor array from UnionType or NullableType AST node. + * Returns ['check' => array, 'typeStr' => string] or empty check array if no check needed. + */ + protected function buildTypeCheckFromNode(NodeAbstract $typeNode): array + { + $check = []; + $names = []; + + if ($typeNode instanceof NullableType) { + $subTypes = [$typeNode->type]; + $isNullable = true; + } elseif ($typeNode instanceof UnionType) { + $subTypes = $typeNode->types; + $isNullable = false; + } else { + return ['check' => [], 'typeStr' => '']; + } + + foreach ($subTypes as $subType) { + $name = $this->parseIdentifier($subType); + $nameLower = strtolower($name); + + if ($nameLower === 'void' or $nameLower === 'never') { + $this->fatalError($subType, "Type '{$nameLower}' cannot be part of a union type"); + } + + if ($nameLower === 'mixed') { + // mixed accepts everything — don't add any check + $names[] = $name; + continue; + } + + $entry = match ($nameLower) { + 'int' => ['kind' => 'isInt'], + 'float', 'double' => ['kind' => 'isFloat'], + 'bool' => ['kind' => 'isBool'], + 'string' => ['kind' => 'isString'], + 'array' => ['kind' => 'isArray'], + 'object' => ['kind' => 'isObject'], + 'null' => ['kind' => 'isNull'], + 'true' => ['kind' => 'isTrue'], + 'false' => ['kind' => 'isFalse'], + 'resource' => ['kind' => 'isResource'], + 'callable' => ['kind' => 'callable'], + 'iterable' => ['kind' => 'iterable'], + default => null, + }; + + if ($entry !== null) { + $check[] = $entry; + } else { + // Class/interface type + if ($name === 'self') { + $class = $this->getFullClassName(); + } elseif ($name === 'parent') { + $class = $this->classDef->extends ?? ''; + } elseif ($name === 'static') { + $class = 'static'; + } else { + $class = $this->getNamespacedClassName($name); + } + if ($class) { + $check[] = ['kind' => 'instanceof', 'class' => $class]; + } + } + $names[] = $name; + } + + if ($isNullable) { + // NullableType: prepend null to both check array and typeStr + array_unshift($check, ['kind' => 'isNull']); + $typeStr = '?' . implode('|', $names); + } else { + $typeStr = implode('|', $names); + } + + if (empty($check)) { + return ['check' => [], 'typeStr' => $typeStr]; + } + + return ['check' => $check, 'typeStr' => $typeStr]; + } + + /** + * Generate a C++ boolean expression for a single type descriptor entry. + */ + protected function genSingleTypeCondition(string $varName, array $entry): string + { + $v = $varName; + return match ($entry['kind']) { + 'isInt' => $v . '.isInt()', + 'isFloat' => $v . '.isFloat()', + 'isBool' => $v . '.isBool()', + 'isString' => $v . '.isString()', + 'isArray' => $v . '.isArray()', + 'isObject' => $v . '.isObject()', + 'isNull' => $v . '.isNull()', + 'isTrue' => $v . '.isTrue()', + 'isFalse' => $v . '.isFalse()', + 'isResource' => $v . '.isResource()', + 'callable' => $v . '.isCallable()', + 'iterable' => '(' . $v . '.isArray() || (' . $v . '.isObject() && php::instanceOf(' . $v . ', zend_ce_traversable)))', + 'instanceof' => $entry['class'] === 'static' + ? '(' . $v . '.isObject() && php::instanceOf(' . $v . ', php_get_called_ce(this_)))' + : '(' . $v . '.isObject() && php::instanceOf(' . $v . ', ' . $this->getClassEntryPtr($entry['class']) . '))', + default => '', + }; + } + + /** + * Generate C++ runtime type-check block for a function parameter with union/nullable type. + */ + protected function genUnionParamCheck(ArgInfo $argInfo, int $argIndex): string + { + if (empty($argInfo->typeCheck)) { + return ''; + } + + $varName = $argInfo->name; + $conditions = []; + foreach ($argInfo->typeCheck as $entry) { + $cond = $this->genSingleTypeCondition($varName, $entry); + if ($cond !== '') { + $conditions[] = $cond; + } + } + if (empty($conditions)) { + return ''; + } + + $orExpr = implode(' || ', $conditions); + $fnName = $this->functionDef->getNamespacedName(); + $msgExpr = 'php::concat(php::concat(php::Str("' . $fnName . '(): Argument #' . ($argIndex + 1) + . ' ($' . $varName . ') must be of type ' . $argInfo->typeStr . ', "), ' + . $varName . '.typeStr()), php::Str(" given"))'; + + $code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + + return $code; + } + + /** + * Generate C++ runtime type-check block for a function return value with union/nullable type. + */ + protected function genUnionReturnCheck(string $varName): string + { + $typeCheck = $this->functionDef->returnTypeCheck; + 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); + $fnName = $this->functionDef->getNamespacedName(); + $typeStr = $this->functionDef->returnTypeStr; + + $msgExpr = 'php::concat(php::concat(php::Str("' . $fnName . '(): Return value must be of type ' + . $typeStr . ', "), ' . $varName . '.typeStr()), php::Str(" given"))'; + + $code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + + return $code; + } + /** * @throws \Exception */ @@ -1167,6 +1350,12 @@ class CompilerBase extends \PhpAot\Core\Translator } $code .= $this->genPropertyPromotion($argInfo); } + // Runtime union/nullable parameter type checks + foreach ($this->functionDef->argInfoList as $i => $argInfo) { + if (!empty($argInfo->typeCheck)) { + $code .= $this->genUnionParamCheck($argInfo, $i); + } + } $this->indentLevel--; // 构建 PHP 级别的函数名用于 debug backtrace if ($this->class) { @@ -2119,6 +2308,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->functionDef->returnTypeCheck) { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, self::TYPE_VAR); + $code = $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL; + $code .= $this->genUnionReturnCheck($tmpVar); + $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + return $code; } else { return 'return ' . self::VALUE_NULL . ';'; } @@ -2153,9 +2349,16 @@ class CompilerBase extends \PhpAot\Core\Translator } $exprCode = $this->convertExprType($expr, $returnType, $type); - // return 如果使用了 Indirect 语句,可能会导致变量提前析构,出现悬空指针 - // 将 Indirect 赋值给临时变量后,使用 Ctor::Copy 解除了 Indirect,保证内存安全 - if (!$this->isVarExpr($v->expr) and !$this->isScalar($v->expr)) { + // Union/nullable return type: always use tmpVar for runtime check + if ($this->functionDef->returnTypeCheck) { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, self::TYPE_VAR); + $code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL; + $code .= $this->genUnionReturnCheck($tmpVar); + $this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $tmpVar . ';'; + } elseif (!$this->isVarExpr($v->expr) and !$this->isScalar($v->expr)) { + // return 如果使用了 Indirect 语句,可能会导致变量提前析构,出现悬空指针 + // 将 Indirect 赋值给临时变量后,使用 Ctor::Copy 解除了 Indirect,保证内存安全 $tmpVar = $this->genTmpVarName(); // 必须提前声明变量,否则在末尾声明并 return 可能会被 gcc 优化掉 $this->addLocalVar($tmpVar, $returnType); @@ -6302,6 +6505,14 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->functionDef->returnType === self::TYPE_VOID) { return ''; } + if ($this->functionDef->returnTypeCheck) { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, self::TYPE_VAR); + $code = $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL; + $code .= $this->genUnionReturnCheck($tmpVar); + $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + return $code; + } if ($this->functionDef->returnType === self::TYPE_INT or $this->functionDef->returnType === self::TYPE_FLOAT or $this->functionDef->returnType === self::TYPE_BOOL) { diff --git a/src/Php/Entity/FunctionDef.php b/src/Php/Entity/FunctionDef.php index 1665ccfb..ff98ccbc 100644 --- a/src/Php/Entity/FunctionDef.php +++ b/src/Php/Entity/FunctionDef.php @@ -9,6 +9,7 @@ namespace PhpAot\Php\Entity; use PhpAot\Php\ArgInfo; +use PhpParser\NodeAbstract; class FunctionDef { @@ -30,6 +31,15 @@ class FunctionDef */ public string $returnClass = ''; + /** Same format as ArgInfo::$typeCheck. Null means no runtime return type check. */ + public ?array $returnTypeCheck = null; + + /** Human-readable return type string for error messages. */ + public string $returnTypeStr = ''; + + /** Original union/nullable return type AST node. */ + public ?NodeAbstract $returnTypeNode = null; + public function __construct(string $name, string $returnType, string $namespace) { $this->name = $name; diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 51aa1e27..de1329cf 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -280,6 +280,14 @@ class Preprocessor extends CompilerBase if ($param->type and $param->type instanceof NullableType) { $argInfo->nullable = true; } + if ($param->type instanceof NullableType or $param->type instanceof UnionType) { + $typeInfo = $this->buildTypeCheckFromNode($param->type); + if (!empty($typeInfo['check'])) { + $argInfo->typeCheck = $typeInfo['check']; + $argInfo->typeStr = $typeInfo['typeStr']; + $argInfo->typeNode = $param->type; + } + } if ($param->variadic) { $list[] = self::TYPE_ARRAY . ' ' . $name; } else { @@ -340,6 +348,15 @@ class Preprocessor extends CompilerBase $functionDef->returnClass = $class; $functionDef->stub = $this->stubFile; + if ($v->returnType instanceof NullableType or $v->returnType instanceof UnionType) { + $typeInfo = $this->buildTypeCheckFromNode($v->returnType); + if (!empty($typeInfo['check'])) { + $functionDef->returnTypeCheck = $typeInfo['check']; + $functionDef->returnTypeStr = $typeInfo['typeStr']; + $functionDef->returnTypeNode = $v->returnType; + } + } + $this->parseParams($v->params, $functionDef); // main 函数,返回值必须为 void 类型,参数必须为空或者 argc, argv 两个参数 diff --git a/tests/aot/type_decl/union-param-check.phpt b/tests/aot/type_decl/union-param-check.phpt new file mode 100644 index 00000000..f0668a0b --- /dev/null +++ b/tests/aot/type_decl/union-param-check.phpt @@ -0,0 +1,102 @@ +--TEST-- +Union type: parameter runtime type checking +--FILE-- +getMessage(); + } + + try { + expect_int_or_string([]); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + try { + expect_nullable_int("hello"); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + try { + expect_bool_or_array(42); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + foreach ($errors as $err) { + var_dump($err); + } +} +?> +--EXPECT-- +int(42) +string(5) "hello" +int(42) +string(5) "hello" +NULL +int(42) +float(3.14) +int(42) +NULL +string(4) "test" +NULL +bool(true) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +string(80) "expect_int_or_string(): Argument #1 ($x) must be of type int|string, float given" +string(80) "expect_int_or_string(): Argument #1 ($x) must be of type int|string, array given" +string(74) "expect_nullable_int(): Argument #1 ($x) must be of type ?int, string given" +string(78) "expect_bool_or_array(): Argument #1 ($x) must be of type bool|array, int given" diff --git a/tests/aot/type_decl/union-return-check.phpt b/tests/aot/type_decl/union-return-check.phpt new file mode 100644 index 00000000..baacb657 --- /dev/null +++ b/tests/aot/type_decl/union-return-check.phpt @@ -0,0 +1,69 @@ +--TEST-- +Union type: return runtime type checking +--FILE-- +getMessage(); + } + + try { + return_int_or_string([]); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + try { + return_nullable_int("hello"); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + try { + return_int_or_float("hello"); + } catch (\TypeError $e) { + $errors[] = $e->getMessage(); + } + + foreach ($errors as $err) { + var_dump($err); + } +} +?> +--EXPECT-- +int(42) +string(5) "hello" +int(42) +NULL +int(42) +float(3.14) +string(76) "return_int_or_string(): Return value must be of type int|string, float given" +string(76) "return_int_or_string(): Return value must be of type int|string, array given" +string(70) "return_nullable_int(): Return value must be of type ?int, string given" +string(75) "return_int_or_float(): Return value must be of type int|float, string given"