fix(compiler): defer cache IDs to convert phase

debug/ci-class-id-cache
韩天峰 8 hours ago
parent 6693d95cd6
commit 74274f8328
  1. 74
      phpunit/src/CompilerBaseApiTest.php
  2. 11
      phpunit/src/PreprocessorTest.php
  3. 10
      src/Build/SourcePipelineTrait.php
  4. 2
      src/Build/WasmInterfaceGenerator.php
  5. 29
      src/CompilerBase.php
  6. 7
      src/Entity/ArgInfo.php
  7. 2
      src/Entity/ConstantDef.php
  8. 2
      src/Entity/PropertyDef.php
  9. 2
      src/Generator/CallArgumentGenerator.php
  10. 2
      src/Generator/DefaultArgumentGenerator.php
  11. 280
      src/Preprocessor.php
  12. 9
      src/Resolver/DeclarationSymbolTrait.php
  13. 17
      src/Translator.php

@ -80,6 +80,7 @@ class CompilerBaseApiTest extends TestCase
public function testMethodCacheKeepsPreviouslyAssignedClassLifetime(): void public function testMethodCacheKeepsPreviouslyAssignedClassLifetime(): void
{ {
$this->setPropertyValue('compilerPhase', 'convert');
// The two maps have independent ID spaces. This reproduces the tpc // The two maps have independent ID spaces. This reproduces the tpc
// bootstrap ordering where both classes occupied slot zero. // bootstrap ordering where both classes occupied slot zero.
$this->setPropertyValue('classMap', ['LateKnownClass' => 0]); $this->setPropertyValue('classMap', ['LateKnownClass' => 0]);
@ -100,6 +101,7 @@ class CompilerBaseApiTest extends TestCase
public function testMethodCacheRejectsMismatchedLifetimeDomains(): void public function testMethodCacheRejectsMismatchedLifetimeDomains(): void
{ {
$this->setPropertyValue('compilerPhase', 'convert');
$this->setPropertyValue('classMap', ['LateKnownClass' => 0]); $this->setPropertyValue('classMap', ['LateKnownClass' => 0]);
$this->setPropertyValue('classIndex', 1); $this->setPropertyValue('classIndex', 1);
$this->setPropertyValue('persistentFuncMap', ['LateKnownClass::run' => 0]); $this->setPropertyValue('persistentFuncMap', ['LateKnownClass::run' => 0]);
@ -112,6 +114,78 @@ class CompilerBaseApiTest extends TestCase
$this->invokeMethod('getMethodPtr', 'LateKnownClass', 'run'); $this->invokeMethod('getMethodPtr', 'LateKnownClass', 'run');
} }
public function testPrepareStoresDeclarationAstsWithoutAllocatingCacheIds(): void
{
$consumer = $this->testDir . '/consumer.php';
$declaration = $this->testDir . '/late.php';
file_put_contents($consumer, <<<'PHP'
<?php
namespace CachePhase;
final class Defaults
{
public const string VALUE = LateClass::VALUE;
public string $property = LateClass::VALUE;
public function read(string $value = LateClass::VALUE): string
{
return $value;
}
}
PHP);
file_put_contents($declaration, <<<'PHP'
<?php
namespace CachePhase;
final class LateClass
{
public const string VALUE = 'ready';
}
PHP);
// Deliberately prepare the consumer first. This was the bootstrap
// ordering that used to assign a request ID before LateClass became
// visible, then assign persistent IDs to later method lookups.
$this->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 private function fixturePath(string $file): string
{ {
return __DIR__ . '/../code/compiler_api/' . $file; return __DIR__ . '/../code/compiler_api/' . $file;

@ -287,7 +287,7 @@ class PreprocessorTest extends TestCase
$this->assertNotEmpty($functionDef->returnTypeCheck); $this->assertNotEmpty($functionDef->returnTypeCheck);
} }
public function testPrepareFileParsesInterfaceArrayConstantInitExpr(): void public function testInterfaceArrayConstantIsLoweredOnlyDuringConvert(): void
{ {
$file = __DIR__ . '/../code/interface_array_constant.php'; $file = __DIR__ . '/../code/interface_array_constant.php';
@ -298,6 +298,15 @@ class PreprocessorTest extends TestCase
$constant = $interfaces['interfacearrayconstant']->constants['ITEMS']; $constant = $interfaces['interfacearrayconstant']->constants['ITEMS'];
$this->assertSame('php::Array', $constant->type); $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); $this->assertStringContainsString('php::Array', $constant->value);
} }

@ -249,6 +249,13 @@ trait SourcePipelineTrait
public function convert(array $files): array public function convert(array $files): array
{ {
$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 = []; $sourceFiles = [];
$validSourceCount = 0; $validSourceCount = 0;
// 生成 C++ 文件 // 生成 C++ 文件
@ -293,5 +300,8 @@ trait SourcePipelineTrait
$sourceFiles[] = $this->genExtension(); $sourceFiles[] = $this->genExtension();
return $sourceFiles; return $sourceFiles;
} finally {
$this->restoreCompilerPhase($previousPhase);
}
} }
} }

