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
master
韩天峰 4 weeks ago
parent 5d4f31a9ab
commit 05c5b591aa
  1. 90
      phpunit/src/Build/CompilerGeneratorFingerprintTest.php
  2. 45
      src/Build/IncrementalCompilationTrait.php
  3. 1
      src/Translator.php

@ -0,0 +1,90 @@
<?php
namespace TypePhp\Tests\Build;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use ReflectionClass;
use TypePhp\CompilerBase;
use TypePhp\CompilerTest;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\FunctionDef;
use TypePhp\Type;
final class CompilerGeneratorFingerprintTest extends TestCase
{
#[RunInSeparateProcess]
public function testEmbeddedSnapshotIsUsedOnlyByNativeEntry(): void
{
define('TYPEPHP_COMPILER_BUILD_FINGERPRINT', str_repeat('a', 64));
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$method = (new ReflectionClass(\TypePhp\Translator::class))->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);
}
}

@ -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;
}
}

@ -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";

Loading…
Cancel
Save