diff --git a/phpunit/src/Build/IncrementalDeclarationTest.php b/phpunit/src/Build/IncrementalDeclarationTest.php index f22d03a3..301f7b9a 100644 --- a/phpunit/src/Build/IncrementalDeclarationTest.php +++ b/phpunit/src/Build/IncrementalDeclarationTest.php @@ -137,6 +137,31 @@ PHP); ); } + public function testGlobalArrayConstantInitializersSurviveColdAndWarmConversion(): void + { + file_put_contents($this->provider, <<<'PHP' + ['left' => LIMIT], 'tail' => [2, [3]]]; +function answer(): int { return FIRST[0][0][0] + SECOND[0]['left']; } +PHP); + $first = $this->convertProject(); + $extension = $this->buildDirectory . '/extension-incremental.cc'; + $coldCode = file_get_contents($extension); + self::assertStringContainsString('php::Array tmp_var_', $coldCode); + $coldConstants = $this->property($first, 'constants'); + self::assertSame(\TypePhp\Type::BOOL, $coldConstants['_const_var_Incremental__ENABLED']->type); + $second = $this->convertProject(); + self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider)); + self::assertSame($coldCode, file_get_contents($extension)); + $warmConstants = $this->property($second, 'constants'); + self::assertSame($coldConstants['_const_var_Incremental__LIMIT']->type, $warmConstants['_const_var_Incremental__LIMIT']->type); + self::assertSame(\TypePhp\Type::BOOL, $warmConstants['_const_var_Incremental__ENABLED']->type); + } + public function testGeneratorFingerprintChangeKeepsIdenticalCppTimestamps(): void { $first = $this->convertProject(); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index e987acb5..75e2e67e 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -917,9 +917,7 @@ class Preprocessor extends CompilerBase if (!isset($this->constants[$key])) { continue; } - $this->resetFunction(); - $this->constants[$key]->value = $this->parseIdentifier($constant->value); - $this->constants[$key]->codegenFinalized = true; + $this->finalizeGlobalConstantValue($this->constants[$key], $constant->value); } } diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index 804b9cc1..5d8fa52d 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -17,16 +17,36 @@ trait DeclarationSymbolTrait { foreach ($v2->consts as $const) { $name = $this->parseIdentifier($const->name); - $value = $this->compilerPhase === self::PHASE_CONVERT - ? $this->parseIdentifier($const->value) - : ''; if ($this->namespace) { $name = $this->namespace . '\\' . $name; } - $this->addConstant($name, $value, $const->value); + // Declaration finalization owns whole-program initialization. + // Re-parsing a dirty file must not replace that metadata with + // temporaries allocated in the function-body context. + if ($this->compilerPhase === self::PHASE_CONVERT + && ($this->constants[$this->escapeConstVar($name)]->codegenFinalized ?? false)) { + continue; + } + $this->addConstant($name, '', $const->value); + if ($this->compilerPhase === self::PHASE_CONVERT) { + $this->finalizeGlobalConstantValue($this->constants[$this->escapeConstVar($name)], $const->value); + } } } + protected function finalizeGlobalConstantValue(\stdClass $constant, Node\Expr $expression): void + { + $this->resetFunction(); + $constant->value = $this->parseIdentifier($expression); + $constant->initializationCode = $this->context->localVars ? $this->genScopeVarDecl() : ''; + $constant->initializationCode .= $this->parseBeforeStmtLines(); + $constant->afterInitializationCode = $this->parseAfterStmtLines(); + // Clean files are not re-parsed by convert(): finalize their scalar + // storage types here too, keeping cold/warm declarations identical. + $constant->type = $this->detectStrValueType($constant->value); + $constant->codegenFinalized = true; + } + protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void { $constInfo = new \stdClass(); @@ -39,6 +59,8 @@ trait DeclarationSymbolTrait $constInfo->namespace = $this->namespace; $constInfo->name = $name; $constInfo->sourceFile = $this->file; + $constInfo->initializationCode = ''; + $constInfo->afterInitializationCode = ''; $this->constants[$this->escapeConstVar($name)] = $constInfo; $this->symbolDeclInFile[$this->getConstantDependencySymbol($name)] = $this->file; } diff --git a/src/Translator.php b/src/Translator.php index 7554c335..bdd14331 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1571,8 +1571,20 @@ CODE; $code .= $this->genCompiledGeneratorFingerprintRegistration(); $code .= '// register constants' . PHP_EOL; foreach ($this->constants as $name => $const) { + $initializationCode = $const->initializationCode ?? ''; + $afterInitializationCode = $const->afterInitializationCode ?? ''; + $scopedInitialization = $initializationCode !== '' || $afterInitializationCode !== ''; + if ($scopedInitialization) { + // Each constant has its own temporary namespace. Declaration + // lowering resets temporary IDs between independent values. + $code .= "do {\n" . $initializationCode; + } $code .= "{$name} = {$const->value};\n"; + $code .= $afterInitializationCode; $code .= 'php::fn::define(' . $this->genCharPtr($const->name, true) . ', ' . $name . ');' . PHP_EOL; + if ($scopedInitialization) { + $code .= "} while (0);\n"; + } } $code .= '// global vars ' . PHP_EOL; foreach ($extensionGlobalVars as $name => $type) { diff --git a/tests/compiler/stdlib/global-nested-array-constants.phpt b/tests/compiler/stdlib/global-nested-array-constants.phpt new file mode 100644 index 00000000..da54ca4e --- /dev/null +++ b/tests/compiler/stdlib/global-nested-array-constants.phpt @@ -0,0 +1,31 @@ +--TEST-- +Global nested array constants keep initialization temporaries in separate scopes +--FILE-- + ['left' => LIMIT], 'tail' => [2, [3]]]; +const THIRD = [FIRST, SECOND]; +} + +namespace { +function main(): void +{ + var_dump(GlobalArrayConstants\FIRST[0][0][0]); + var_dump(GlobalArrayConstants\FIRST[1][0][1]); + var_dump(GlobalArrayConstants\SECOND[0]['left']); + var_dump(GlobalArrayConstants\SECOND['tail'][1][0]); + var_dump(GlobalArrayConstants\THIRD[0] === GlobalArrayConstants\FIRST); + var_dump(GlobalArrayConstants\THIRD[1] === GlobalArrayConstants\SECOND); +} +} +?> +--EXPECT-- +int(42) +int(3) +int(42) +int(3) +bool(true) +bool(true)