$files */ public function discoverNativeClassDeclarations(array $files): void { foreach ($files as $file) { if (!$this->isPhpFileForNativeDiscovery($file)) { continue; } $source = file_get_contents($file); if (!is_string($source) || !str_contains($source, '#[') || stripos($source, 'native') === false ) { continue; } try { $ast = $this->parser->parse($source); } catch (\PhpParser\Error) { // prepareFile() owns the normal source diagnostic, including // the filename and compiler formatting. Avoid reporting a // syntax error twice from this declaration-only pass. continue; } $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $ast = $traverser->traverse($ast); $this->discoverNativeClassDeclarationsInAst($ast); } } private function isPhpFileForNativeDiscovery(string $file): bool { return str_ends_with(strtolower($file), '.php'); } /** @param array $ast */ private function discoverNativeClassDeclarationsInAst(array $ast): void { $finder = new NodeFinder(); foreach ($finder->findInstanceOf($ast, Node\Stmt\Class_::class) as $class) { if ($class->name === null || (!NativeClassAttributeLowering::isNative($class) && CompileTimeAttribute::find($class, 'Native') === null) ) { continue; } $name = isset($class->namespacedName) ? $class->namespacedName->toString() : $class->name->toString(); $name = ltrim($name, '\\'); $this->nativeClassDeclarations[strtolower($name)] = $name; } } /** * Discover Native Object globals after all declarations have been prepared * but before the first translation unit is emitted. * * @param list $files */ public function discoverNativeGlobalObjects(array $files): void { $hasNativeClass = false; foreach ($this->symbols->classes() as $class) { if ($class->nativeObject) { $hasNativeClass = true; break; } } if (!$hasNativeClass) { return; } $candidateSources = []; foreach ($files as $file) { if (!$this->isPhpFileForNativeDiscovery($file)) { continue; } $source = file_get_contents($file); if (is_string($source) && (str_contains($source, 'global') || str_contains($source, '$GLOBALS')) ) { $candidateSources[$file] = $source; } } if ($candidateSources === []) { return; } $functionReturns = []; foreach ($this->symbols->functions() as $function) { if (!$function->method && $this->hasClass($function->returnClass)) { $functionReturns[strtolower(ltrim($function->getNamespacedName(), '\\'))] = $this->getClass($function->returnClass)->getNamespacedName(false); } } $resolver = new NativeGlobalTypeResolver($this->symbols->classes(), $this->constants); $this->nativeGlobalTypeResolver = $resolver; $discovery = new NativeGlobalDiscovery($resolver, $functionReturns); foreach ($candidateSources as $source) { try { $ast = $this->parser->parse($source); } catch (\PhpParser\Error) { // prepareFile() has already emitted the authoritative syntax // diagnostic. This pass must not report it a second time. continue; } $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $ast = $traverser->traverse($ast); foreach ($discovery->discover($ast) as $slot) { $this->registerNativeGlobalObject($slot['name'], $slot['class'], $slot['node']); } } } public function getSortedFiles(array $list): array { $sorter = new StringSort(); $fileDeps = []; // 构建依赖关系图 foreach ($this->symbolCallInFile as $file => $symbols) { $deps = []; foreach ($symbols as $symbol) { if (isset($this->symbolDeclInFile[$symbol])) { $depFile = $this->symbolDeclInFile[$symbol]; if ($depFile !== $file) { $deps[] = $depFile; } } } $deps = array_unique($deps); $fileDeps[$file] = $deps; $sorter->add($file, $deps); } $sortedFiles = $sorter->sort(); // 添加未参与依赖管理的文件(非 stub 文件且不在已排序列表中) foreach ($list as $file) { if (!$this->isStubFile($file) and !in_array($file, $sortedFiles)) { $sortedFiles[] = $file; } } $this->climate->lightBlue('prepare completed: ' . count($sortedFiles) . ' source files in total'); return $sortedFiles; } protected function genArgumentDeclaration(ArgInfo $argInfo): string { $nativeObjectType = $this->getNativeObjectArgumentType($argInfo); if ($nativeObjectType !== null) { return $nativeObjectType . $argInfo->name; } $type = $argInfo->type; if ($type === Type::STREAM || $type === Type::BOX) { $type = Type::VAR; } return $type . ' ' . $argInfo->name; } public function getCppFile(string $file): string { $info = pathinfo($file); $separator = $this->getPlatform()->getPathSeparator(); $relativePath = $this->removeCommonPrefix($this->buildDir, $info['dirname']); return $this->buildDir . $separator . $relativePath . $separator . $info['filename'] . '.cc'; } public function getObjectFile(string $cppFile): string { $info = pathinfo($cppFile); $ext = $this->getPlatform()->getObjectExtension(); // 保持与 cppFile 相同的路径分隔符 $normalizedFile = str_replace('\\', '/', $cppFile); $normalizedMiscDir = str_replace('\\', '/', $this->getPhpxDir() . '/src/misc/'); if (str_starts_with($normalizedFile, $normalizedMiscDir)) { $separator = $this->getPlatform()->getPathSeparator(); $objectDir = $this->buildDir . $separator . 'phpx-misc' . $separator . $this->targetName; if (!is_dir($objectDir)) { mkdir($objectDir, 0777, true); } return $objectDir . $separator . $info['filename'] . $ext; } return $info['dirname'] . $this->getPlatform()->getPathSeparator() . $info['filename'] . $ext; } public function prepareFile(string $file): void { $previousPhase = $this->enterCompilerPhase(self::PHASE_PREPARE); try { $phpCode = $this->loadFile($file); $this->symbolCallInFile[$this->file] = []; $this->resetFile(); $this->resetFunction(); $this->resetMethod(); $this->resetClass(); $this->resetNamespace(); $this->climate->info('prepare: ' . $this->getRelativePath($this->file)); try { $ast = $this->parser->parse($phpCode); } catch (\PhpParser\Error $e) { $this->climate->red("Fatal error: {$e->getMessage()} in {$this->file}"); throw new SyntaxError($e->getMessage(), $e->getCode()); } $this->stubImportLibrary = $this->stubFile && $this->hasLibraryImportAnnotation($ast) ? $this->getExternalImportLibraryName($this->file) : ''; if ($this->stubImportLibrary !== '') { $this->externalImportStubFiles[$this->file] = true; } if ($this->stubImportLibrary !== '' && !in_array($this->stubImportLibrary, $this->linkLibs, true)) { $this->linkLibs[] = $this->stubImportLibrary; } $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $traverser->addVisitor(new VoidCastValidationVisitor( fn (Node $node, string $message) => $this->fatalError($node, $message), )); $traverser->addVisitor(new Visitor( fn (Node $node, string $message) => $this->warning($node, $message), $this->file, )); $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); // CompilerTest and embedding users may invoke prepareFile() // directly instead of the project pipeline. Preserve same-file // forward Native references for that public entry path as well. $this->discoverNativeClassDeclarationsInAst($stmts); foreach ($stmts as $v) { $type = $v->getType(); switch ($type) { case 'Stmt_Namespace': $this->prepareNamespace($v); break; case 'Stmt_Enum': case 'Stmt_Class': case 'Stmt_Trait': $this->prepareClass($v); break; case 'Stmt_Interface': $this->parseInterface($v); break; case 'Stmt_Function': $this->prepareFunction($v); break; case 'Stmt_Use': $this->parseUse($v); break; case 'Stmt_GroupUse': $this->parseGroupUse($v); break; case 'Stmt_Declare': case 'Stmt_Nop': break; case 'Stmt_Const': $this->parseConstDef($v); break; case 'Stmt_Expression': $this->foundStrayCode($v); break; default: $this->fatalError($v, 'Unsupported statement: ' . $type); break; } } } finally { $this->restoreCompilerPhase($previousPhase); } } /** @param array $stmts */ private function hasLibraryImportAnnotation(array $stmts): bool { foreach ($stmts as $stmt) { foreach ($stmt->getComments() as $comment) { if (preg_match('/@import-library\b/', $comment->getText()) === 1) { return true; } } } return false; } private function hasNoExportAttribute(NodeAbstract $node): bool { foreach ($node->attrGroups as $group) { foreach ($group->attrs as $attribute) { if (!$this->isRootCompileTimeAttribute($attribute, 'NoExport')) { continue; } if ($attribute->args !== []) { $this->fatalCompileTimeAttribute( $node, 'NoExport', 'NoExport does not accept arguments', $attribute, ); } return true; } } return false; } private function parseWasmExportAttribute(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): ?string { foreach ($node->attrGroups as $group) { foreach ($group->attrs as $attribute) { if (!$this->isRootCompileTimeAttribute($attribute, 'WasmExport')) { continue; } if ($node instanceof Node\Stmt\ClassMethod) { $this->fatalCompileTimeAttribute( $node, 'WasmExport', 'WasmExport can only be applied to named functions', $attribute, ); } if (count($attribute->args) > 1) { $this->fatalCompileTimeAttribute( $node, 'WasmExport', 'WasmExport accepts at most one string name', $attribute, ); } if ($attribute->args === []) { return ''; } $argument = $attribute->args[0]; if ($argument->name !== null && strcasecmp($argument->name->toString(), 'name') !== 0) { $this->fatalCompileTimeAttribute( $node, 'WasmExport', 'WasmExport only accepts the named argument `name`', $attribute, ); } if (!$argument->value instanceof Node\Scalar\String_) { $this->fatalCompileTimeAttribute( $node, 'WasmExport', 'WasmExport name must be a constant string', $attribute, ); } $name = $argument->value->value; if ($name === '') { $this->fatalCompileTimeAttribute( $node, 'WasmExport', 'WasmExport name must not be empty', $attribute, ); } return $name; } } return null; } private function isRootCompileTimeAttribute(Node\Attribute $attribute, string $name): bool { return strcasecmp($this->getResolvedPhpName($attribute->name), $name) === 0; } private function getResolvedPhpName(Node\Name $name): string { $resolvedName = $name->getAttribute('resolvedName') ?? $name->getAttribute('namespacedName') ?? $name; return ltrim($resolvedName->toString(), '\\'); } private function getExternalImportLibraryName(string $stubFile): string { $name = basename($stubFile, '.stub.php'); $name = str_replace(['-', '*'], '_', $name); if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) { throw new SyntaxError('Invalid external import stub filename `' . basename($stubFile) . '`'); } return $name; } /** * Collect per-file symbol dependencies for the incremental compilation cache. * * The cache does not consume this graph yet, but this collector is retained * intentionally so cache invalidation can later be based on symbol usage. */ protected function findSymbolUsing(NodeAbstract $ast): void { $nodeFinder = new NodeFinder(); $functionCalls = $nodeFinder->findInstanceOf($ast, Node\Expr\FuncCall::class); foreach ($functionCalls as $call) { if ($call->name instanceof Node\Name) { // 内置函数不参与依赖管理 $funcName = strtolower($call->name->toString()); if (!$this->isInternalFunction($funcName)) { $this->symbolCallInFile[$this->file][] = $funcName; } } } $depClasses = []; $depClasses = array_merge($depClasses, $nodeFinder->findInstanceOf($ast, Node\Expr\StaticCall::class)); $depClasses = array_merge($depClasses, $nodeFinder->findInstanceOf($ast, Node\Expr\StaticPropertyFetch::class)); $depClasses = array_merge($depClasses, $nodeFinder->findInstanceOf($ast, Node\Expr\ClassConstFetch::class)); $depClasses = array_merge($depClasses, $nodeFinder->findInstanceOf($ast, Node\Expr\New_::class)); foreach ($depClasses as $call) { if ($call->class instanceof Node\Name) { $className = $this->parseIdentifier($call->class); if ($className !== 'self' && $className !== 'static') { $fullClassName = $this->getNamespacedClassName($className); $this->symbolCallInFile[$this->file][] = strtolower($fullClassName); } } } // 依赖去重 $this->symbolCallInFile[$this->file] = array_unique($this->symbolCallInFile[$this->file]); } protected function prepareNamespace(Node\Stmt\Namespace_ $node): void { $this->resetClass(); $this->resetMethod(); $this->resetFunction(); $this->resetNamespace(); $this->namespace = $node->name ? $this->parseIdentifier($node->name) : ''; foreach ($node->stmts as $v2) { $type2 = $v2->getType(); switch ($type2) { case 'Stmt_Class': case 'Stmt_Enum': case 'Stmt_Trait': $this->prepareClass($v2); break; case 'Stmt_Function': $this->prepareFunction($v2); break; case 'Stmt_Use': $this->parseUse($v2); break; case 'Stmt_GroupUse': $this->parseGroupUse($v2); break; case 'Stmt_Const': $this->parseConstDef($v2); break; case 'Stmt_Interface': $this->parseInterface($v2); break; case 'Stmt_Nop': break; default: $this->foundStrayCode($v2); break; } } } protected function parseParameterType(Node\Param $param, ArgInfo $argInfo, string $var): string { // Capture the late-bound parameter type keyword *before* resolveTypeDecl // runs, because resolveTypeDecl mutates the `self`/`static`/`parent` node // name to the declaring class when the method belongs to a trait. $typeKeyword = ''; if ($param->type instanceof Node\Name) { $ptLower = strtolower($param->type->toString()); if ($ptLower === 'self' || $ptLower === 'static' || $ptLower === 'parent') { $typeKeyword = $ptLower; } } [$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); $this->assertSupportedNativeObjectTypeNode($param->type, self::DECL_TYPE_OF_PARAM, $param); $nullableNative = $this->resolveNullableNativeObjectType($param->type, self::DECL_TYPE_OF_PARAM); if ($nullableNative !== null) { [$type, $class] = $nullableNative; $argInfo->nullable = true; } $argInfo->undeclared = $param->type === null; if ( $param->type !== null && !$param->type instanceof NullableType && !$param->type instanceof UnionType && !$param->type instanceof IntersectionType ) { $argInfo->explicitMixed = in_array(strtolower($this->parseIdentifier($param->type)), ['mixed', 'any'], true); } if ($class) { $argInfo->declaredClass = $class; } if ($class and !$this->hasInterface($class)) { $argInfo->class = $class; } // Record late-bound parameter type keywords so they can be re-resolved // to the consuming class when a trait method is flattened into a class. $argInfo->typeKeyword = $typeKeyword; // Ordinary PHP references use php::Ref at the native ABI. Native // object references are rejected after the complete signature has // been parsed: a typed pointer already shares object identity, while // PHP & would additionally expose caller-slot rebinding. return $param->byRef ? Type::REF : $type; } /** * @param $params array */ protected function parseParams(array $params, FunctionDef $functionDef): void { $list = []; $functionDef->argCountRequired = count($params); $lastRequiredIndex = -1; $lastRequiredName = ''; $last = array_key_last($params); foreach ($params as $i => $param) { if (!$param->default && !$param->variadic) { $lastRequiredIndex = $i; if (is_string($param->var->name)) { $lastRequiredName = $param->var->name; } } } 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); // Local stubs define C++ native functions and require explicit ABI types. // Generated external stubs may preserve an untyped PHP declaration as php::Var. if ($this->stubFile && $this->stubImportLibrary === '' && !$param->type) { throw new \RuntimeException('No type for ' . $phpName); } // 构造方法属性定义语法(Constructor Property Promotion) if ($param->isPromoted()) { if (!$this->classDef or !$this->methodDef or $this->methodDef->name !== '__construct') { $this->fatalError($param, 'Promoted properties are not supported'); } $nullable = $param->type instanceof NullableType; // Promoted property defaults belong to the constructor parameter, // not to the property default table. The property itself must stay // uninitialized until __construct assigns it. $promotedProperty = $this->addClassProperty($phpName, $param->flags, $param->type, null, $nullable, $param, true); $promotedProperty->arrayDef = $this->parseArrayDefinition($param); } if ($param->variadic) { if ($i !== $last) { $this->fatalError($param, 'Variadic parameters must be the last parameter'); } elseif ($param->byRef) { $this->fatalError($param, 'Variadic parameters cannot be passed by reference'); } } if ($param->default && $i < $lastRequiredIndex) { $this->fatalError( $param, $this->getFunctionDisplayName($functionDef) . '(): optional parameter `$' . $phpName . '` cannot be declared before required parameter `$' . $lastRequiredName . '`' ); } if ($this->method and $name === 'this_') { $this->fatalError($param, 'Cannot use `$this` as parameter of class method'); } $argInfo = new ArgInfo(); $type = $this->parseParameterType($param, $argInfo, $name); $argInfo->name = $name; $argInfo->phpName = $phpName; $argInfo->type = $type; $argInfo->byRef = $param->byRef; $argInfo->variadic = $param->variadic; $argInfo->property = $param->isPromoted(); $argInfo->immutable = \TypePhp\Transform\CompileTimeAttribute::consume($param, 'Immutable'); if ($param->type === null || $param->type instanceof NullableType) { $argInfo->nullable = true; } if ($param->type instanceof NullableType || $param->type instanceof UnionType || $param->type instanceof IntersectionType) { $typeInfo = $this->buildTypeCheckFromNode($param->type); if (!empty($typeInfo['check']) && !$this->isNativeObjectClass($argInfo->declaredClass)) { $argInfo->typeCheck = $typeInfo['check']; $argInfo->typeStr = $typeInfo['typeStr']; $argInfo->typeNode = $param->type; } } if ($param->variadic) { $list[] = Type::ARRAY . ' ' . $name; } else { $list[] = $this->genArgumentDeclaration($argInfo); } if ($param->default) { $arrayInitPlan = $param->default instanceof Node\Expr\Array_ ? $this->withoutLocalClassEntryHoisting( fn (): ArrayInitPlan => $this->buildLiteralArrayInitPlan($param->default), ) : null; if ($param->byRef) { if ($this->isEmptyArray($param->default)) { $argInfo->default = 'php::getEmptyArrayRef()'; $argInfo->defaultValue = null; } elseif ($this->isNull($param->default)) { $argInfo->default = 'nullptr'; $argInfo->defaultValue = null; } elseif ($arrayInitPlan) { $argInfo->default = 'php::newReference(' . $arrayInitPlan->expr . ')'; $argInfo->arrayInitPlan = $arrayInitPlan; } else { $argInfo->default = 'php::newReference(' . $this->parseParamDefaultValue($param->default) . ')'; } } else { $argInfo->default = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default); $argInfo->arrayInitPlan = $arrayInitPlan; $argInfo->defaultValue = $param->default; } } elseif ($param->variadic) { // 变长参数可以视为空数组默认值 $argInfo->default = '{}'; $argInfo->defaultValue = new Node\Expr\Array_(); } $functionDef->argInfoList[] = $argInfo; } $functionDef->params = implode(', ', $list); $functionDef->argCountRequired = $lastRequiredIndex + 1; } protected function getFunctionDisplayName(FunctionDef $functionDef): string { if ($this->class) { return $this->class . '::' . $functionDef->name; } return $functionDef->getNamespacedName(); } protected function parseFunctionDecl(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): FunctionDef { // Local stubs define C++ native functions and require an explicit ABI return type. // Generated external stubs may preserve an untyped PHP declaration as php::Var. if ($this->stubFile && $this->stubImportLibrary === '' && !$v->returnType) { // 以下魔术方法都不能声明返回值类型 __construct()/__destruct()/__clone() if (($this->method and !in_array($this->method, ['__construct', '__destruct', '__clone'])) or !$this->method) { $name = $this->class ? $this->class . '::' . $v->name : $v->name; $this->fatalError($v, 'The return type of the function `' . $name . '` must be specified'); } } if ($this->method and $v->returnType !== null) { $methodName = $this->class . '::' . $this->method; if (in_array($this->method, ['__construct', '__destruct'], true)) { $this->fatalError($v, 'Method `' . $methodName . '()` cannot declare a return type'); } if ($this->method === '__clone' and (!$v->returnType instanceof Node\Identifier or strtolower($v->returnType->name) !== 'void')) { $this->fatalError($v, 'Method `' . $methodName . '()` return type must be void when declared'); } } $fnName = $this->parseIdentifier($v->name); // Capture the late-bound return type keyword *before* resolveTypeDecl runs, // because resolveTypeDecl mutates the `self`/`static`/`parent` node name to // the declaring class when the method belongs to a trait. $returnTypeKeyword = ''; if ($v->returnType instanceof Node\Name) { $rtLower = strtolower($v->returnType->toString()); if ($rtLower === 'self' || $rtLower === 'static' || $rtLower === 'parent') { $returnTypeKeyword = $rtLower; } } [$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN); $this->assertSupportedNativeObjectTypeNode($v->returnType, self::DECL_TYPE_OF_RETURN, $v); $nullableNativeReturn = $this->resolveNullableNativeObjectType( $v->returnType, self::DECL_TYPE_OF_RETURN, ); if ($nullableNativeReturn !== null) { [$returnType, $class] = $nullableNativeReturn; } // 构造、析构、克隆方法不能有返回值 if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) { $returnType = Type::VOID; } $functionDef = new FunctionDef($fnName, $returnType, $this->namespace); $functionDef->mustUse = (bool) $v->getAttribute(FunctionAttributeLowering::MUST_USE_ATTRIBUTE, false); $functionDef->immutable = (bool) $v->getAttribute(FunctionAttributeLowering::IMMUTABLE_ATTRIBUTE, false); $functionDef->overrideRequired = (bool) $v->getAttribute(FunctionAttributeLowering::OVERRIDE_ATTRIBUTE, false); $functionDef->hot = (bool) $v->getAttribute(FunctionAttributeLowering::HOT_ATTRIBUTE, false); $functionDef->cold = (bool) $v->getAttribute(FunctionAttributeLowering::COLD_ATTRIBUTE, false); $wasmExportName = $this->parseWasmExportAttribute($v); $functionDef->wasmExport = $wasmExportName !== null; $functionDef->wasmExportName = $wasmExportName ?? ''; if ($functionDef->mustUse && $returnType === Type::VOID) { $this->fatalCompileTimeAttribute( $v, 'MustUse', 'MustUse cannot be applied to a function or method returning void', ); } $functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v)); $functionDef->returnClass = $class; $functionDef->returnNullable = $nullableNativeReturn !== null; $functionDef->returnTypeStr = $v->returnType === null ? '' : $this->typeCheckNodeToString($v->returnType); // Record late-bound return type keywords so they can be re-resolved to // the consuming class when a trait method is flattened into a class. $functionDef->returnTypeKeyword = $returnTypeKeyword; $functionDef->stub = $this->stubFile; $functionDef->importLibrary = $this->stubImportLibrary; $functionDef->returnTypeUndeclared = $v->returnType === null; $functionDef->returnsByRef = $v->byRef; if ($this->containsYield($v)) { $this->prepareGeneratorFunction($v, $functionDef); } if (!$functionDef->generator && !$this->isNativeObjectClass($functionDef->returnClass) && ($v->returnType instanceof NullableType || $v->returnType instanceof UnionType || $v->returnType instanceof IntersectionType)) { $typeInfo = $this->buildTypeCheckFromNode($v->returnType); if (!empty($typeInfo['check'])) { $functionDef->returnTypeCheck = $typeInfo['check']; $functionDef->returnTypeNode = $v->returnType; } } if (!$this->method && $this->canOptimizeMultiReturn($v, $functionDef)) { $functionDef->multiReturnCount = count($v->stmts[array_key_last($v->stmts)]->expr->items); // The fixed tuple is an internal ABI detail. PHP and ordinary native // callers continue to observe an array return value. $functionDef->returnType = Type::ARRAY; } $this->parseParams($v->params, $functionDef); $this->assertNativeObjectFunctionSignature($v, $functionDef); if ($functionDef->generator && ($this->classDef?->nativeObject || $this->functionUsesNativeObject($functionDef)) ) { // FiberGenerator lowers the body to a Zend Closure and captures all // parameters through php::Args. Native pointers deliberately have // no zval representation, and a Native method's `this` cannot be // bound to that Closure either. $this->fatalError( $v, 'Generator functions cannot accept, capture, or return Native objects', ); } if ($this->classDef !== null && !$this->classDef->nativeObject && strtolower($fnName) === '__construct' && $this->functionUsesNativeObject($functionDef) ) { $this->fatalError($v, 'Zend-backed constructors cannot accept or return native objects'); } // main 函数,返回值必须为 void 类型,参数必须为空或者 argc, argv 两个参数 if (!$this->class and !$this->namespace and $fnName === self::ENTRY_FUNCTION) { if (count($v->params) > 0) { if (count($v->params) != 2) { $this->fatalError($v, 'The parameters of the main function must be `(int $argc, array $argv)`.'); } if ($returnType !== Type::VOID) { $this->fatalError($v, 'main function must return void'); } if (!$this->checkArgType($functionDef->argInfoList[0]->type, Type::INT)) { $this->fatalError($v, 'The first parameter of the main function must be of type `int`.'); } if (!$this->checkArgType($functionDef->argInfoList[1]->type, Type::ARRAY)) { $this->fatalError($v, 'The second parameter of the main function must be of type `array`.'); } } } return $functionDef; } private function canOptimizeMultiReturn(Node\Stmt\Function_|Node\Stmt\ClassMethod $function, FunctionDef $functionDef): bool { if ($functionDef->stub || $functionDef->generator || $functionDef->returnsByRef || ($functionDef->returnType !== Type::ARRAY && !$functionDef->returnTypeUndeclared) || !$function->stmts) { return false; } $return = $function->stmts[array_key_last($function->stmts)] ?? null; if (!$return instanceof Node\Stmt\Return_ || !$return->expr instanceof Node\Expr\Array_ || count($return->expr->items) < 2) { return false; } $returns = (new NodeFinder())->findInstanceOf($function->stmts, Node\Stmt\Return_::class); if (count($returns) !== 1) { return false; } foreach ($return->expr->items as $item) { if ($item === null || $item->key !== null || $item->unpack || $item->byRef) { return false; } $value = $item->value; if (($value instanceof Node\Expr\Variable && is_string($value->name)) || $value instanceof Node\Scalar || $value instanceof Node\Expr\ConstFetch) { continue; } return false; } return true; } protected function prepareFunction(Node\Stmt\ClassMethod|Node\Stmt\Function_ $v): void { $this->resetFunction(); $this->function = $this->parseIdentifier($v->name); $name = $this->getFunctionName($v); if ($this->hasFunction($name)) { $existing = $this->getFunction($name); $currentIsMethod = $this->methodDef !== null; if ($existing->method !== $currentIsMethod) { $currentDisplayName = $currentIsMethod ? $this->classDef->getNamespacedName(false) . '::' . $this->function : ltrim($this->namespace . '\\' . $this->function, '\\'); $existingDisplayName = $existing->displayName !== '' ? $existing->displayName : $existing->getNamespacedName(); $existingLocation = $existing->sourceFile !== '' ? " (previously declared in {$existing->sourceFile}:{$existing->startLine})" : ''; $this->fatalError( $v, "C++ symbol collision: `{$existingDisplayName}()` and `{$currentDisplayName}()` " . "both map to `" . self::PREFIX . "{$name}`{$existingLocation}; rename one of them", ); } $this->fatalError($v, "Duplicate function `{$name}`"); } // 禁止重定义内置函数 if (!$this->methodDef and $this->isInternalFunction($name)) { $this->fatalError($v, "The function `{$name}` is a built-in function and cannot be redefined"); } $functionDef = $this->parseFunctionDecl($v); $functionDef->attributeFactory = (bool) $v->getAttribute( RuntimeAttributeFactoryLowering::FACTORY_FUNCTION_ATTRIBUTE, false, ); if ($functionDef->attributeFactory) { $functionDef->exported = false; $functionDef->attributeFactoryScope = (string) $v->getAttribute( RuntimeAttributeFactoryLowering::FACTORY_SCOPE_ATTRIBUTE, '', ); } $functionDef->sourceFile = $this->file; $functionDef->startLine = $v->getStartLine(); $functionDef->method = $this->methodDef !== null; $functionDef->declaringClass = $functionDef->method ? $this->classDef->getNamespacedName(false) : ''; $functionDef->displayName = $functionDef->method ? $this->classDef->getNamespacedName(false) . '::' . $functionDef->name : $functionDef->getNamespacedName(); $this->addFunction($name, $functionDef); if ($this->methodDef) { $this->methodDef->functionDef = $functionDef; } } protected function prepareClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): string { $this->resetClass(); $this->class = $this->parseIdentifier($class->name); $fullClassName = $this->getFullClassName(); $fullClassNameLower = strtolower($fullClassName); if ($class instanceof Node\Stmt\Class_) { $flags = $class->flags; } else { $flags = Modifiers::PUBLIC; } if (isset($this->symbolDeclInFile[$fullClassNameLower])) { $this->fatalError($class, "Duplicate class `{$fullClassName}`"); } $this->classDef = new ClassDef($this->class, $flags, $this->namespace); $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->exported = !$this->hasNoExportAttribute($class); if ($this->classDef->nativeObject && $this->stubFile) { $this->fatalCompileTimeAttribute( $class, 'Native', '#[Native] cannot be used in .stub.php; Native class layout must be owned by the TypePHP compiler', ); } if ($this->classDef->nativeObject && $this->classDef->exported && $this->isBuildModeLib() && !$this->isWasiTarget() ) { $this->fatalCompileTimeAttribute( $class, 'Native', "Native class `{$fullClassName}` cannot be exported through a library stub; mark it with #[NoExport]", ); } $this->classDef->methodsForTarget = $this->parseMethodsForTarget($class); $this->addClass($fullClassName, $this->classDef); if (!empty($class->extends)) { $this->parentClass = $this->getNamespacedClassName($this->parseIdentifier($class->extends)); $parentClassLower = strtolower($this->parentClass); if ($parentClassLower === $fullClassNameLower) { $this->fatalError($class, "Class {$fullClassName} cannot extend itself"); } $this->symbols->setParent($fullClassNameLower, $parentClassLower); $this->classSubClasses[$parentClassLower][] = $fullClassNameLower; if (!$this->isInternalClass($parentClassLower)) { $this->symbolCallInFile[$this->file][] = $parentClassLower; } $this->classDef->extends = $this->parentClass; // 是否继承了内置类 $this->classDef->inheritedFromInternalClass = $this->isInternalClass($parentClassLower); } if ($class instanceof Node\Stmt\Enum_) { $this->classDef->enum = true; if ($class->scalarType !== null) { $this->classDef->enumBackingType = $class->scalarType->name; } } if (!$class instanceof Node\Stmt\Trait_) { $this->classDef->implements = $this->parseImplements($class->implements); } else { $this->classDef->trait = $class; // Trait members are compiled later in the consuming class, but // names inside them retain the lexical imports of the trait // declaration. $this->classDef->traitUseNamespaces = $this->useNamespaces; $this->classDef->traitUseAliases = $this->useAliases; $this->classDef->traitUseFunctions = $this->useFunctions; $this->classDef->traitUseConstants = $this->useConstants; } $this->symbolDeclInFile[$fullClassNameLower] = $this->file; if ($class instanceof Node\Stmt\Class_) { $generatedPrinter = null; $generatedArrayable = null; foreach ($class->getMethods() as $method) { if ($method->getAttribute(PrinterLowering::GENERATED_ATTRIBUTE)) { $generatedPrinter = $method; } if ($method->getAttribute(ArrayableLowering::GENERATED_ATTRIBUTE)) { $generatedArrayable = $method; } } if ($generatedPrinter !== null) { $this->classDef->printerGenerated = true; $this->classDef->printerFields = $generatedPrinter->getAttribute(PrinterLowering::FIELDS_ATTRIBUTE); $properties = $this->classDef->printerFields ?? [...$this->parentPublicProperties($this->classDef->extends), ...ClassFieldSelection::ownPublicProperties($class)]; PrinterLowering::rebuildGeneratedMethod( $class, $properties, $this->classDef->printerFields, $this->classStringProperties($this->classDef), ); } if ($generatedArrayable !== null) { $this->classDef->arrayableGenerated = true; $this->classDef->arrayableFields = $generatedArrayable->getAttribute(ArrayableLowering::FIELDS_ATTRIBUTE); $properties = $this->classDef->arrayableFields ?? [...$this->parentPublicProperties($this->classDef->extends), ...ClassFieldSelection::ownPublicProperties($class)]; ArrayableLowering::rebuildGeneratedMethod( $class, $properties, $this->classDef->arrayableFields, ); } } // Property defaults may reference class constants declared later in the // class body. Collect every constant first so default-value validation // is independent of declaration order, matching PHP's class semantics. foreach ($class->stmts as $stmt) { if ($stmt instanceof Node\Stmt\ClassConst) { $this->parseClassConstDef($stmt); } } $code = ''; foreach ($class->stmts as $v) { $type = $v->getType(); switch ($type) { case 'Stmt_ClassConst': break; case 'Stmt_Property': $this->parseClassPropertyDef($v); break; case 'Stmt_TraitUse': $this->prepareTraitUse($v); break; case 'Stmt_Nop': break; case 'Stmt_EnumCase': $caseName = $this->parseIdentifier($v->name); $this->classDef->enumCases[$caseName] = $v->expr?->value; break; case 'Stmt_ClassMethod': $this->prepareClassMethod($v, $class); break; case 'Stmt_Expression': $this->foundStrayCode($v); break; default: abort($v); break; } } // Trait members are later injected into the consuming class for stub // generation. Fully qualify every declared class type while the // trait's own namespace/import context is still active. if ($class instanceof Node\Stmt\Trait_) { foreach ($class->stmts as $v) { if ($v instanceof Node\Stmt\ClassMethod) { $v->returnType = $this->upgradeToFullyQualifiedName($v->returnType); foreach ($v->params as $param) { $param->type = $this->upgradeToFullyQualifiedName($param->type); } } elseif ($v instanceof Node\Stmt\Property || $v instanceof Node\Stmt\ClassConst) { $v->type = $this->upgradeToFullyQualifiedName($v->type); } } } $this->resetClass(); return $code; } /** @return list */ protected function parentPublicProperties(string $parent): array { if ($parent === '') { return []; } $classDef = $this->getClassDef($parent); if ($classDef === null) { return []; } $properties = $this->parentPublicProperties($classDef->extends); foreach ($classDef->properties as $property) { if ($property->isPublic() && !$property->isStatic()) { $properties[] = $property->name; } } return array_values(array_unique($properties)); } /** @return list */ protected function selectableProperties(ClassDef $classDef): array { $properties = []; $parent = $classDef->extends; while ($parent !== '') { $parentDef = $this->getClassDef($parent); if ($parentDef === null) { break; } foreach ($parentDef->properties as $property) { if (!$property->isStatic() && !$property->isPrivate()) { $properties[] = $property->name; } } $parent = $parentDef->extends; } foreach ($classDef->properties as $property) { if (!$property->isStatic()) { $properties[] = $property->name; } } return array_values(array_unique($properties)); } /** @return list */ protected function classStringProperties(ClassDef $classDef): array { $types = []; if ($classDef->extends !== '') { $parent = $this->getClassDef($classDef->extends); if ($parent !== null) { foreach ($this->classStringProperties($parent) as $property) { $types[$property] = true; } } } foreach ($classDef->properties as $property) { if (!$property->isStatic()) { $types[$property->name] = $property->type === Type::STR; } } return array_keys(array_filter($types)); } protected function parseMethodsForTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string { foreach ($class->attrGroups as $groupIndex => $group) { foreach ($group->attrs as $attributeIndex => $attribute) { if (!$this->isRootCompileTimeAttribute($attribute, 'MethodsFor')) { continue; } if (!$class instanceof Node\Stmt\Class_) { $this->fatalCompileTimeAttribute( $class, 'MethodsFor', 'MethodsFor can only be applied to classes', $attribute, ); } if (count($attribute->args) !== 1) { $this->fatalCompileTimeAttribute( $class, 'MethodsFor', 'MethodsFor expects exactly one target', $attribute, ); } $target = $this->parseMethodsForTargetValue($attribute->args[0]->value, $attribute, $class); unset($group->attrs[$attributeIndex]); $group->attrs = array_values($group->attrs); if (empty($group->attrs)) { unset($class->attrGroups[$groupIndex]); $class->attrGroups = array_values($class->attrGroups); } return $target; } } return null; } private function parseMethodsForTargetValue( Node\Expr $value, NodeAbstract $errorNode, Node\Stmt\Class_ $class, ): string { if ($value instanceof Node\Scalar\String_ && $value->value === '*') { return '*'; } if ($value instanceof Node\Expr\ClassConstFetch && $this->isNameExpr($value->class) && $this->isIdExpr($value->name)) { $targetClass = $this->getResolvedPhpName($value->class); $constant = $value->name->toString(); if (strtolower($constant) === 'class') { return $targetClass; } $targets = [ 'Int' => Type::INT, 'Float' => Type::FLOAT, 'Bool' => Type::BOOL, 'BigInt' => Type::BIGINT, 'BigFloat' => Type::BIGFLOAT, 'Decimal' => Type::DECIMAL, 'String' => Type::STR, 'Array' => Type::ARRAY, 'Object' => Type::OBJECT, 'Any' => Type::VAR, 'Stream' => Type::STREAM, 'Box' => Type::BOX, ]; if (strcasecmp($targetClass, 'Type') === 0 && isset($targets[$constant])) { return $targets[$constant]; } } $this->fatalCompileTimeAttribute( $class, 'MethodsFor', "MethodsFor target must be '*', Type::*, or ClassName::class", $errorNode, ); } protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan { $localVarCount = count($this->context->localVars); $beforeStmtCount = count($this->context->beforeStmtLines); $afterStmtCount = count($this->context->afterStmtLines); $expr = $this->parseIdentifier($defaultNode); $init = ''; $clean = ''; $newLocalVars = array_slice($this->context->localVars, $localVarCount, null, true); $newBeforeStmtLines = array_slice($this->context->beforeStmtLines, $beforeStmtCount); $newAfterStmtLines = array_slice($this->context->afterStmtLines, $afterStmtCount); if ($newLocalVars) { $init .= $this->genLocalVarDecl($newLocalVars); $this->context->localVars = array_slice($this->context->localVars, 0, $localVarCount, true); } if ($newBeforeStmtLines) { $init .= implode(PHP_EOL, $newBeforeStmtLines) . PHP_EOL; $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount); } if ($newAfterStmtLines) { $clean .= implode(PHP_EOL, $newAfterStmtLines) . PHP_EOL; $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount); } return new ArrayInitPlan($expr, $init, $clean); } protected function getMethodName(Node\Stmt\ClassMethod $v): string { return $this->parseIdentifier($v->name); } protected function parseClassConstDef(Node\Stmt\ClassConst $v): void { $this->resetFunction(); $flags = $this->parseModifiers($v->flags); [$declaredType, $class] = $v->type ? $this->resolveTypeDecl($v->type, self::DECL_TYPE_OF_CONST) : [null, '']; foreach ($v->consts as $const) { $type = $declaredType; if ($type === null) { $type = match ($const->value->getType()) { 'Expr_Array' => Type::ARRAY, 'Scalar_String' => Type::STR, default => Type::VAR, }; // `::class` is a compile-time magic constant that always yields a string, // so a constant declared as `X = self::class` (or `Foo::class`) must be // typed as a string rather than a generic variant. if ($type === Type::VAR && $const->value instanceof Node\Expr\ClassConstFetch && strtolower((string) $const->value->name) === 'class') { $type = Type::STR; } // A constant whose value references another class constant // (e.g. `X = ParentClass::Y` or `X = self::Y`) must take the referenced // constant's type. This keeps override compatibility checks and the C++ // declaration correct, mirroring PHP where overriding an untyped constant // with a value of any (compatible) type is allowed. if ($type === Type::VAR && $const->value instanceof Node\Expr\ClassConstFetch && $const->value->class instanceof Node\Name) { $refType = $this->resolveReferencedConstantType($const->value, $this->getFullClassName()); if ($refType !== null) { $type = $refType; } } } $constName = $this->parseIdentifier($const->name); if ($this->classDef->hasConstant($constName)) { $this->fatalError($v, "Duplicate constant `{$constName}`"); } $constInfo = $this->parseClassLikeConstant($const, $flags, $type, $class, $declaredType); $constInfo->class = $class; $this->classDef->constants[$constInfo->name] = $constInfo; } } /** * Resolve the compile-time type of a class constant whose value is a * `ClassConstFetch` referencing another constant (e.g. `X = ParentClass::Y` * or `X = self::Y`). Returns the referenced constant's type, or null when * the reference cannot be resolved yet (for instance when the referenced * class has not been prepared). `::class` always resolves to a string. */ protected function resolveReferencedConstantType(Node\Expr\ClassConstFetch $fetch, string $currentClass): ?string { $constName = $fetch->name->toString(); if (strcasecmp($constName, 'class') === 0) { return Type::STR; } if (!($fetch->class instanceof Node\Name)) { return null; } $className = $fetch->class->toString(); if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { $targetClass = $currentClass; } elseif (strcasecmp($className, 'parent') === 0) { $targetClass = $this->getParentClass($currentClass); } else { $targetClass = $this->getNamespacedClassName($className); } if ($targetClass === '' || !$this->hasClass($targetClass)) { return null; } $def = $this->getClass($targetClass); if (!$def->hasConstant($constName)) { return null; } $refConst = $def->getConstant($constName); // Follow the chain in case the referenced constant is itself an // expression that resolves to another constant. if ($refConst->type !== Type::VAR) { return $refConst->type; } if ($refConst->valueExpr instanceof Node\Expr\ClassConstFetch) { return $this->resolveReferencedConstantType($refConst->valueExpr, $targetClass); } return null; } private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = '', ?string $declaredType = null): ConstantDef { $constName = $this->parseIdentifier($const->name); $constValue = $this->parseIdentifier($const->value); $constInfo = new ConstantDef($constName, $flags, $type, $constValue); $constInfo->valueExpr = $const->value; $constInfo->declaredType = $declaredType; if ($this->context->beforeStmtLines) { $arrayExpr = ''; if ($this->context->localVars) { $arrayExpr .= $this->genScopeVarDecl(); } $arrayExpr .= $this->parseBeforeStmtLines(); $constInfo->arrayExpr = $arrayExpr; } $constInfo->class = $class; return $constInfo; } /** * Create and register a class property with type normalization, shared by * regular property declarations and constructor property promotion. */ protected function addClassProperty(string $name, int $flags, ?NodeAbstract $typeNode, $defaultNode, bool $nullable, NodeAbstract $errorNode, bool $promoted = false): PropertyDef { $flags = $this->parseModifiers($flags); $this->validateAsymmetricPropertyDeclaration($name, $flags, $typeNode, $errorNode); [$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY); $this->assertSupportedNativeObjectTypeNode($typeNode, self::DECL_TYPE_OF_PROPERTY, $errorNode); $nullableNative = $this->resolveNullableNativeObjectType( $typeNode, self::DECL_TYPE_OF_PROPERTY, ); if ($nullableNative !== null) { [$type, $class] = $nullableNative; $nullable = true; } if ($this->isNativeObjectClass($class) && !$this->classDef->nativeObject) { $this->fatalError( $errorNode, 'Native object types can only be used as properties of native classes', ); } $default = null; $arrayInitPlan = null; if ($defaultNode !== null) { $this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode); if ($defaultNode instanceof Node\Expr\Array_) { $arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode); $default = $arrayInitPlan->expr; // Only narrow the property type to `array` when the declared type // cannot already hold an array. `mixed`/`iterable`/union/nullable // types are represented as php::Var and can legally store an array, // so forcing `array` here would wrongly reject non-array assignments // (e.g. `mixed $value = []` followed by `$this->value = 123`). if ($type !== Type::VAR) { $type = Type::ARRAY; } } else { $default = $this->parseIdentifier($defaultNode); } } if ($this->classDef->hasProperty($name)) { $this->fatalError($errorNode, "Duplicate property `{$name}`"); } $propDef = new PropertyDef($name, $flags, $type, $default, $nullable); $propDef->overrideRequired = (bool) $errorNode->getAttribute( FunctionAttributeLowering::OVERRIDE_ATTRIBUTE, false, ); $propDef->node = $errorNode; if ($typeNode !== null && !$typeNode instanceof NullableType && !$typeNode instanceof UnionType && !$typeNode instanceof IntersectionType ) { $propDef->explicitAny = strtolower($this->parseIdentifier($typeNode)) === 'any'; } $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->class = $class; $propDef->arrayInitPlan = $arrayInitPlan; $propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode); $propDef->promoted = $promoted; if ($typeNode instanceof NullableType || $typeNode instanceof UnionType || $typeNode instanceof IntersectionType) { $typeInfo = $this->buildTypeCheckFromNode($typeNode); $propDef->typeCheck = $typeInfo['check']; $propDef->typeStr = $typeInfo['typeStr']; } $this->classDef->properties[$name] = $propDef; return $propDef; } private function validateAsymmetricPropertyDeclaration( string $name, int $flags, ?NodeAbstract $typeNode, NodeAbstract $errorNode, ): void { if (!($flags & (Modifiers::PRIVATE_SET | Modifiers::PROTECTED_SET))) { return; } $className = $this->classDef->getNamespacedName(false); if ($typeNode === null) { $this->fatalError( $errorNode, "Property with asymmetric visibility {$className}::\${$name} must have type", ); } $readVisibility = $flags & Modifiers::PRIVATE ? 1 : ($flags & Modifiers::PROTECTED ? 2 : 3); $setVisibility = $flags & Modifiers::PRIVATE_SET ? 1 : 2; if ($readVisibility < $setVisibility) { $this->fatalError( $errorNode, "Visibility of property {$className}::\${$name} must not be weaker than set visibility", ); } } /** * gen_stub emits compile-time scalar values and empty arrays exactly into * the internal class default-property table. Non-empty arrays are emitted * there as an empty-array placeholder, while enum cases need a live object; * both must therefore be restored by create_object. Keep an unresolved * expression on that conservative runtime path as well. */ private function propertyDefaultRequiresRuntimeInit(?NodeAbstract $default): bool { if ($default === null) { return false; } if ($default instanceof Node\Expr\Array_) { return $default->items !== []; } // A null result includes enum cases and constants which cannot be // resolved safely during preprocessing. Their runtime initialization // must not be removed merely because their source syntax resembles a // scalar constant expression. $type = $this->detectDefaultValueType($default); return $type === null || $type === 'array'; } /** * Diagnose, during preprocessing, whether a property's default value is * compatible with its declared type. * * TypePHP rejects obvious mismatches such as `int $a = []` at compile time * instead of silently coercing the declared type or deferring to a runtime * TypeError, matching the static-compilation principles in CLAUDE.md. */ protected function checkPropertyDefaultType(string $name, ?NodeAbstract $typeNode, NodeAbstract $defaultNode, NodeAbstract $errorNode): void { if ($typeNode === null) { // Untyped property accepts any default value. return; } $valueType = $this->detectDefaultValueType($defaultNode); if ($valueType === null) { // The value type is not statically decidable (e.g. user or class // constant references); leave it to later stages. return; } $allowed = $this->collectAllowedDefaultTypes($typeNode); if ($allowed === null) { // mixed / callable / otherwise unconstrained type declaration. return; } if (in_array($valueType, $allowed, true)) { return; } $className = $this->getFullClassName(); $typeStr = $this->propertyTypeDeclToString($typeNode); $this->fatalError( $errorNode, "Cannot use {$valueType} as default value for property {$className}::\${$name} of type {$typeStr}" ); } /** * Determine the PHP value type of a constant expression used as a default * value. Returns one of int/float/string/true/false/array/null, or null when * the type cannot be decided statically. */ protected function detectDefaultValueType(NodeAbstract $node, ?string $scopeClass = null, int $depth = 0): ?string { if ($depth > 16) { return null; } $scopeClass ??= $this->getFullClassName(); switch ($node->getType()) { case 'Scalar_Int': return 'int'; case 'Scalar_Float': return 'float'; case 'Scalar_String': case 'Scalar_InterpolatedString': case 'Expr_BinaryOp_Concat': return 'string'; case 'Expr_Array': return 'array'; case 'Expr_UnaryMinus': case 'Expr_UnaryPlus': return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1); case 'Expr_ConstFetch': return match (strtolower($node->name->toString())) { 'true' => 'true', 'false' => 'false', 'null' => 'null', default => null, }; case 'Expr_ClassConstFetch': if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) { return null; } $constName = $node->name->toString(); if (strcasecmp($constName, 'class') === 0) { return 'string'; } $className = $node->class->toString(); if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { $targetClass = $scopeClass; } elseif (strcasecmp($className, 'parent') === 0) { $targetClass = $this->getParentClass($scopeClass); } else { $targetClass = $this->getNamespacedClassName($className); } if ($targetClass === '' || !$this->hasClass($targetClass)) { return null; } $targetDef = $this->getClass($targetClass); if (!$targetDef->hasConstant($constName)) { return null; } return $this->detectDefaultValueType( $targetDef->getConstant($constName)->valueExpr, $targetClass, $depth + 1 ); default: try { $value = (new ConstExprEvaluator( static function (Node\Expr $expr): never { throw new \RuntimeException('Unresolved constant expression'); } ))->evaluateDirectly($node); } catch (\Throwable) { return null; } return match (true) { is_int($value) => 'int', is_float($value) => 'float', is_string($value) => 'string', $value === true => 'true', $value === false => 'false', is_array($value) => 'array', $value === null => 'null', default => null, }; } } /** * Collect the set of value types accepted as a default for a declared type * node. Returns null when the type imposes no statically-checkable * constraint (mixed / callable / unknown). * * @return array|null */ protected function collectAllowedDefaultTypes(NodeAbstract $typeNode): ?array { if ($typeNode instanceof NullableType) { $inner = $this->collectAllowedDefaultTypes($typeNode->type); if ($inner === null) { return null; } return array_values(array_unique(array_merge($inner, ['null']))); } if ($typeNode instanceof UnionType) { $all = []; foreach ($typeNode->types as $sub) { $part = $this->collectAllowedDefaultTypes($sub); if ($part === null) { // A mixed-like member accepts any default value. return null; } $all = array_merge($all, $part); } return array_values(array_unique($all)); } if ($typeNode instanceof IntersectionType) { // Intersection types are object-only; no scalar/array default valid. return []; } return match (strtolower($this->parseIdentifier($typeNode))) { 'int' => ['int'], 'float', 'double' => ['float', 'int'], // int coerces to float 'string' => ['string'], 'bool' => ['true', 'false'], 'true' => ['true'], 'false' => ['false'], 'array' => ['array'], 'iterable' => ['array'], 'null' => ['null'], 'object' => [], // no literal object default exists 'self', 'parent', 'static' => [], 'mixed', 'any' => null, 'callable' => null, // string/array/closure — not checkable default => [], // class type: only null via ?Type }; } protected function propertyTypeDeclToString(NodeAbstract $typeNode): string { if ($typeNode instanceof NullableType) { return '?' . $this->propertyTypeDeclToString($typeNode->type); } if ($typeNode instanceof UnionType) { $parts = []; foreach ($typeNode->types as $t) { $parts[] = $this->propertyTypeDeclToString($t); } return implode('|', $parts); } if ($typeNode instanceof IntersectionType) { $parts = []; foreach ($typeNode->types as $t) { $parts[] = $this->propertyTypeDeclToString($t); } return implode('&', $parts); } return $this->parseIdentifier($typeNode); } protected function parseClassPropertyDef(Node\Stmt\Property $v): void { $arrayDef = $this->parseArrayDefinition($v); if ($this->classDef->nativeObject) { if ($v->type === null) { $this->fatalError($v, 'Native class properties must declare a type'); } if ($v->isStatic()) { $this->fatalError($v, 'Native class static properties are not supported'); } // A Zend readonly property carries runtime initialization state. // Native fields deliberately have no Zend property slot, so a raw // C++ assignment would silently bypass the readonly contract. // Reject it until Native objects have an equally explicit state // representation instead of emitting behavior that only appears // readonly at compile time. if ($v->isReadonly() || ($this->classDef->flags & Modifiers::READONLY)) { $this->fatalError($v, 'Native class readonly properties are not supported'); } } $oriCtx = $this->context; $this->context = $this->classDef->propertyContext; $nullable = $v->type instanceof NullableType; foreach ($v->props as $prop) { $propName = $this->parseIdentifier($prop->name); $propDef = $this->addClassProperty($propName, $v->flags, $v->type, $prop->default, $nullable, $v); $propDef->arrayDef = $arrayDef; if ($this->classDef->nativeObject && $this->isNativeObjectForbiddenPropertyType($propDef)) { $message = $propDef->type === Type::BOX ? 'Native class properties cannot use Box types' : 'Native class properties cannot use Std Container types'; $this->fatalError($v, $message); } $hookMetadata = $v->getAttribute(PropertyHookLowering::PROPERTY_ATTRIBUTE, []); $propDef->virtual = (bool) ($hookMetadata['virtual'] ?? false); foreach ($v->hooks as $hook) { $kind = strtolower($hook->name->toString()); if ($kind === 'get') { if ($hook->byRef) { $this->fatalError($hook, 'Property get hooks returning by reference are not supported'); } $propDef->getter = PropertyHookLowering::getterName($propName); } elseif ($kind === 'set') { $propDef->setter = PropertyHookLowering::setterName($propName); } } } $this->context = $oriCtx; } protected function prepareClassMethod(Node\Stmt\ClassMethod $v, Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): void { $this->resetMethod(); $name = $this->getMethodName($v); $this->method = $name; $this->assertNativeMagicMethodSupported($v, $name); $flags = $this->parseModifiers($v->flags); $abstract = $flags & Modifiers::ABSTRACT; if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) { $this->fatalError($v, 'Native class static methods are not supported'); } if (!$abstract) { $this->methodDef = new MethodDef($flags, $name); $this->methodDef->node = $v; $traitOrigin = $v->getAttribute('typephp_trait_origin'); if (is_string($traitOrigin)) { $this->methodDef->traitOrigin = $traitOrigin; } if ($this->classDef->hasMethod($name)) { $generatedBy = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY); $generatedTarget = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET); if (is_string($generatedBy) && $generatedTarget instanceof Node) { $this->fatalCompileTimeAttribute( $generatedTarget, $generatedBy, "Duplicate method `{$this->method}`", $generatedTarget, null, $this->classDef->getMethod($name)->node, ); } $this->fatalError($v, "Duplicate method `{$this->method}`"); } $this->prepareFunction($v); if ($class instanceof Node\Stmt\Trait_) { // Trait functions are templates, not native symbols. The // FunctionDef remains owned by MethodDef and is cloned into // every class that composes the trait. $this->symbols->removeFunction($this->getFunctionName($v)); } $this->checkRequiredArgNum($name, $this->methodDef, $v); $this->classDef->addMethod($this->methodDef); } else { if ($this->classDef->hasMethod($name) || $this->classDef->hasAbstractMethod($name)) { $this->fatalError($v, "Duplicate method `{$this->method}`"); } if (!$class instanceof Node\Stmt\Trait_ && isset($class->flags) && !($class->flags & Modifiers::ABSTRACT)) { $this->fatalError($v, "Non-abstract class {$this->class} contains abstract method {$v->name}"); } $this->methodDef = new MethodDef($flags, $name); $this->methodDef->node = $v; $traitOrigin = $v->getAttribute('typephp_trait_origin'); if (is_string($traitOrigin)) { $this->methodDef->traitOrigin = $traitOrigin; } // Keep abstract method metadata in the symbol repository as well. // Native virtual calls need the same argument/default lowering as // concrete calls, even though no free-function body is emitted. $this->prepareFunction($v); $this->methodDef->functionDef->abstractMethod = true; $this->checkRequiredArgNum($name, $this->methodDef, $v); if ($this->method === '__construct') { foreach ($v->params as $param) { if ($param->isPromoted()) { $this->fatalError($v, 'Cannot declare promoted property in an abstract constructor'); } } } $this->classDef->addAbstractMethod($name, $flags, $this->methodDef); } $fullClassName = $this->getFullClassName(); $fullMethodName = $fullClassName . '::' . $this->method; $fullMethodNameLower = strtolower($fullMethodName); $fullClassNameLower = strtolower($fullClassName); // 检查子类是否已覆盖此方法(子类先于父类被预处理的情况) $isOverridden = $this->isMethodOverriddenInSubClasses($fullClassNameLower, $this->method); $this->classMethodOverride[$fullMethodNameLower] = $isOverridden; // 查找父类是否有同名方法,递归向上标记父类方法已被覆盖 while (($parentClass = $this->symbols->parent($fullClassNameLower)) !== '') { $parentMethodLower = strtolower($parentClass . '::' . $this->method); if (isset($this->classMethodOverride[$parentMethodLower])) { $this->classMethodOverride[$parentMethodLower] = true; } $fullClassNameLower = strtolower($parentClass); } $this->resetMethod(); } /** * 递归检查所有子类(及子类的子类)是否已定义了同名方法,用于处理子类先于父类被预处理的情况。 */ private function isMethodOverriddenInSubClasses(string $classNameLower, string $method): bool { if (!isset($this->classSubClasses[$classNameLower])) { return false; } $stack = $this->classSubClasses[$classNameLower]; while (!empty($stack)) { $subClass = array_shift($stack); $subMethodLower = $subClass . '::' . strtolower($method); if (isset($this->classMethodOverride[$subMethodLower])) { return true; } if (isset($this->classSubClasses[$subClass])) { foreach ($this->classSubClasses[$subClass] as $grandChild) { $stack[] = $grandChild; } } } return false; } protected function parseInterface(Node\Stmt\Interface_ $v): void { $this->resetClass(); $this->resetMethod(); $this->resetFunction(); $name = $this->parseIdentifier($v->name); $this->interface = $name; $this->interfaceDef = new InterfaceDef($name, $this->namespace); $interfaceName = $this->interfaceDef->getNamespacedName(false); $interfaceNameLower = strtolower($interfaceName); foreach ($v->extends as $parent) { $parentName = $this->getNamespacedClassName($this->parseIdentifier($parent)); $this->interfaceDef->extendsList[] = $parentName; if ($this->interfaceDef->extends === '') { $this->interfaceDef->extends = $parentName; } if (!$this->isInternalInterface($parentName)) { $this->symbolCallInFile[$this->file][] = strtolower($parentName); } } if (isset($this->symbolDeclInFile[$interfaceNameLower])) { $this->fatalError($v, "Duplicate interface `{$interfaceName}`"); } $this->symbolDeclInFile[$interfaceNameLower] = $this->file; $this->symbols->putInterface($this->escapeClass($interfaceName), $this->interfaceDef); $this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef; foreach ($v->stmts as $stmt) { if ($stmt instanceof Node\Stmt\ClassConst) { foreach ($stmt->consts as $const) { $constName = $this->parseIdentifier($const->name); if ($this->interfaceDef->hasConstant($constName)) { $this->fatalError($stmt, "Duplicate constant `{$constName}`"); } if ($stmt->type) { [$type, $class] = $this->resolveTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST); } else { $class = ''; $type = match ($const->value->getType()) { 'Expr_Array' => Type::ARRAY, 'Scalar_String' => Type::STR, default => Type::VAR, }; } $constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class, $stmt->type ? $type : null); $this->interfaceDef->constants[$constName] = $constInfo; } continue; } if ($stmt instanceof Node\Stmt\ClassMethod) { $methodName = $this->getMethodName($stmt); if ($this->interfaceDef->hasMethod($methodName)) { $this->fatalError($stmt, "Duplicate method `{$methodName}`"); } $this->method = $methodName; $methodDef = new MethodDef($this->parseModifiers($stmt->flags), $methodName); $methodDef->node = $stmt; $methodDef->functionDef = $this->parseFunctionDecl($stmt); $methodDef->functionDef->method = true; $this->interfaceDef->addMethod($methodDef); $this->resetMethod(); $this->resetFunction(); continue; } if ($stmt instanceof Node\Stmt\Property) { $this->prepareInterfaceProperty($stmt); continue; } if (!$stmt instanceof Node\Stmt\Nop) { $this->fatalError($stmt, 'Unsupported interface statement: ' . $stmt->getType()); } } $this->resetMethod(); $this->resetFunction(); $this->interface = ''; $this->interfaceDef = null; } private function prepareInterfaceProperty(Node\Stmt\Property $property): void { if ($property->hooks === []) { $this->fatalError($property, 'Interfaces may only include hooked properties'); } if ($property->flags & (Modifiers::PRIVATE | Modifiers::PROTECTED)) { $this->fatalError($property, 'Property in interface cannot be protected or private'); } if ($property->flags & Modifiers::FINAL) { $this->fatalError($property, 'Property in interface cannot be final'); } if ($property->flags & Modifiers::STATIC) { $this->fatalError($property, 'Cannot declare hooks for static property'); } if ($property->flags & Modifiers::READONLY) { $this->fatalError($property, 'Hooked properties cannot be readonly'); } $readable = false; $writable = false; foreach ($property->hooks as $hook) { if ($hook->body !== null) { $this->fatalError($hook, 'Abstract property hook cannot have body'); } $kind = strtolower($hook->name->toString()); if ($kind === 'get') { if ($hook->byRef) { $this->fatalError($hook, 'Property get hooks returning by reference are not supported'); } if ($readable) { $this->fatalError($hook, 'Cannot redeclare property hook "get"'); } $readable = true; } elseif ($kind === 'set') { if ($writable) { $this->fatalError($hook, 'Cannot redeclare property hook "set"'); } if ($hook->params !== []) { $this->fatalError( $hook, 'Explicit setter parameters in interface property hooks are not supported yet', ); } $writable = true; } else { $this->fatalError($hook, "Unknown hook `{$kind}`, expected `get` or `set`"); } } [$type, $class] = $this->resolveTypeDecl($property->type, self::DECL_TYPE_OF_PROPERTY); $nullable = $property->type instanceof NullableType; foreach ($property->props as $prop) { $name = $this->parseIdentifier($prop->name); if ($property->getAttribute(FunctionAttributeLowering::OVERRIDE_ATTRIBUTE, false)) { $this->fatalCompileTimeAttribute( $property, 'Override', "{$this->interfaceDef->getNamespacedName(false)}::\${$name} has #[\\Override] attribute, " . 'but no matching parent class property exists', ); } if ($this->interfaceDef->hasProperty($name)) { $this->fatalError($property, "Duplicate property `{$name}`"); } if ($prop->default !== null) { $this->fatalError($property, "Cannot specify default value for virtual hooked property {$this->interfaceDef->getNamespacedName(false)}::\${$name}"); } $definition = new InterfacePropertyDef( $name, $this->parseModifiers($property->flags), $type, $nullable, $readable, $writable, $property, ); $definition->class = $class; if ($property->type instanceof NullableType || $property->type instanceof UnionType || $property->type instanceof IntersectionType ) { $typeInfo = $this->buildTypeCheckFromNode($property->type); $definition->typeCheck = $typeInfo['check']; $definition->typeStr = $typeInfo['typeStr']; } $this->interfaceDef->properties[$name] = $definition; } } protected function parseTraitUseOptions(Node\Stmt\TraitUse $traitUse, array &$aliases, array &$ignored): void { foreach ($traitUse->adaptations as $adaptation) { if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { $traits = []; if (!$adaptation->trait) { // use THello1, THello2 { // hello as hello3; // } // 未指定 trait,将添加所有 trait 的别名映射,在预处理阶段无法获取 trait 的方法列表 $traits = $traitUse->traits; } else { $traits[] = $adaptation->trait; } foreach ($traits as $trait) { $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait)); $methodName = $adaptation->method->toString(); /* * 例如: * use TraitA { TraitA::method as newMethod} * 这表示 TraitA::method() 会被重命名为 TraitA::newMethod() */ $aliases[$this->getFullMethodName($traitName, $methodName)][] = [ 'newName' => $adaptation->newName ? $adaptation->newName->toString() : $methodName, 'newModifier' => $adaptation->newModifier ?: 0, ]; } } if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) { if (!$adaptation->trait) { $this->fatalError($traitUse, 'Trait precedence cannot be used without a trait'); } $methodName = $adaptation->method->toString(); /* * 例如: * use TraitA { TraitA::method insteadof TraitB} * 这表示 TraitB::method() 将会被忽略,真正执行的是 TraitA::method() */ foreach ($adaptation->insteadof as $trait2) { $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait2)); $ignored[$this->getFullMethodName($traitName, $methodName)] = true; } } } } protected function prepareTraitUse(Node\Stmt\TraitUse $v): void { $aliases = []; $ignored = []; if ($v->adaptations) { $this->parseTraitUseOptions($v, $aliases, $ignored); } foreach ($v->traits as $trait) { $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait)); if (!$this->isInternalClass($traitName)) { $this->symbolCallInFile[$this->file][] = strtolower($traitName); } } foreach ($aliases as $fullMethodName => $aliasList) { foreach ($aliasList as $alias) { $this->classDef->traitAliases[$fullMethodName][] = $alias; } } $this->classDef->traitIgnored = array_merge($this->classDef->traitIgnored, $ignored); } }