From 5d4f31a9ab2faf92b5899f9322c14f456940bde5 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 15 Sep 2026 14:03:33 +0800 Subject: [PATCH] refactor(build): split class array constant lifecycle into separate translation units - Move large array-valued class constant initialization from module_init() to isolated functions - Generate independent cacheable translation units for request-lifecycle helpers - Add static zend_class_entry pointers with null initialization for module entry unit - Preserve arginfo-backed class registration in extension-*.cc while splitting array constants - Implement proper cleanup of split lifecycle sources during incremental builds - Cache PHP SDK discovery results to avoid repeated php-config invocations - Add comprehensive tests for class constant lifecycle splitting behavior - Verify static class entry declarations do not expose unnecessary linkable symbols --- .../src/ClassConstantLifecycleSplitTest.php | 155 ++++++++++ .../InternalParentExtensionCodegenTest.php | 8 + phpunit/src/Platform/PlatformTest.php | 23 +- src/Build/SourcePipelineTrait.php | 7 + src/Platform/UnixPlatform.php | 48 ++- src/Translator.php | 277 +++++++++++++----- 6 files changed, 446 insertions(+), 72 deletions(-) create mode 100644 phpunit/src/ClassConstantLifecycleSplitTest.php diff --git a/phpunit/src/ClassConstantLifecycleSplitTest.php b/phpunit/src/ClassConstantLifecycleSplitTest.php new file mode 100644 index 00000000..748c08fd --- /dev/null +++ b/phpunit/src/ClassConstantLifecycleSplitTest.php @@ -0,0 +1,155 @@ +projectDir = sys_get_temp_dir() . '/typephp_class_constant_split_' + . bin2hex(random_bytes(6)); + mkdir($this->projectDir, 0777, true); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->projectDir); + parent::tearDown(); + } + + public function testArrayConstantLifecycleIsSplitWithoutMovingArginfoRegistration(): void + { + $parent = $this->writeSource('parent.php', <<<'PHP' +writeSource('child.php', <<<'PHP' +projectDir); + $translator = $compiler; + $compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $compiler->setTargetName('lifecycle_split'); + $compiler->addFiles([$parent, $child]); + foreach ([$parent, $child] as $file) { + $compiler->prepareFile($file); + } + + $sources = $compiler->convert($compiler->getSortedFiles([$parent, $child])); + $lifecycleSources = array_values(array_filter( + $sources, + static fn (string $source): bool => str_contains($source, '/init/') + && str_ends_with($source, '_class_constants.cc'), + )); + + self::assertCount(2, $lifecycleSources); + $lifecycleCode = implode(PHP_EOL, array_map('file_get_contents', $lifecycleSources)); + self::assertStringContainsString( + 'zend_never_inline void typephp_request_init_class_constant_', + $lifecycleCode, + ); + self::assertStringContainsString( + 'php::updateConstant("LifecycleParent", "ITEMS"', + $lifecycleCode, + ); + self::assertStringContainsString( + 'php::updateConstant("LifecycleChild", "ITEMS"', + $lifecycleCode, + ); + self::assertStringNotContainsString('_arginfo.h>', $lifecycleCode); + + $extension = (string) file_get_contents( + $this->projectDir . '/build/extension-lifecycle_split.cc', + ); + self::assertStringContainsString( + $compiler->getArgInfoHeaderFile($parent, true) . '>', + $extension, + ); + self::assertStringContainsString( + $compiler->getArgInfoHeaderFile($child, true) . '>', + $extension, + ); + self::assertStringContainsString( + 'php_class_entry_LifecycleParent = php_register_class_LifecycleParent();', + $extension, + ); + self::assertStringContainsString( + 'zend_never_inline void typephp_request_init_class_constant_', + $extension, + ); + + $moduleInitStart = strpos($extension, 'static void module_init()'); + $moduleCleanStart = strpos($extension, 'static void module_clean()'); + self::assertIsInt($moduleInitStart); + self::assertIsInt($moduleCleanStart); + $moduleInit = substr($extension, $moduleInitStart, $moduleCleanStart - $moduleInitStart); + self::assertStringContainsString('typephp_request_init_class_constant_', $moduleInit); + self::assertStringNotContainsString('php::updateConstant("LifecycleParent"', $moduleInit); + + // Removing the last array-valued constant must also remove generated + // lifecycle sources from the previous incremental graph. + $this->writeSource('parent.php', <<<'PHP' +projectDir); + $translator = $nextCompiler; + $nextCompiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $nextCompiler->setTargetName('lifecycle_split'); + $nextCompiler->addFiles([$parent, $child]); + foreach ([$parent, $child] as $file) { + $nextCompiler->prepareFile($file); + } + $nextSources = $nextCompiler->convert($nextCompiler->getSortedFiles([$parent, $child])); + self::assertSame( + [], + array_values(array_filter( + $nextSources, + static fn (string $source): bool => str_contains($source, '/init/') + && str_ends_with($source, '_class_constants.cc'), + )), + ); + self::assertSame([], glob($this->projectDir . '/build/init/*_class_constants.cc')); + } + + private function writeSource(string $name, string $code): string + { + $file = $this->projectDir . '/' . $name; + file_put_contents($file, $code); + return $file; + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + foreach (array_diff(scandir($directory), ['.', '..']) as $entry) { + $path = $directory . '/' . $entry; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($directory); + } +} diff --git a/phpunit/src/InternalParentExtensionCodegenTest.php b/phpunit/src/InternalParentExtensionCodegenTest.php index e1846455..ce8c1e0b 100644 --- a/phpunit/src/InternalParentExtensionCodegenTest.php +++ b/phpunit/src/InternalParentExtensionCodegenTest.php @@ -42,6 +42,14 @@ PHP); $extension = file_get_contents($compiler->genExtension()); + self::assertStringContainsString( + 'static zend_class_entry *php_class_entry_ArrayObject = nullptr;', + $extension, + ); + self::assertStringContainsString( + 'static zend_class_entry *php_class_entry_InternalParentChild = nullptr;', + $extension, + ); self::assertStringContainsString( 'php_class_entry_ArrayObject = php::getInternalClassEntrySafe("ArrayObject");', $extension, diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index baaad8ae..63f9cbb4 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -398,8 +398,9 @@ class PlatformTest extends TestCase mkdir($wrongInclude, 0755, true); $versionedConfig = $phpHome . '/bin/php-config' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + $queryLog = $root . '/queries.log'; $wrongMinor = PHP_MINOR_VERSION === 4 ? 5 : 4; - $this->writePhpConfig($versionedConfig, PHP_VERSION, $phpHome, $rightInclude); + $this->writePhpConfig($versionedConfig, PHP_VERSION, $phpHome, $rightInclude, $queryLog); $this->writePhpConfig( $phpHome . '/bin/php-config', PHP_MAJOR_VERSION . '.' . $wrongMinor . '.0', @@ -414,11 +415,26 @@ class PlatformTest extends TestCase $this->assertSame($phpHome, $platform->getPhpDir()); $this->assertSame([$rightInclude], $platform->buildPhpIncludePaths($phpHome)); + for ($i = 0; $i < 10; ++$i) { + $this->assertSame($phpHome, $platform->getPhpDir()); + $this->assertSame([$rightInclude], $platform->buildPhpIncludePaths($phpHome)); + } + $this->assertSame("--version\n--includes\n", file_get_contents($queryLog)); + + // A changed authoritative environment must not reuse the old SDK. + putenv('PHP_HOME=' . $root . '/missing'); + try { + $platform->getPhpDir(); + $this->fail('Expected changed PHP_HOME to be validated'); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('PHP_HOME is not a directory', $e->getMessage()); + } } finally { $previousPhpHome === false ? putenv('PHP_HOME') : putenv('PHP_HOME=' . $previousPhpHome); unlink($versionedConfig); + unlink($queryLog); unlink($phpHome . '/bin/php-config'); rmdir($rightInclude); rmdir($wrongInclude); @@ -455,7 +471,7 @@ class PlatformTest extends TestCase } } - private function writePhpConfig(string $path, string $version, string $prefix, string $include): void + private function writePhpConfig(string $path, string $version, string $prefix, string $include, ?string $queryLog = null): void { $script = sprintf( "#!/bin/sh\ncase \"\$1\" in\n --version) printf '%%s\\n' %s ;;\n --prefix) printf '%%s\\n' %s ;;\n --includes) printf '%%s\\n' %s ;;\nesac\n", @@ -463,6 +479,9 @@ class PlatformTest extends TestCase escapeshellarg($prefix), escapeshellarg('-I' . $include), ); + if ($queryLog !== null) { + $script = str_replace("#!/bin/sh\n", "#!/bin/sh\nprintf '%s\\n' \"\$1\" >> " . escapeshellarg($queryLog) . "\n", $script); + } file_put_contents($path, $script); chmod($path, 0755); } diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index d55b4041..06d8b5b8 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -428,6 +428,13 @@ trait SourcePipelineTrait // Function and data declarations are emitted together, one header // per PHP source, plus a small project-runtime ABI header. $this->genDeclarationHeaders($files); + // Large array-valued class constants used to make module_init() one + // enormous GCC optimization unit. Emit their request-lifecycle + // helpers as independent, cacheable translation units while the + // arginfo-backed class registration remains in extension-*.cc. + foreach ($this->genClassArrayConstantLifecycleSources() as $lifecycleSource) { + $sourceFiles[] = $lifecycleSource; + } // Nano keeps the ordinary statically registered Zend class/module // metadata, then adds a direct native process entry beside it. $sourceFiles[] = $this->genExtension(); diff --git a/src/Platform/UnixPlatform.php b/src/Platform/UnixPlatform.php index 2a1b4731..c2bd519b 100644 --- a/src/Platform/UnixPlatform.php +++ b/src/Platform/UnixPlatform.php @@ -8,6 +8,22 @@ namespace TypePhp\Platform; */ abstract class UnixPlatform extends PlatformBase { + /** @var array */ + private array $phpDirectoryCache = []; + /** @var array */ + private array $phpConfigCache = []; + /** @var array> */ + private array $phpIncludeCache = []; + /** @var array */ + private array $phpConfigValueCache = []; + + private function getPhpSdkCacheKey(string $directory = ''): string + { + // Scope discovery to this platform instance and the selected environment. + // Constructing every compile/cache-key command must not rerun php-config. + return serialize([$directory, getenv('PHP_HOME'), getenv('PATH')]); + } + public function getSharedLinkFlag(): string { return '-shared'; @@ -97,6 +113,12 @@ abstract class UnixPlatform extends PlatformBase } public function getPhpDir(): string + { + $key = $this->getPhpSdkCacheKey(); + return $this->phpDirectoryCache[$key] ??= $this->resolvePhpDir(); + } + + private function resolvePhpDir(): string { $phpDir = getenv('PHP_HOME'); if (is_string($phpDir) && $phpDir !== '') { @@ -173,6 +195,12 @@ abstract class UnixPlatform extends PlatformBase * Build the PHP include paths (obtained dynamically via php-config). */ public function buildPhpIncludePaths(string $phpDir): array + { + $key = $this->getPhpSdkCacheKey($phpDir); + return $this->phpIncludeCache[$key] ??= $this->resolvePhpIncludePaths($phpDir); + } + + private function resolvePhpIncludePaths(string $phpDir): array { $phpConfigPath = $this->findPhpConfig($phpDir); if ($phpConfigPath) { @@ -213,6 +241,13 @@ abstract class UnixPlatform extends PlatformBase * Locate the php-config executable. */ protected function findPhpConfig(string $phpDir): ?string + { + $key = $this->getPhpSdkCacheKey($phpDir); + // Do not cache failed discovery: an SDK may be installed afterwards. + return $this->phpConfigCache[$key] ??= $this->resolvePhpConfig($phpDir); + } + + private function resolvePhpConfig(string $phpDir): ?string { $candidates = []; $phpDir = rtrim($phpDir, '/'); @@ -263,6 +298,13 @@ abstract class UnixPlatform extends PlatformBase $candidates[] = $versioned; } + // Validate preferred installations before invoking the PATH fallback. + foreach (array_unique($candidates) as $config) { + if ($this->phpConfigMatchesCurrentPhp($config)) { + return $config; + } + } + // PATH is only a fallback, and its prefix must match the selected PHP. $whichResult = trim(shell_exec('which php-config 2>/dev/null')); if ($whichResult && is_executable($whichResult)) { @@ -325,11 +367,15 @@ abstract class UnixPlatform extends PlatformBase protected function getPhpConfigValue(string $phpConfig, string $option): ?string { + $key = $this->getPhpSdkCacheKey($phpConfig) . "\0" . $option; + if (isset($this->phpConfigValueCache[$key])) { + return $this->phpConfigValueCache[$key]; + } $value = shell_exec(escapeshellarg($phpConfig) . ' ' . escapeshellarg($option) . ' 2>/dev/null'); if (!is_string($value) || trim($value) === '') { return null; } - return trim($value); + return $this->phpConfigValueCache[$key] = trim($value); } protected function resolvePhpLibDir(string $phpDir): ?string diff --git a/src/Translator.php b/src/Translator.php index b82f715c..83654d88 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -102,6 +102,17 @@ class Translator extends Preprocessor /** Generated per-file teardown functions for persistent AST class constants. */ protected array $releaseAstConstantFns = []; + /** @var list Cross-TU helpers called by module_init(). */ + private array $classArrayConstantInitCalls = []; + + /** @var list Cross-TU helpers called by module_clean(). */ + private array $classArrayConstantCleanCalls = []; + + /** @var list Forward declarations for split request-lifecycle helpers. */ + private array $classArrayConstantLifecycleDeclarations = []; + + private bool $classArrayConstantLifecycleSplit = false; + // Windows resource file configuration (icon, version info, etc.) protected array $resourceConfig = []; @@ -1184,6 +1195,12 @@ class Translator extends Preprocessor // whole-program property-default invariant local to the consumer too. $this->finalizeRequestArrayDefaultMetadata(); $this->genClassCeList(); + // Clean incremental metadata is restored in source-path order, while a + // full conversion discovers globals in dependency order. Definitions, + // request registration and cleanup have no ordering dependency, so use + // one canonical order and keep extension-*.cc byte-stable across both. + $extensionGlobalVars = $this->globalVars; + ksort($extensionGlobalVars, SORT_STRING); $this->indentLevel++; $code = $this->genExtensionIncludeHeaderFiles(); @@ -1217,7 +1234,7 @@ class Translator extends Preprocessor $code .= 'namespace ' . $projectNamespace . ' {' . PHP_EOL . PHP_EOL; $code .= "// global vars \n"; - foreach ($this->globalVars as $name => $type) { + foreach ($extensionGlobalVars as $name => $type) { $cppType = isset($this->nativeGlobalObjects[$name]) ? $this->getNativeObjectPointerType($this->nativeGlobalObjects[$name]) : Type::VAR; @@ -1230,7 +1247,11 @@ class Translator extends Preprocessor $code .= "// class register functions \n"; foreach ($this->classCeList as $ce) { - $code .= 'zend_class_entry *' . $ce . ';' . PHP_EOL; + // These slots are consumed only by the module-entry translation + // unit. Generated project sources resolve classes through the + // request/persistent cache accessors instead, so exposing one + // linkable data symbol per class is unnecessary. + $code .= 'static zend_class_entry *' . $ce . ' = nullptr;' . PHP_EOL; } $code .= "// request-local caches \n"; @@ -1403,6 +1424,12 @@ CODE; } $code .= $this->genRequestArrayDefaultInitializers(); + if ($this->classArrayConstantLifecycleDeclarations !== []) { + $code .= "// split request-lifecycle helpers\n"; + $code .= implode(PHP_EOL, $this->classArrayConstantLifecycleDeclarations) + . PHP_EOL . PHP_EOL; + } + $traitMetadata = $this->genTraitMetadataCode(); $code .= $traitMetadata['declarations']; @@ -1537,7 +1564,7 @@ CODE; $code .= 'php::fn::define(' . $this->genCharPtr($const->name, true) . ', ' . $name . ');' . PHP_EOL; } $code .= '// global vars ' . PHP_EOL; - foreach ($this->globalVars as $name => $type) { + foreach ($extensionGlobalVars as $name => $type) { if ($name == 'GLOBALS') { continue; } @@ -1591,7 +1618,7 @@ CODE; // request-level module state cleanup $code .= 'static void module_clean() {' . PHP_EOL; - foreach ($this->globalVars as $name => $type) { + foreach ($extensionGlobalVars as $name => $type) { if ($name != 'GLOBALS') { if (isset($this->nativeGlobalObjects[$name])) { $code .= $this->escapeGlobalVar($name) . ' = nullptr;' . PHP_EOL; @@ -1615,62 +1642,7 @@ CODE; $code .= $this->genPythonModuleCleanup(); $code .= '// class array constants' . PHP_EOL; - foreach ($this->getClassLikesWithConstants() as $classDef) { - foreach ($classDef->constants as $constant) { - if ($constant->type === Type::ARRAY) { - $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); - $code .= $constName . ".unset();\n"; - - if (!$classDef instanceof ClassDef || !$classDef->nativeObject) { - $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); - $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; - } - } - } - } - - // Clean up inherited array constants from child classes - foreach ($this->symbols->classes() as $className => $classDef) { - if ($classDef->nativeObject) { - continue; - } - $ownConstNames = []; - foreach ($classDef->constants as $constant) { - if ($constant->type === Type::ARRAY) { - $ownConstNames[$constant->name] = true; - } - } - - $parentName = $this->escapeClass($classDef->extends); - while ($parentName && $this->symbols->hasClass($parentName)) { - $parentDef = $this->symbols->class($parentName); - foreach ($parentDef->constants as $constant) { - if ($constant->type === Type::ARRAY && !isset($ownConstNames[$constant->name])) { - $ownConstNames[$constant->name] = true; - $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); - $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; - } - } - $parentName = $this->escapeClass($parentDef->extends); - } - - foreach ($this->getClassImplementedInterfaces($classDef) as $interfaceName) { - if (!$this->hasInterface($interfaceName)) { - continue; - } - $interfaceDef = $this->getInterface($interfaceName); - foreach ($interfaceDef->constants as $constant) { - if ($constant->type === Type::ARRAY && !isset($ownConstNames[$constant->name])) { - $ownConstNames[$constant->name] = true; - $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); - $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; - } - } - } - } + $code .= $this->genClassArrayConstantCleanup(); $code .= '}' . PHP_EOL . PHP_EOL; // module_clean end @@ -3460,20 +3432,173 @@ CODE; protected function genClassArrayConstants(): string { - $code = ''; + if ($this->classArrayConstantLifecycleSplit) { + return implode(PHP_EOL, $this->classArrayConstantInitCalls) + . ($this->classArrayConstantInitCalls === [] ? '' : PHP_EOL); + } + + return implode('', array_column($this->getClassArrayConstantLifecycleOperations(), 'init')); + } + + protected function genClassArrayConstantCleanup(): string + { + if ($this->classArrayConstantLifecycleSplit) { + return implode(PHP_EOL, $this->classArrayConstantCleanCalls) + . ($this->classArrayConstantCleanCalls === [] ? '' : PHP_EOL); + } + + return implode('', array_column($this->getClassArrayConstantLifecycleOperations(), 'clean')); + } + + /** + * Keep the arginfo-defined php_register_class_* functions in the extension + * translation unit, but isolate the much larger request-time array builders. + * Each constant gets its own non-inline function so GCC's points-to analysis + * cannot reconstruct the former multi-thousand-line module_init() under LTO. + * + * @return list + */ + public function genClassArrayConstantLifecycleSources(): array + { + $this->classArrayConstantInitCalls = []; + $this->classArrayConstantCleanCalls = []; + $this->classArrayConstantLifecycleDeclarations = []; + $this->classArrayConstantLifecycleSplit = true; + + /** @var array> $definitions */ + $definitions = []; + /** @var array> $dependencyFiles */ + $dependencyFiles = []; + foreach ($this->getClassArrayConstantLifecycleOperations() as $operation) { + $sourceFile = $operation['sourceFile']; + if ($sourceFile === '' || !isset($this->declarationHeaderFiles[$sourceFile])) { + // Project class-likes normally always have an owner. Falling + // back to the extension keeps the generator correct for direct + // embedding APIs that construct symbols without source files. + $this->classArrayConstantInitCalls[] = $operation['init']; + $this->classArrayConstantCleanCalls[] = $operation['clean']; + continue; + } + + $suffix = substr(hash('sha256', $sourceFile . "\0" . $operation['key']), 0, 20); + $initFunction = 'typephp_request_init_class_constant_' . $suffix; + $cleanFunction = 'typephp_request_clean_class_constant_' . $suffix; + $this->classArrayConstantLifecycleDeclarations[] = + "zend_never_inline void {$initFunction}();"; + $this->classArrayConstantLifecycleDeclarations[] = + "zend_never_inline void {$cleanFunction}();"; + $this->classArrayConstantInitCalls[] = "{$initFunction}();"; + $this->classArrayConstantCleanCalls[] = "{$cleanFunction}();"; + $definitions[$sourceFile][] = "zend_never_inline void {$initFunction}() {\n" + . $operation['init'] . "}\n\n"; + $definitions[$sourceFile][] = "zend_never_inline void {$cleanFunction}() {\n" + . $operation['clean'] . "}\n\n"; + foreach ($operation['dependencies'] as $dependencyFile) { + if ($dependencyFile !== '' && $dependencyFile !== $sourceFile) { + $dependencyFiles[$sourceFile][$dependencyFile] = true; + } + } + } + + $sources = []; + foreach ($definitions as $sourceFile => $sourceDefinitions) { + $headers = [ + ...$this->getGeneratedSourceGlobalHeaders(), + $this->getRuntimeDeclarationHeaderName(), + ...$this->getDeclarationHeadersForFile($sourceFile), + ]; + foreach (array_keys($dependencyFiles[$sourceFile] ?? []) as $dependencyFile) { + if (isset($this->declarationHeaderFiles[$dependencyFile])) { + $headers[] = $this->declarationHeaderFiles[$dependencyFile]; + } + } + + $lifecycleSource = $this->getClassArrayConstantLifecycleSourceFile($sourceFile); + $code = $this->renderIncludeHeaderFiles($headers) . PHP_EOL; + $code .= 'namespace ' . $this->getProjectNamespace() . ' {' . PHP_EOL . PHP_EOL; + $code .= implode('', $sourceDefinitions); + $code .= '} // namespace ' . $this->getProjectNamespace() . PHP_EOL; + $this->writeFile($lifecycleSource, $code); + $this->formatCppCode($lifecycleSource); + $this->registerGeneratedProjectSource($lifecycleSource); + $sources[] = $lifecycleSource; + } + + $this->removeStaleClassArrayConstantLifecycleSources($sources); + return $sources; + } + + private function getClassArrayConstantLifecycleSourceFile(string $sourceFile): string + { + $header = $this->declarationHeaderFiles[$sourceFile] + ?? basename($this->getDeclarationHeaderFile($sourceFile)); + $stem = preg_replace('/_decl\\.h$/', '', basename($header)) ?: pathinfo($header, PATHINFO_FILENAME); + return $this->getBuildDir() . '/init/' . $stem . '_class_constants.cc'; + } + + /** @param list $currentSources */ + private function removeStaleClassArrayConstantLifecycleSources(array $currentSources): void + { + $manifest = $this->getBuildDir() . '/cache/incremental/' . $this->targetName + . '/class-constant-sources.json'; + $previous = []; + if (is_file($manifest)) { + $decoded = json_decode((string) file_get_contents($manifest), true); + if (is_array($decoded)) { + $previous = array_values(array_filter($decoded, 'is_string')); + } + } + $current = array_map('basename', $currentSources); + foreach (array_diff($previous, $current) as $staleBasename) { + $staleSource = $this->getBuildDir() . '/init/' . basename($staleBasename); + $staleObject = $this->getObjectFile($staleSource); + foreach ([$staleSource, $staleObject, $this->getMiscObjectCacheMetadataFile($staleObject)] as $artifact) { + if (is_file($artifact)) { + @unlink($artifact); + } + } + } + $this->writeFile( + $manifest, + json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) + . PHP_EOL, + ); + } + + /** + * @return list, + * init: string, + * clean: string + * }> + */ + private function getClassArrayConstantLifecycleOperations(): array + { + $operations = []; foreach ($this->getClassLikesWithConstants() as $classDef) { foreach ($classDef->constants as $constant) { if ($constant->type === Type::ARRAY) { $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); - $code .= "do {\n"; - $code .= $constant->arrayExpr; - $code .= $constName . ' = ' . $constant->value . ";\n"; + $init = "do {\n"; + $init .= $constant->arrayExpr; + $init .= $constName . ' = ' . $constant->value . ";\n"; + $clean = $constName . ".unset();\n"; if (!$classDef instanceof ClassDef || !$classDef->nativeObject) { $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, {$constant->value});\n"; + $init .= "php::updateConstant($classNameStr, $classConstStr, {$constant->value});\n"; + $clean .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; } - $code .= "} while(0);\n"; + $init .= "} while(0);\n"; + $operations[] = [ + 'sourceFile' => $classDef->sourceFile, + 'key' => 'own:' . $classDef->getNamespacedName(false) . ':' . $constant->name, + 'dependencies' => [], + 'init' => $init, + 'clean' => $clean, + ]; } } } @@ -3499,7 +3624,14 @@ CODE; $constName = self::PREFIX . $this->getNativeName($constant->name, $parentDef->namespace, $parentDef->name); $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, {$constName});\n"; + $operations[] = [ + 'sourceFile' => $classDef->sourceFile, + 'key' => 'inherited:' . $classDef->getNamespacedName(false) . ':' + . $constant->name . ':' . $parentDef->getNamespacedName(false), + 'dependencies' => [$parentDef->sourceFile], + 'init' => "php::updateConstant($classNameStr, $classConstStr, {$constName});\n", + 'clean' => "php::updateConstant($classNameStr, $classConstStr, php::null);\n", + ]; } } $parentName = $this->escapeClass($parentDef->extends); @@ -3516,13 +3648,20 @@ CODE; $constName = self::PREFIX . $this->getNativeName($constant->name, $interfaceDef->namespace, $interfaceDef->name); $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, {$constName});\n"; + $operations[] = [ + 'sourceFile' => $classDef->sourceFile, + 'key' => 'interface:' . $classDef->getNamespacedName(false) . ':' + . $constant->name . ':' . $interfaceDef->getNamespacedName(false), + 'dependencies' => [$interfaceDef->sourceFile], + 'init' => "php::updateConstant($classNameStr, $classConstStr, {$constName});\n", + 'clean' => "php::updateConstant($classNameStr, $classConstStr, php::null);\n", + ]; } } } } - return $code; + return $operations; } protected function getAbsolutePath(string $path, string $projectDir): string