diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 706b4ec5..221963e0 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -80,6 +80,7 @@ class CompilerBaseApiTest extends TestCase public function testMethodCacheKeepsPreviouslyAssignedClassLifetime(): void { + $this->setPropertyValue('compilerPhase', 'convert'); // The two maps have independent ID spaces. This reproduces the tpc // bootstrap ordering where both classes occupied slot zero. $this->setPropertyValue('classMap', ['LateKnownClass' => 0]); @@ -100,6 +101,7 @@ class CompilerBaseApiTest extends TestCase public function testMethodCacheRejectsMismatchedLifetimeDomains(): void { + $this->setPropertyValue('compilerPhase', 'convert'); $this->setPropertyValue('classMap', ['LateKnownClass' => 0]); $this->setPropertyValue('classIndex', 1); $this->setPropertyValue('persistentFuncMap', ['LateKnownClass::run' => 0]); @@ -112,6 +114,78 @@ class CompilerBaseApiTest extends TestCase $this->invokeMethod('getMethodPtr', 'LateKnownClass', 'run'); } + public function testPrepareStoresDeclarationAstsWithoutAllocatingCacheIds(): void + { + $consumer = $this->testDir . '/consumer.php'; + $declaration = $this->testDir . '/late.php'; + file_put_contents($consumer, <<<'PHP' +compiler->prepareFile($consumer); + $this->compiler->prepareFile($declaration); + + foreach (['classMap', 'persistentClassMap', 'funcMap', 'persistentFuncMap', 'persistentPropMap'] as $map) { + $this->assertSame([], $this->getPropertyValue($map), $map); + } + $defaults = $this->compiler->getClassDef('CachePhase\\Defaults'); + $this->assertNotNull($defaults); + $this->assertInstanceOf( + \PhpParser\Node\Expr\ClassConstFetch::class, + $defaults->getConstant('VALUE')->valueExpr, + ); + $this->assertSame('', $defaults->getConstant('VALUE')->value); + $this->assertNotNull($defaults->getProperty('property')->defaultExpr); + $this->assertTrue($defaults->getMethod('read')->functionDef->argInfoList[0]->hasDefaultValue()); + $this->assertSame('', $defaults->getMethod('read')->functionDef->argInfoList[0]->default); + + $this->setPropertyValue('compilerPhase', 'convert'); + $this->compiler->finalizeDeclarationExpressions([$consumer, $declaration]); + + $this->assertSame([], $this->getPropertyValue('classMap')); + $this->assertArrayHasKey( + 'CachePhase\\LateClass', + $this->getPropertyValue('persistentClassMap'), + ); + $this->assertNotSame('', $defaults->getConstant('VALUE')->value); + $this->assertNotNull($defaults->getProperty('property')->default); + $this->assertNotSame('', $defaults->getMethod('read')->functionDef->argInfoList[0]->default); + } + + public function testCacheIdAllocationIsRejectedOutsideConvertPhase(): void + { + $this->setPropertyValue('compilerPhase', 'prepare'); + $this->expectException(TestError::class); + $this->expectExceptionMessage( + 'class cache ID allocation can only be used during convert phase, current phase is prepare', + ); + $this->invokeMethod('getClassId', 'ForbiddenDuringPrepare'); + } + private function fixturePath(string $file): string { return __DIR__ . '/../code/compiler_api/' . $file; diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 86984f93..377f9029 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -287,7 +287,7 @@ class PreprocessorTest extends TestCase $this->assertNotEmpty($functionDef->returnTypeCheck); } - public function testPrepareFileParsesInterfaceArrayConstantInitExpr(): void + public function testInterfaceArrayConstantIsLoweredOnlyDuringConvert(): void { $file = __DIR__ . '/../code/interface_array_constant.php'; @@ -298,6 +298,15 @@ class PreprocessorTest extends TestCase $constant = $interfaces['interfacearrayconstant']->constants['ITEMS']; $this->assertSame('php::Array', $constant->type); + $this->assertInstanceOf(\PhpParser\Node\Expr\Array_::class, $constant->valueExpr); + $this->assertSame('', $constant->value); + $this->assertSame([], $this->getProperty('classMap')); + $this->assertSame([], $this->getProperty('persistentClassMap')); + $this->assertSame([], $this->getProperty('funcMap')); + $this->assertSame([], $this->getProperty('persistentFuncMap')); + + $this->setProperty('compilerPhase', 'convert'); + $this->compiler->finalizeDeclarationExpressions([$file]); $this->assertStringContainsString('php::Array', $constant->value); } diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 1ba70006..4fbf35e8 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -249,49 +249,59 @@ trait SourcePipelineTrait public function convert(array $files): array { - $sourceFiles = []; - $validSourceCount = 0; - // 生成 C++ 文件 - foreach ($files as $k => $file) { - try { - if (FileScanner::isPhpFile($file)) { - $cppFile = $this->convertFile($file); - } elseif (FileScanner::isNativeSourceFile($file)) { - $cppFile = $file; - } else { - continue; - } - $validSourceCount++; - if ($cppFile !== null) { - $sourceFiles[] = $cppFile; + $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); + try { + // All declarations are now known. Lower declaration constant + // expressions before translating any function body so cache IDs + // are assigned exclusively in the convert phase. + $this->finalizeDeclarationExpressions($files); + + $sourceFiles = []; + $validSourceCount = 0; + // 生成 C++ 文件 + foreach ($files as $k => $file) { + try { + if (FileScanner::isPhpFile($file)) { + $cppFile = $this->convertFile($file); + } elseif (FileScanner::isNativeSourceFile($file)) { + $cppFile = $file; + } else { + continue; + } + $validSourceCount++; + if ($cppFile !== null) { + $sourceFiles[] = $cppFile; + } + } catch (Unsupported $e) { + echo ' unsupported syntax: ' . $e->getMessage() . "\n"; + echo ' skip: ' . $file . "\n"; + unset($files[$k]); } - } catch (Unsupported $e) { - echo ' unsupported syntax: ' . $e->getMessage() . "\n"; - echo ' skip: ' . $file . "\n"; - unset($files[$k]); } - } - // A valid PHP input may intentionally emit no standalone translation - // unit (for example a compile-time trait or an interface). The shared - // extension source still carries its runtime metadata, so only reject - // an input set in which no supported source was converted at all. - if ($validSourceCount === 0) { - $this->stop('No valid source file found'); - } + // A valid PHP input may intentionally emit no standalone translation + // unit (for example a compile-time trait or an interface). The shared + // extension source still carries its runtime metadata, so only reject + // an input set in which no supported source was converted at all. + if ($validSourceCount === 0) { + $this->stop('No valid source file found'); + } - // A WASI library publishes WIT/Component exports rather than a native - // TypePHP shared-library ABI, so a PHP import stub would be misleading. - if ($this->isBuildModeLib() && !$this->isWasiTarget()) { - $this->genLibraryImportStub($files); - } + // A WASI library publishes WIT/Component exports rather than a native + // TypePHP shared-library ABI, so a PHP import stub would be misleading. + if ($this->isBuildModeLib() && !$this->isWasiTarget()) { + $this->genLibraryImportStub($files); + } - // 生成构建期内部头文件:函数声明、运行时数据声明 - $this->genFunctionDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_func_decl.h"); - $this->genDataDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_data_decl.h"); - // 生成扩展模块源文件 - $sourceFiles[] = $this->genExtension(); + // 生成构建期内部头文件:函数声明、运行时数据声明 + $this->genFunctionDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_func_decl.h"); + $this->genDataDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_data_decl.h"); + // 生成扩展模块源文件 + $sourceFiles[] = $this->genExtension(); - return $sourceFiles; + return $sourceFiles; + } finally { + $this->restoreCompilerPhase($previousPhase); + } } } diff --git a/src/Build/WasmInterfaceGenerator.php b/src/Build/WasmInterfaceGenerator.php index 1ab059be..6109e67e 100644 --- a/src/Build/WasmInterfaceGenerator.php +++ b/src/Build/WasmInterfaceGenerator.php @@ -55,7 +55,7 @@ final class WasmInterfaceGenerator $parameters = []; foreach ($function->argInfoList as $argument) { - if ($argument->byRef || $argument->variadic || $argument->default !== '') { + if ($argument->byRef || $argument->variadic || $argument->hasDefaultValue()) { throw new RuntimeException( "WasmExport parameter \${$argument->phpName} of {$displayName}() cannot be by-reference, variadic, or optional" ); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0924868b..0e05a3d8 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -316,6 +316,9 @@ class CompilerBase implements PropertyAccessContext */ protected array $persistentPropMap = []; protected int $persistentPropIndex = 0; + /** @var array> Prepared declaration ASTs keyed by real path. */ + protected array $preparedFileAsts = []; + protected bool $declarationExpressionsFinalized = false; protected const array PHP_RUNTIME_TYPE_MAP = [ 'integer' => Type::INT, 'double' => Type::FLOAT, @@ -1277,19 +1280,6 @@ class CompilerBase implements PropertyAccessContext { if (str_contains($funcName, '::')) { [$class] = explode('::', $funcName, 2); - - // Class and method caches use parallel lifetime domains. Once a - // class has been assigned an ID, keep every subsequently resolved - // method in the same domain even if the class becomes visible in - // the symbol repository later in the prepare pass. Otherwise a - // request-local class ID may be used to index persistentClassMap. - if (isset($this->classMap[$class])) { - return false; - } - if (isset($this->persistentClassMap[$class])) { - return true; - } - return $this->isProcessStableClass($class); } if ($this->hasFunction($funcName)) { @@ -1301,6 +1291,7 @@ class CompilerBase implements PropertyAccessContext protected function getClassId(string $className): int { + $this->assertCompilerPhase(self::PHASE_CONVERT, 'class cache ID allocation'); if (isset($this->classMap[$className])) { return $this->classMap[$className]; } @@ -1319,6 +1310,7 @@ class CompilerBase implements PropertyAccessContext protected function getFuncId(string $funcName): int { + $this->assertCompilerPhase(self::PHASE_CONVERT, 'function cache ID allocation'); if (isset($this->funcMap[$funcName])) { return $this->funcMap[$funcName]; } @@ -1346,6 +1338,7 @@ class CompilerBase implements PropertyAccessContext */ protected function getPropertyId(string $className, string $propName): int { + $this->assertCompilerPhase(self::PHASE_CONVERT, 'property cache ID allocation'); $key = $className . '::' . $propName; if (isset($this->persistentPropMap[$key])) { return $this->persistentPropMap[$key]; @@ -2869,6 +2862,9 @@ class CompilerBase implements PropertyAccessContext if ($interfaceConstDef->type === Type::ARRAY) { return self::PREFIX . $this->getNativeName($interfaceConstDef->name, $interfaceDef->namespace, $interfaceDef->name); } + if (!$interfaceConstDef->codegenFinalized) { + return false; + } $expr->setAttribute('nativeConst', $interfaceConstDef); return $interfaceConstDef->value; } @@ -2887,6 +2883,13 @@ class CompilerBase implements PropertyAccessContext if ($constDef->type === Type::ARRAY) { return self::PREFIX . $this->getNativeName($constDef->name, $classDef->namespace, $classDef->name); } else { + // Forward constant references may be encountered while the + // declaration-expression pass is still visiting another file. + // Fall back to the Zend class-constant lookup instead of emitting + // an incomplete value; the lookup is cached in the convert phase. + if (!$constDef->codegenFinalized) { + return false; + } $expr->setAttribute('nativeConst', $constDef); return $constDef->value; } diff --git a/src/Entity/ArgInfo.php b/src/Entity/ArgInfo.php index a7d9038d..b7c17f70 100644 --- a/src/Entity/ArgInfo.php +++ b/src/Entity/ArgInfo.php @@ -19,6 +19,8 @@ class ArgInfo public string $type; public string $default = ''; public ?ArrayInitPlan $arrayInitPlan = null; + /** Original declaration AST; lowered to $default only in the convert phase. */ + public ?Expr $defaultExpr = null; public ?Expr $defaultValue = null; public string $class = ''; @@ -56,4 +58,9 @@ class ArgInfo /** Original union/nullable AST node. Only set when typeCheck is non-null. */ public ?NodeAbstract $typeNode = null; + + public function hasDefaultValue(): bool + { + return $this->defaultExpr !== null; + } } diff --git a/src/Entity/ConstantDef.php b/src/Entity/ConstantDef.php index ed587ee9..c4c62cde 100644 --- a/src/Entity/ConstantDef.php +++ b/src/Entity/ConstantDef.php @@ -19,6 +19,8 @@ class ConstantDef public string $arrayExpr = ''; public string $class = ''; public ?NodeAbstract $valueExpr = null; + /** True after the declaration AST has been lowered to C++ in convert. */ + public bool $codegenFinalized = false; /** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */ public ?string $declaredType = null; diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index c36891fa..cd3a0b01 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -19,6 +19,8 @@ class PropertyDef public int $flags; public ?string $default = null; public ?ArrayInitPlan $arrayInitPlan = null; + /** Original declaration AST; lowered to $default only in the convert phase. */ + public ?NodeAbstract $defaultExpr = null; public ?ArrayDefinition $arrayDef = null; public bool $nullable = false; /** The declared type is TypePHP's unconstrained, reference-capable `any` type. */ diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index f60d35b5..2e27a97a 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -99,7 +99,7 @@ trait CallArgumentGenerator 'Named calls to Native virtual methods cannot skip an earlier optional parameter', ); } - if ($argInfo->default === '') { + if (!$argInfo->hasDefaultValue()) { $errorNode = null; foreach ($callArgs as $a) { if ($a instanceof Node\Arg && $a->name) { diff --git a/src/Generator/DefaultArgumentGenerator.php b/src/Generator/DefaultArgumentGenerator.php index ef41305a..2242f3b7 100644 --- a/src/Generator/DefaultArgumentGenerator.php +++ b/src/Generator/DefaultArgumentGenerator.php @@ -127,6 +127,6 @@ trait DefaultArgumentGenerator return true; } - return $argInfo->default !== ''; + return $argInfo->hasDefaultValue(); } } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index a663b15e..1926cd62 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -303,6 +303,11 @@ class Preprocessor extends CompilerBase $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); + // Keep the resolved declaration AST until convert. Defaults and + // constants are validated here, but their C++ expressions are not + // generated until the complete symbol table is available. + $this->preparedFileAsts[$this->file] = $stmts; + $this->declarationExpressionsFinalized = false; // 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. @@ -350,6 +355,218 @@ class Preprocessor extends CompilerBase } } + /** + * Lower declaration-only constant expressions after every symbol is known. + * + * @param list $files + */ + public function finalizeDeclarationExpressions(array $files): void + { + $this->assertCompilerPhase(self::PHASE_CONVERT, 'declaration expression finalization'); + if ($this->declarationExpressionsFinalized) { + return; + } + foreach ($files as $file) { + $path = realpath($file); + if ($path === false || !isset($this->preparedFileAsts[$path])) { + continue; + } + $this->loadFile($path); + $this->resetFile(); + $this->resetFunction(); + $this->resetMethod(); + $this->resetClass(); + $this->resetNamespace(); + $this->finalizeDeclarationStatementList($this->preparedFileAsts[$path]); + } + $this->declarationExpressionsFinalized = true; + } + + /** @param array $statements */ + private function finalizeDeclarationStatementList(array $statements): void + { + foreach ($statements as $statement) { + if ($statement instanceof Node\Stmt\Namespace_) { + $this->resetClass(); + $this->resetMethod(); + $this->resetFunction(); + $this->resetNamespace(); + $this->namespace = $statement->name ? $this->parseIdentifier($statement->name) : ''; + $this->finalizeDeclarationStatementList($statement->stmts); + continue; + } + if ($statement instanceof Node\Stmt\Use_) { + $this->parseUse($statement); + continue; + } + if ($statement instanceof Node\Stmt\GroupUse) { + $this->parseGroupUse($statement); + continue; + } + if ($statement instanceof Node\Stmt\Class_ + || $statement instanceof Node\Stmt\Trait_ + || $statement instanceof Node\Stmt\Enum_ + ) { + $this->finalizeClassDeclarationExpressions($statement); + continue; + } + if ($statement instanceof Node\Stmt\Interface_) { + $this->finalizeInterfaceDeclarationExpressions($statement); + continue; + } + if ($statement instanceof Node\Stmt\Function_) { + $this->resetClass(); + $this->resetMethod(); + $this->finalizePreparedFunctionDefaults( + $statement, + $this->getFunction($this->getFunctionName($statement)), + ); + continue; + } + if ($statement instanceof Node\Stmt\Const_) { + $this->finalizeGlobalConstantExpressions($statement); + } + } + } + + private function finalizeClassDeclarationExpressions( + Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class, + ): void { + $this->resetClass(); + $this->class = $this->parseIdentifier($class->name); + $this->classDef = $this->getClass($this->getFullClassName()); + foreach ($class->stmts as $statement) { + if ($statement instanceof Node\Stmt\ClassConst) { + foreach ($statement->consts as $constant) { + $name = $this->parseIdentifier($constant->name); + $this->finalizePreparedConstant( + $this->classDef->getConstant($name), + $constant->value, + ); + } + continue; + } + if ($statement instanceof Node\Stmt\Property) { + foreach ($statement->props as $property) { + $name = $this->parseIdentifier($property->name); + if ($property->default !== null && $this->classDef->hasProperty($name)) { + $this->finalizePreparedProperty( + $this->classDef->getProperty($name), + $property->default, + ); + } + } + continue; + } + if (!$statement instanceof Node\Stmt\ClassMethod) { + continue; + } + $name = $this->getMethodName($statement); + $this->resetMethod(); + $this->method = $name; + $this->methodDef = $this->classDef->hasMethod($name) + ? $this->classDef->getMethod($name) + : ($this->classDef->hasAbstractMethod($name) + ? $this->classDef->getAbstractMethod($name) + : null); + if ($this->methodDef !== null && $this->methodDef->functionDef !== null) { + $this->finalizePreparedFunctionDefaults($statement, $this->methodDef->functionDef); + } + } + } + + private function finalizeInterfaceDeclarationExpressions(Node\Stmt\Interface_ $interface): void + { + $this->resetClass(); + $this->interface = $this->parseIdentifier($interface->name); + $this->interfaceDef = $this->getInterface($this->getFullClassLikeName()); + foreach ($interface->stmts as $statement) { + if ($statement instanceof Node\Stmt\ClassConst) { + foreach ($statement->consts as $constant) { + $name = $this->parseIdentifier($constant->name); + $this->finalizePreparedConstant( + $this->interfaceDef->constants[$name], + $constant->value, + ); + } + continue; + } + if (!$statement instanceof Node\Stmt\ClassMethod) { + continue; + } + $name = $this->getMethodName($statement); + $this->resetMethod(); + $this->method = $name; + $this->methodDef = $this->interfaceDef->methods[strtolower($name)] ?? null; + if ($this->methodDef !== null && $this->methodDef->functionDef !== null) { + $this->finalizePreparedFunctionDefaults($statement, $this->methodDef->functionDef); + } + } + $this->interface = ''; + $this->interfaceDef = null; + } + + private function finalizePreparedFunctionDefaults( + Node\Stmt\Function_|Node\Stmt\ClassMethod $function, + FunctionDef $functionDef, + ): void { + $this->resetFunction(); + $this->function = $this->parseIdentifier($function->name); + $this->functionDef = $functionDef; + foreach ($function->params as $index => $parameter) { + if ($parameter->default === null || !isset($functionDef->argInfoList[$index])) { + continue; + } + $argument = $functionDef->argInfoList[$index]; + $argument->default = ''; + $argument->arrayInitPlan = null; + $this->lowerArgumentDefault($parameter, $argument); + } + } + + private function finalizePreparedProperty(PropertyDef $property, Node\Expr $expression): void + { + $this->resetFunction(); + $property->arrayInitPlan = null; + if ($expression instanceof Node\Expr\Array_) { + $property->arrayInitPlan = $this->buildLiteralArrayInitPlan($expression); + $property->default = $property->arrayInitPlan->expr; + } else { + $property->default = $this->parseIdentifier($expression); + } + } + + private function finalizePreparedConstant(ConstantDef $constant, Node\Expr $expression): void + { + $this->resetFunction(); + $constant->arrayExpr = ''; + $constant->value = $this->parseIdentifier($expression); + if ($this->context->beforeStmtLines) { + if ($this->context->localVars) { + $constant->arrayExpr .= $this->genScopeVarDecl(); + } + $constant->arrayExpr .= $this->parseBeforeStmtLines(); + } + $constant->codegenFinalized = true; + } + + private function finalizeGlobalConstantExpressions(Node\Stmt\Const_ $statement): void + { + foreach ($statement->consts as $constant) { + $name = $this->parseIdentifier($constant->name); + if ($this->namespace !== '') { + $name = $this->namespace . '\\' . $name; + } + $key = $this->escapeConstVar($name); + if (!isset($this->constants[$key])) { + continue; + } + $this->resetFunction(); + $this->constants[$key]->value = $this->parseIdentifier($constant->value); + $this->constants[$key]->codegenFinalized = true; + } + } + /** @param array $stmts */ private function hasLibraryImportAnnotation(array $stmts): bool { @@ -679,28 +896,10 @@ class Preprocessor extends CompilerBase $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; + $argInfo->defaultExpr = $param->default; + $argInfo->defaultValue = $param->default; + if ($this->compilerPhase === self::PHASE_CONVERT) { + $this->lowerArgumentDefault($param, $argInfo); } } elseif ($param->variadic) { // 变长参数可以视为空数组默认值 @@ -713,6 +912,33 @@ class Preprocessor extends CompilerBase $functionDef->argCountRequired = $lastRequiredIndex + 1; } + protected function lowerArgumentDefault(Node\Param $param, ArgInfo $argInfo): void + { + if ($param->default === null) { + return; + } + $arrayInitPlan = $param->default instanceof Node\Expr\Array_ + ? $this->withoutLocalClassEntryHoisting( + fn (): ArrayInitPlan => $this->buildLiteralArrayInitPlan($param->default), + ) + : null; + $argInfo->arrayInitPlan = $arrayInitPlan; + if ($param->byRef) { + if ($this->isEmptyArray($param->default)) { + $argInfo->default = 'php::getEmptyArrayRef()'; + return; + } + if ($this->isNull($param->default)) { + $argInfo->default = 'nullptr'; + return; + } + $value = $arrayInitPlan?->expr ?? $this->parseParamDefaultValue($param->default); + $argInfo->default = 'php::newReference(' . $value . ')'; + return; + } + $argInfo->default = $arrayInitPlan?->expr ?? $this->parseParamDefaultValue($param->default); + } + protected function getFunctionDisplayName(FunctionDef $functionDef): string { if ($this->class) { @@ -1398,13 +1624,16 @@ class Preprocessor extends CompilerBase 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); + $constValue = $this->compilerPhase === self::PHASE_CONVERT + ? $this->parseIdentifier($const->value) + : ''; $constInfo = new ConstantDef($constName, $flags, $type, $constValue); $constInfo->valueExpr = $const->value; $constInfo->declaredType = $declaredType; + $constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT; - if ($this->context->beforeStmtLines) { + if ($constInfo->codegenFinalized && $this->context->beforeStmtLines) { $arrayExpr = ''; if ($this->context->localVars) { $arrayExpr .= $this->genScopeVarDecl(); @@ -1455,8 +1684,10 @@ class Preprocessor extends CompilerBase if ($defaultNode !== null) { $this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode); if ($defaultNode instanceof Node\Expr\Array_) { - $arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode); - $default = $arrayInitPlan->expr; + if ($this->compilerPhase === self::PHASE_CONVERT) { + $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, @@ -1465,7 +1696,7 @@ class Preprocessor extends CompilerBase if ($type !== Type::VAR) { $type = Type::ARRAY; } - } else { + } elseif ($this->compilerPhase === self::PHASE_CONVERT) { $default = $this->parseIdentifier($defaultNode); } } @@ -1489,6 +1720,7 @@ class Preprocessor extends CompilerBase } $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->class = $class; + $propDef->defaultExpr = $defaultNode; $propDef->arrayInitPlan = $arrayInitPlan; $propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode); $propDef->promoted = $promoted; diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index cc2949bb..ea3aa2e6 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -17,7 +17,9 @@ trait DeclarationSymbolTrait { foreach ($v2->consts as $const) { $name = $this->parseIdentifier($const->name); - $value = $this->parseIdentifier($const->value); + $value = $this->compilerPhase === self::PHASE_CONVERT + ? $this->parseIdentifier($const->value) + : ''; if ($this->namespace) { $name = $this->namespace . '\\' . $name; } @@ -30,7 +32,10 @@ trait DeclarationSymbolTrait $constInfo = new \stdClass(); $constInfo->value = $value; $constInfo->valueExpr = $valueExpr; - $constInfo->type = $this->detectStrValueType($value); + $constInfo->type = $this->compilerPhase === self::PHASE_CONVERT + ? $this->detectStrValueType($value) + : Type::VAR; + $constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT; $constInfo->namespace = $this->namespace; $constInfo->name = $name; $this->constants[$this->escapeConstVar($name)] = $constInfo; diff --git a/src/Translator.php b/src/Translator.php index dcfe3a19..8a7b170b 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -537,6 +537,9 @@ class Translator extends Preprocessor { $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); try { + if (!$this->declarationExpressionsFinalized) { + $this->finalizeDeclarationExpressions(array_keys($this->preparedFileAsts)); + } $file = realpath($file); $phpCode = $this->loadFile($file); $this->localHeaders = []; @@ -808,6 +811,16 @@ class Translator extends Preprocessor } public function genExtension(): string + { + $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); + try { + return $this->doGenExtension(); + } finally { + $this->restoreCompilerPhase($previousPhase); + } + } + + private function doGenExtension(): string { if ($this->isBuildModeBin()) { if (!$this->hasFunction(self::ENTRY_FUNCTION)) { @@ -1860,7 +1873,7 @@ CODE; . ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex); } else { $arg = $this->genArgumentDeclaration($argInfo); - if ($argInfo->default !== '' && !$this->isConstructorNativeFunction($func)) { + if ($argInfo->hasDefaultValue() && !$this->isConstructorNativeFunction($func)) { $arg .= ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex); } } @@ -3722,7 +3735,7 @@ CODE; $cppCode .= $this->getIndent() . '}' . PHP_EOL; $cppCode .= $this->genExtraNamedVariadicArgs($var); } else { - if ($argInfo->default !== '') { + if ($argInfo->hasDefaultValue()) { $nativeName = str_starts_with($fn, self::PREFIX) ? substr($fn, strlen(self::PREFIX)) : $fn;