From 05c5b591aa06b53650ae5885dfbc31e93db1a06d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 15 Sep 2026 14:20:56 +0800 Subject: [PATCH] test(build): add compiler generator fingerprint tests - Add CompilerGeneratorFingerprintTest to verify embedded snapshot usage - Test that compiler snapshots are stable and change with source or options - Verify ordinary programs and extensions do not register compiler snapshots - Add separate process attribute for accurate testing conditions - Test incremental generator fingerprint generation and registration logic feat(build): implement compiler build fingerprint registration - Embed immutable source snapshot in self-hosted compiler builds - Add fallback mechanism for executables built before metadata introduction - Optimize performance by avoiding expensive source file hashing - Generate compiled generator fingerprint registration during translation - Prevent recursive fingerprinting in repeated self-hosted builds - Register compiler build fingerprint only for binary builds with specific criteria --- .../CompilerGeneratorFingerprintTest.php | 90 +++++++++++++++++++ src/Build/IncrementalCompilationTrait.php | 45 ++++++++++ src/Translator.php | 1 + 3 files changed, 136 insertions(+) create mode 100644 phpunit/src/Build/CompilerGeneratorFingerprintTest.php diff --git a/phpunit/src/Build/CompilerGeneratorFingerprintTest.php b/phpunit/src/Build/CompilerGeneratorFingerprintTest.php new file mode 100644 index 00000000..6c62361e --- /dev/null +++ b/phpunit/src/Build/CompilerGeneratorFingerprintTest.php @@ -0,0 +1,90 @@ +getMethod('getIncrementalGeneratorFingerprint'); + $native = $method->invoke($compiler); + self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/D', $native); + self::assertSame($native, $method->invoke($compiler)); + define('TYPEPHP_PHP_SCRIPT_ENTRY', true); + self::assertNotSame($native, $method->invoke($compiler)); + } + + public function testCompilerSnapshotIsStableAndChangesWithSourceOrOptions(): void + { + $compiler = $this->compiler(); + $registration = $this->registration($compiler); + self::assertMatchesRegularExpression('/php::fn::define\("TYPEPHP_COMPILER_BUILD_FINGERPRINT", php::Str\("[a-f0-9]{64}"\)\);/', $registration); + self::assertSame($registration, $this->registration($compiler)); + + $this->set($compiler, 'incrementalGeneratorFingerprint', 'a different predecessor'); + self::assertSame($registration, $this->registration($compiler)); + $this->set($compiler, 'incrementalSourceHashes', ['compiler.php' => 'changed']); + self::assertNotSame($registration, $this->registration($compiler)); + $sourceRegistration = $this->registration($compiler); + $this->set($compiler, 'optimizeLevel', 1); + self::assertNotSame($sourceRegistration, $this->registration($compiler)); + } + + public function testOrdinaryProgramsAndExtensionsDoNotRegisterCompilerSnapshot(): void + { + $compiler = $this->compiler(); + $compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + self::assertSame('', $this->registration($compiler)); + $compiler->setBuildMode(CompilerBase::BUILD_MODE_BIN); + $this->set($compiler, 'incrementalPlanInitialized', false); + self::assertSame('', $this->registration($compiler)); + $this->set($compiler, 'incrementalPlanInitialized', true); + $symbols = (new ReflectionClass($compiler))->getProperty('symbols')->getValue($compiler); + $entry = $symbols->function((new ReflectionClass($compiler))->getMethod('escapeFunction')->invoke($compiler, 'main')); + $entry->sourceFile = TYPEPHP_ROOT_PATH . '/src/CompilerBase.php'; + self::assertSame('', $this->registration($compiler)); + } + + private function compiler(): CompilerTest + { + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $compiler->setBuildMode(CompilerBase::BUILD_MODE_BIN); + $reflection = new ReflectionClass($compiler); + $symbols = $reflection->getProperty('symbols')->getValue($compiler); + $entry = new FunctionDef('main', Type::VOID, ''); + $entry->sourceFile = TYPEPHP_ROOT_PATH . '/src/compiler.php'; + $symbols->putFunction($reflection->getMethod('escapeFunction')->invoke($compiler, 'main'), $entry); + $class = new ClassDef('Translator', 0, 'TypePhp'); + $class->sourceFile = TYPEPHP_ROOT_PATH . '/src/Translator.php'; + $symbols->putClass($reflection->getMethod('escapeClass')->invoke($compiler, 'TypePhp\\Translator'), $class); + $this->set($compiler, 'incrementalPlanInitialized', true); + $this->set($compiler, 'incrementalSourceHashes', ['compiler.php' => 'original']); + return $compiler; + } + + private function registration(CompilerTest $compiler): string + { + return (new ReflectionClass($compiler))->getMethod('genCompiledGeneratorFingerprintRegistration')->invoke($compiler); + } + + private function set(CompilerTest $compiler, string $property, mixed $value): void + { + $reflection = new ReflectionClass($compiler); + while (!$reflection->hasProperty($property)) { + $reflection = $reflection->getParentClass(); + } + $reflection->getProperty($property)->setValue($compiler, $value); + } +} diff --git a/src/Build/IncrementalCompilationTrait.php b/src/Build/IncrementalCompilationTrait.php index 5e6b58d6..8a7e1ca2 100644 --- a/src/Build/IncrementalCompilationTrait.php +++ b/src/Build/IncrementalCompilationTrait.php @@ -338,6 +338,19 @@ trait IncrementalCompilationTrait 'debug' => $this->debug, ])); + // New self-hosted compiler builds embed their immutable source snapshot. + // Reading and hashing a 20+ MB executable on every tiny build costs more + // than processing the consumer source. Keep the executable fallback for + // binaries built before this metadata was introduced. + if (!defined('TYPEPHP_PHP_SCRIPT_ENTRY') + && defined('TYPEPHP_COMPILER_BUILD_FINGERPRINT')) { + $snapshot = constant('TYPEPHP_COMPILER_BUILD_FINGERPRINT'); + if (is_string($snapshot) && preg_match('/^[a-f0-9]{64}$/D', $snapshot) === 1) { + hash_update($context, $snapshot); + return hash_final($context); + } + } + // A compiled tpc executable is an immutable snapshot of the generator. // Walking and hashing every compiler PHP source on each consumer build // is both unnecessary and disproportionately expensive through the AOT @@ -375,4 +388,36 @@ trait IncrementalCompilationTrait } return hash_final($context); } + + protected function genCompiledGeneratorFingerprintRegistration(): string + { + // Only the compiler executable owns this runtime constant; ordinary + // consumer binaries, libraries and extensions must not register it. + if (!$this->isBuildModeBin() + || !$this->incrementalPlanInitialized + || !$this->hasFunction(self::ENTRY_FUNCTION) + || !$this->hasClass('TypePhp\\Translator') + || realpath($this->getFunction(self::ENTRY_FUNCTION)->sourceFile) + !== realpath($this->rootPath . '/src/compiler.php') + || realpath($this->getClass('TypePhp\\Translator')->sourceFile) + !== realpath($this->rootPath . '/src/Translator.php')) { + return ''; + } + // Do not include the running generator's fingerprint: that would make + // repeated self-hosted builds recursively fingerprint their predecessor. + $snapshot = hash('sha256', serialize([ + 'sources' => $this->incrementalSourceHashes, + 'php' => $this->phpVersion, + 'platform' => $this->targetPlatform, + 'cxxStd' => $this->cxxStd, + 'cxxFlags' => $this->cxxFlags, + 'optimize' => $this->optimizeLevel, + 'debug' => $this->debug, + ])); + // Embed mode activates this module after PHP startup has snapshotted + // persistent_constants_count. A late persistent constant would be freed + // as request memory by Zend shutdown. Publish the tiny value in RINIT. + return 'php::fn::define("TYPEPHP_COMPILER_BUILD_FINGERPRINT", php::Str("' + . $snapshot . '"));' . PHP_EOL; + } } diff --git a/src/Translator.php b/src/Translator.php index 83654d88..a33f2f17 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1558,6 +1558,7 @@ CODE; // request-level module state initialization $code .= 'static void module_init() {' . PHP_EOL; + $code .= $this->genCompiledGeneratorFingerprintRegistration(); $code .= '// register constants' . PHP_EOL; foreach ($this->constants as $name => $const) { $code .= "{$name} = {$const->value};\n";