@ -55,7 +55,7 @@ final class WasmInterfaceGenerator
$parameters = []; $parameters = [];
foreach ($function->argInfoList as $argument) { foreach ($function->argInfoList as $argument) {
if ($argument->byRef || $argument->variadic || $argument->default !== '') { if ($argument->byRef || $argument->variadic || $argument->hasDefaultValue()) {
throw new RuntimeException( throw new RuntimeException(
"WasmExport parameter \${$argument->phpName} of {$displayName}() cannot be by-reference, variadic, or optional" "WasmExport parameter \${$argument->phpName} of {$displayName}() cannot be by-reference, variadic, or optional"
); );

@ -316,6 +316,9 @@ class CompilerBase implements PropertyAccessContext
*/ */
protected array $persistentPropMap = []; protected array $persistentPropMap = [];
protected int $persistentPropIndex = 0; protected int $persistentPropIndex = 0;
/** @var array<string, array<Node\Stmt>> Prepared declaration ASTs keyed by real path. */
protected array $preparedFileAsts = [];
protected bool $declarationExpressionsFinalized = false;
protected const array PHP_RUNTIME_TYPE_MAP = [ protected const array PHP_RUNTIME_TYPE_MAP = [
'integer' => Type::INT, 'integer' => Type::INT,
'double' => Type::FLOAT, 'double' => Type::FLOAT,
@ -1277,19 +1280,6 @@ class CompilerBase implements PropertyAccessContext
{ {
if (str_contains($funcName, '::')) { if (str_contains($funcName, '::')) {
[$class] = explode('::', $funcName, 2); [$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); return $this->isProcessStableClass($class);
} }
if ($this->hasFunction($funcName)) { if ($this->hasFunction($funcName)) {
@ -1301,6 +1291,7 @@ class CompilerBase implements PropertyAccessContext
protected function getClassId(string $className): int protected function getClassId(string $className): int
{ {
$this->assertCompilerPhase(self::PHASE_CONVERT, 'class cache ID allocation');
if (isset($this->classMap[$className])) { if (isset($this->classMap[$className])) {
return $this->classMap[$className]; return $this->classMap[$className];
} }
@ -1319,6 +1310,7 @@ class CompilerBase implements PropertyAccessContext
protected function getFuncId(string $funcName): int protected function getFuncId(string $funcName): int
{ {
$this->assertCompilerPhase(self::PHASE_CONVERT, 'function cache ID allocation');
if (isset($this->funcMap[$funcName])) { if (isset($this->funcMap[$funcName])) {
return $this->funcMap[$funcName]; return $this->funcMap[$funcName];
} }
@ -1346,6 +1338,7 @@ class CompilerBase implements PropertyAccessContext
*/ */
protected function getPropertyId(string $className, string $propName): int protected function getPropertyId(string $className, string $propName): int
{ {
$this->assertCompilerPhase(self::PHASE_CONVERT, 'property cache ID allocation');
$key = $className . '::' . $propName; $key = $className . '::' . $propName;
if (isset($this->persistentPropMap[$key])) { if (isset($this->persistentPropMap[$key])) {
return $this->persistentPropMap[$key]; return $this->persistentPropMap[$key];
@ -2869,6 +2862,9 @@ class CompilerBase implements PropertyAccessContext
if ($interfaceConstDef->type === Type::ARRAY) { if ($interfaceConstDef->type === Type::ARRAY) {
return self::PREFIX . $this->getNativeName($interfaceConstDef->name, $interfaceDef->namespace, $interfaceDef->name); return self::PREFIX . $this->getNativeName($interfaceConstDef->name, $interfaceDef->namespace, $interfaceDef->name);
} }
if (!$interfaceConstDef->codegenFinalized) {
return false;
}
$expr->setAttribute('nativeConst', $interfaceConstDef); $expr->setAttribute('nativeConst', $interfaceConstDef);
return $interfaceConstDef->value; return $interfaceConstDef->value;
} }
@ -2887,6 +2883,13 @@ class CompilerBase implements PropertyAccessContext
if ($constDef->type === Type::ARRAY) { if ($constDef->type === Type::ARRAY) {
return self::PREFIX . $this->getNativeName($constDef->name, $classDef->namespace, $classDef->name); return self::PREFIX . $this->getNativeName($constDef->name, $classDef->namespace, $classDef->name);
} else { } 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); $expr->setAttribute('nativeConst', $constDef);
return $constDef->value; return $constDef->value;
} }

@ -19,6 +19,8 @@ class ArgInfo
public string $type; public string $type;
public string $default = ''; public string $default = '';
public ?ArrayInitPlan $arrayInitPlan = null; public ?ArrayInitPlan $arrayInitPlan = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?Expr $defaultExpr = null;
public ?Expr $defaultValue = null; public ?Expr $defaultValue = null;
public string $class = ''; public string $class = '';
@ -56,4 +58,9 @@ class ArgInfo
/** Original union/nullable AST node. Only set when typeCheck is non-null. */ /** Original union/nullable AST node. Only set when typeCheck is non-null. */
public ?NodeAbstract $typeNode = null; public ?NodeAbstract $typeNode = null;
public function hasDefaultValue(): bool
{
return $this->defaultExpr !== null;
}
} }

@ -19,6 +19,8 @@ class ConstantDef
public string $arrayExpr = ''; public string $arrayExpr = '';
public string $class = ''; public string $class = '';
public ?NodeAbstract $valueExpr = null; 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. */ /** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */
public ?string $declaredType = null; public ?string $declaredType = null;

@ -19,6 +19,8 @@ class PropertyDef
public int $flags; public int $flags;
public ?string $default = null; public ?string $default = null;
public ?ArrayInitPlan $arrayInitPlan = 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 ?ArrayDefinition $arrayDef = null;
public bool $nullable = false; public bool $nullable = false;
/** The declared type is TypePHP's unconstrained, reference-capable `any` type. */ /** The declared type is TypePHP's unconstrained, reference-capable `any` type. */

@ -99,7 +99,7 @@ trait CallArgumentGenerator
'Named calls to Native virtual methods cannot skip an earlier optional parameter', 'Named calls to Native virtual methods cannot skip an earlier optional parameter',
); );
} }
if ($argInfo->default === '') { if (!$argInfo->hasDefaultValue()) {
$errorNode = null; $errorNode = null;
foreach ($callArgs as $a) { foreach ($callArgs as $a) {
if ($a instanceof Node\Arg && $a->name) { if ($a instanceof Node\Arg && $a->name) {

@ -127,6 +127,6 @@ trait DefaultArgumentGenerator
return true; return true;
} }
return $argInfo->default !== ''; return $argInfo->hasDefaultValue();
} }
} }

@ -303,6 +303,11 @@ class Preprocessor extends CompilerBase
$traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion));
$traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file));
$stmts = $traverser->traverse($ast); $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() // CompilerTest and embedding users may invoke prepareFile()
// directly instead of the project pipeline. Preserve same-file // directly instead of the project pipeline. Preserve same-file
// forward Native references for that public entry path as well. // 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<string> $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<Node\Stmt> $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<Node> $stmts */ /** @param array<Node> $stmts */
private function hasLibraryImportAnnotation(array $stmts): bool private function hasLibraryImportAnnotation(array $stmts): bool
{ {
@ -679,28 +896,10 @@ class Preprocessor extends CompilerBase
$list[] = $this->genArgumentDeclaration($argInfo); $list[] = $this->genArgumentDeclaration($argInfo);
} }
if ($param->default) { if ($param->default) {
$arrayInitPlan = $param->default instanceof Node\Expr\Array_ $argInfo->defaultExpr = $param->default;
? $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->defaultValue = $param->default;
if ($this->compilerPhase === self::PHASE_CONVERT) {
$this->lowerArgumentDefault($param, $argInfo);
} }
} elseif ($param->variadic) { } elseif ($param->variadic) {
// 变长参数可以视为空数组默认值 // 变长参数可以视为空数组默认值
@ -713,6 +912,33 @@ class Preprocessor extends CompilerBase
$functionDef->argCountRequired = $lastRequiredIndex + 1; $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 protected function getFunctionDisplayName(FunctionDef $functionDef): string
{ {
if ($this->class) { 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 private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = '', ?string $declaredType = null): ConstantDef
{ {
$constName = $this->parseIdentifier($const->name); $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 = new ConstantDef($constName, $flags, $type, $constValue);
$constInfo->valueExpr = $const->value; $constInfo->valueExpr = $const->value;
$constInfo->declaredType = $declaredType; $constInfo->declaredType = $declaredType;
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT;
if ($this->context->beforeStmtLines) { if ($constInfo->codegenFinalized && $this->context->beforeStmtLines) {
$arrayExpr = ''; $arrayExpr = '';
if ($this->context->localVars) { if ($this->context->localVars) {
$arrayExpr .= $this->genScopeVarDecl(); $arrayExpr .= $this->genScopeVarDecl();
@ -1455,8 +1684,10 @@ class Preprocessor extends CompilerBase
if ($defaultNode !== null) { if ($defaultNode !== null) {
$this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode); $this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode);
if ($defaultNode instanceof Node\Expr\Array_) { if ($defaultNode instanceof Node\Expr\Array_) {
if ($this->compilerPhase === self::PHASE_CONVERT) {
$arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode); $arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode);
$default = $arrayInitPlan->expr; $default = $arrayInitPlan->expr;
}
// Only narrow the property type to `array` when the declared type // Only narrow the property type to `array` when the declared type
// cannot already hold an array. `mixed`/`iterable`/union/nullable // cannot already hold an array. `mixed`/`iterable`/union/nullable
// types are represented as php::Var and can legally store an array, // types are represented as php::Var and can legally store an array,
@ -1465,7 +1696,7 @@ class Preprocessor extends CompilerBase
if ($type !== Type::VAR) { if ($type !== Type::VAR) {
$type = Type::ARRAY; $type = Type::ARRAY;
} }
} else { } elseif ($this->compilerPhase === self::PHASE_CONVERT) {
$default = $this->parseIdentifier($defaultNode); $default = $this->parseIdentifier($defaultNode);
} }
} }
@ -1489,6 +1720,7 @@ class Preprocessor extends CompilerBase
} }
$propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY);
$propDef->class = $class; $propDef->class = $class;
$propDef->defaultExpr = $defaultNode;
$propDef->arrayInitPlan = $arrayInitPlan; $propDef->arrayInitPlan = $arrayInitPlan;
$propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode); $propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode);
$propDef->promoted = $promoted; $propDef->promoted = $promoted;

@ -17,7 +17,9 @@ trait DeclarationSymbolTrait
{ {
foreach ($v2->consts as $const) { foreach ($v2->consts as $const) {
$name = $this->parseIdentifier($const->name); $name = $this->parseIdentifier($const->name);
$value = $this->parseIdentifier($const->value); $value = $this->compilerPhase === self::PHASE_CONVERT
? $this->parseIdentifier($const->value)
: '';
if ($this->namespace) { if ($this->namespace) {
$name = $this->namespace . '\\' . $name; $name = $this->namespace . '\\' . $name;
} }
@ -30,7 +32,10 @@ trait DeclarationSymbolTrait
$constInfo = new \stdClass(); $constInfo = new \stdClass();
$constInfo->value = $value; $constInfo->value = $value;
$constInfo->valueExpr = $valueExpr; $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->namespace = $this->namespace;
$constInfo->name = $name; $constInfo->name = $name;
$this->constants[$this->escapeConstVar($name)] = $constInfo; $this->constants[$this->escapeConstVar($name)] = $constInfo;

@ -537,6 +537,9 @@ class Translator extends Preprocessor
{ {
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try { try {
if (!$this->declarationExpressionsFinalized) {
$this->finalizeDeclarationExpressions(array_keys($this->preparedFileAsts));
}
$file = realpath($file); $file = realpath($file);
$phpCode = $this->loadFile($file); $phpCode = $this->loadFile($file);
$this->localHeaders = []; $this->localHeaders = [];
@ -808,6 +811,16 @@ class Translator extends Preprocessor
} }
public function genExtension(): string 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->isBuildModeBin()) {
if (!$this->hasFunction(self::ENTRY_FUNCTION)) { if (!$this->hasFunction(self::ENTRY_FUNCTION)) {
@ -1860,7 +1873,7 @@ CODE;
. ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex); . ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
} else { } else {
$arg = $this->genArgumentDeclaration($argInfo); $arg = $this->genArgumentDeclaration($argInfo);
if ($argInfo->default !== '' && !$this->isConstructorNativeFunction($func)) { if ($argInfo->hasDefaultValue() && !$this->isConstructorNativeFunction($func)) {
$arg .= ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex); $arg .= ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
} }
} }
@ -3722,7 +3735,7 @@ CODE;
$cppCode .= $this->getIndent() . '}' . PHP_EOL; $cppCode .= $this->getIndent() . '}' . PHP_EOL;
$cppCode .= $this->genExtraNamedVariadicArgs($var); $cppCode .= $this->genExtraNamedVariadicArgs($var);
} else { } else {
if ($argInfo->default !== '') { if ($argInfo->hasDefaultValue()) {
$nativeName = str_starts_with($fn, self::PREFIX) $nativeName = str_starts_with($fn, self::PREFIX)
? substr($fn, strlen(self::PREFIX)) ? substr($fn, strlen(self::PREFIX))
: $fn; : $fn;

Loading…
Cancel
Save