TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

357 lines
14 KiB

<?php
namespace TypePhp\Build;
/** Project-level incremental planning kept separate from the language parser. */
trait IncrementalCompilationTrait
{
private ?IncrementalBuildState $incrementalBuildState = null;
private string $incrementalBuildStateFile = '';
/** @var array<string, string> */
private array $incrementalSourceHashes = [];
/** @var array<string, list<string>> */
private array $incrementalDependencies = [];
/** @var array<string, true> */
private array $incrementalDirtyFiles = [];
/** @var array<string, array<string, mixed>> */
private array $incrementalFileMetadata = [];
/** @var array<string, bool> */
private array $incrementalTranslationUnits = [];
private string $incrementalGeneratorFingerprint = '';
private bool $incrementalPlanInitialized = false;
/** @param list<string> $files */
protected function initializeIncrementalCompilation(array $files): void
{
$this->incrementalPlanInitialized = false;
$this->incrementalFileMetadata = [];
$this->incrementalTranslationUnits = [];
$this->incrementalDirtyFiles = [];
$phpFiles = [];
foreach ($files as $file) {
if (!FileScanner::isPhpFile($file)) {
continue;
}
$path = realpath($file);
if ($path !== false) {
$phpFiles[] = $path;
}
}
sort($phpFiles, SORT_STRING);
$this->incrementalSourceHashes = [];
foreach ($phpFiles as $file) {
$hash = hash_file('sha256', $file);
if (!is_string($hash)) {
throw new \RuntimeException('Cannot hash PHP source: ' . $file);
}
$this->incrementalSourceHashes[$file] = $hash;
}
$state = $this->getIncrementalBuildState();
$this->restoreGlobalDependencyOwnersForPlanning($phpFiles, $state);
$this->incrementalDependencies = $this->buildPhpFileDependencyGraph($phpFiles);
$this->incrementalGeneratorFingerprint = $this->getIncrementalGeneratorFingerprint();
$this->incrementalDirtyFiles = $state->dirtyFiles(
$this->incrementalSourceHashes,
$this->incrementalDependencies,
$this->incrementalGeneratorFingerprint,
function (string $source, ?array $metadata): bool {
if (!is_array($metadata)
|| !is_file($this->getDeclarationHeaderFile($source))
|| !is_file($this->getArgInfoHeaderFile($source))) {
return false;
}
$emits = $metadata['emitsTranslationUnit'] ?? null;
if (!is_bool($emits)) {
return false;
}
return !$emits || is_file($this->getCppFile($source));
},
$this->climate->arguments->defined('force'),
);
$this->incrementalPlanInitialized = true;
}
protected function shouldRegeneratePhpFile(string $file): bool
{
if (!$this->incrementalPlanInitialized) {
return true;
}
$path = realpath($file) ?: $file;
return isset($this->incrementalDirtyFiles[$path]);
}
protected function incrementalTranslationUnitWasEmitted(string $file): bool
{
$path = realpath($file) ?: $file;
return $this->incrementalTranslationUnits[$path]
?? (bool) ($this->getIncrementalBuildState()->metadata($path)['emitsTranslationUnit'] ?? false);
}
protected function registerGeneratedProjectSource(string $source): void
{
$this->generatedProjectSources[$source] = true;
}
/**
* Restore conversion outputs which are consumed by whole-program generators.
*
* @param list<string> $files
*/
protected function restoreCleanIncrementalMetadata(array $files): void
{
if (!$this->incrementalPlanInitialized) {
return;
}
foreach ($files as $file) {
if (!FileScanner::isPhpFile($file)) {
continue;
}
$file = realpath($file) ?: $file;
if ($this->shouldRegeneratePhpFile($file)) {
continue;
}
$metadata = $this->getIncrementalBuildState()->metadata($file);
if (!is_array($metadata)) {
continue;
}
$this->incrementalFileMetadata[$file] = $metadata;
$this->incrementalTranslationUnits[$file] = (bool) ($metadata['emitsTranslationUnit'] ?? false);
$statistics = $metadata['statistics'] ?? [];
if (is_array($statistics)) {
$this->compilationStatistics->merge($statistics);
}
$globals = $metadata['globals'] ?? [];
if (is_array($globals)) {
foreach ($globals as $name => $type) {
if (is_string($name) && is_string($type)) {
$this->globalVarsInFile[$file][$name] = $type;
}
}
}
$initializers = $metadata['nativeStaticInitializers'] ?? [];
if (is_array($initializers)) {
foreach ($initializers as $name) {
if (is_string($name)) {
$this->nativeStaticInitializersInFile[$file][$name] = true;
}
}
}
$this->registerExistingArgInfoHeader($file);
}
$this->rebuildIncrementalGlobalState();
}
/** @param array<string, array<string, int>> $statistics */
protected function recordIncrementalConversion(
string $file,
bool $emitsTranslationUnit,
array $statistics,
): void {
$path = realpath($file) ?: $file;
$this->incrementalTranslationUnits[$path] = $emitsTranslationUnit;
$this->incrementalFileMetadata[$path]['statistics'] = $statistics;
}
protected function rebuildIncrementalGlobalState(): void
{
foreach ($this->globalVarDeclInFile as $name => $_file) {
unset($this->symbolDeclInFile[$this->getGlobalDependencySymbol($name)]);
}
$this->globalVarDeclInFile = [];
$this->nativeStaticInitializerDeclInFile = [];
foreach ($this->globalVarsInFile as $file => $globals) {
ksort($globals, SORT_STRING);
foreach ($globals as $name => $type) {
if (!isset($this->nativeGlobalObjects[$name])) {
$this->globalVars[$name] = $type;
}
if (!isset($this->globalVarDeclInFile[$name])) {
$this->globalVarDeclInFile[$name] = $file;
$this->symbolDeclInFile[$this->getGlobalDependencySymbol($name)] = $file;
}
$this->symbolCallInFile[$file][] = $this->getGlobalDependencySymbol($name);
}
}
foreach ($this->nativeStaticInitializersInFile as $file => $initializers) {
foreach ($initializers as $name => $_) {
$this->nativeStaticInitializers[$name] = true;
$this->nativeStaticInitializerDeclInFile[$name] = $file;
}
}
}
/** @param list<string> $files */
protected function finalizeIncrementalConversionMetadata(array $files): void
{
if (!$this->incrementalPlanInitialized) {
return;
}
$this->rebuildIncrementalGlobalState();
$phpFiles = [];
foreach ($files as $file) {
if (!FileScanner::isPhpFile($file)) {
continue;
}
$path = realpath($file);
if ($path !== false) {
$phpFiles[] = $path;
}
}
$this->incrementalDependencies = $this->buildPhpFileDependencyGraph($phpFiles);
}
/** @param list<string> $files */
protected function saveIncrementalCompilationState(array $files): void
{
if (!$this->incrementalPlanInitialized) {
return;
}
$this->rebuildIncrementalGlobalState();
$stateFiles = [];
foreach ($files as $file) {
if (!FileScanner::isPhpFile($file)) {
continue;
}
$path = realpath($file);
if ($path === false || !isset($this->incrementalSourceHashes[$path])) {
continue;
}
$previous = $this->incrementalFileMetadata[$path]
?? $this->getIncrementalBuildState()->metadata($path)
?? [];
$declared = [];
foreach ($this->symbolDeclInFile as $symbol => $declaringFile) {
if ($declaringFile === $path) {
$declared[] = $symbol;
}
}
sort($declared, SORT_STRING);
$used = array_values(array_unique($this->symbolCallInFile[$path] ?? []));
sort($used, SORT_STRING);
$dependencies = $this->incrementalDependencies[$path] ?? [];
sort($dependencies, SORT_STRING);
$initializers = array_keys($this->nativeStaticInitializersInFile[$path] ?? []);
sort($initializers, SORT_STRING);
$stateFiles[$path] = [
'hash' => $this->incrementalSourceHashes[$path],
'dependencies' => $dependencies,
'symbolsDeclared' => $declared,
'symbolsUsed' => $used,
'emitsTranslationUnit' => $this->incrementalTranslationUnits[$path]
?? (bool) ($previous['emitsTranslationUnit'] ?? false),
'header' => $this->getDeclarationHeaderFile($path),
'cpp' => $this->getCppFile($path),
'statistics' => $this->incrementalFileMetadata[$path]['statistics']
?? ($previous['statistics'] ?? []),
'globals' => $this->globalVarsInFile[$path] ?? [],
'nativeStaticInitializers' => $initializers,
];
}
$this->getIncrementalBuildState()->save(
$this->incrementalGeneratorFingerprint,
$stateFiles,
);
}
/** @param list<string> $files @return array<string, list<string>> */
private function buildPhpFileDependencyGraph(array $files): array
{
$known = array_fill_keys($files, true);
$dependencies = [];
foreach ($files as $file) {
$requirements = [];
foreach ($this->symbolCallInFile[$file] ?? [] as $symbol) {
$dependency = $this->symbolDeclInFile[$symbol] ?? null;
if (is_string($dependency)
&& $dependency !== $file
&& isset($known[$dependency])) {
$requirements[$dependency] = true;
}
}
$list = array_keys($requirements);
sort($list, SORT_STRING);
$dependencies[$file] = $list;
}
return $dependencies;
}
private function getIncrementalBuildState(): IncrementalBuildState
{
$file = $this->getBuildDir() . '/cache/incremental/' . $this->targetName . '/build-state.json';
if ($this->incrementalBuildState === null || $this->incrementalBuildStateFile !== $file) {
$this->incrementalBuildState = new IncrementalBuildState($file);
$this->incrementalBuildStateFile = $file;
}
return $this->incrementalBuildState;
}
/** @param list<string> $files */
private function restoreGlobalDependencyOwnersForPlanning(
array $files,
IncrementalBuildState $state,
): void {
$knownFiles = array_fill_keys($files, true);
foreach ($files as $file) {
$metadata = $state->metadata($file);
if (!is_array($metadata)) {
continue;
}
foreach ($metadata['symbolsDeclared'] ?? [] as $symbol) {
if (is_string($symbol) && str_starts_with($symbol, 'global:')) {
$this->symbolDeclInFile[$symbol] = $file;
}
}
}
foreach ($files as $file) {
foreach ($this->symbolCallInFile[$file] ?? [] as $symbol) {
if (!str_starts_with($symbol, 'global:')
|| isset($this->symbolDeclInFile[$symbol])) {
continue;
}
// A newly introduced global has no previous owner. Assign one
// deterministically for this planning pass; conversion will
// record the actual first declaration owner in sorted order.
$this->symbolDeclInFile[$symbol] = $file;
}
}
foreach ($this->symbolDeclInFile as $symbol => $file) {
if (str_starts_with($symbol, 'global:') && !isset($knownFiles[$file])) {
unset($this->symbolDeclInFile[$symbol]);
}
}
}
private function getIncrementalGeneratorFingerprint(): string
{
$context = hash_init('sha256');
hash_update($context, serialize([
'translator' => self::VERSION,
'php' => $this->phpVersion,
'mode' => $this->buildMode,
'nano' => $this->isNanoMode(),
'nanoPolicy' => $this->isNanoPolicyMode(),
'wasi' => $this->isWasiTarget(),
'platform' => $this->targetPlatform,
'literalStrings' => !$this->noLiteralStrings,
'debug' => $this->debug,
]));
$sourceDirectory = dirname(__DIR__);
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDirectory, \FilesystemIterator::SKIP_DOTS),
);
$sources = [];
foreach ($iterator as $entry) {
if ($entry->isFile() && $entry->getExtension() === 'php') {
$sources[] = $entry->getPathname();
}
}
sort($sources, SORT_STRING);
foreach ($sources as $source) {
hash_update($context, str_replace('\\', '/', $source) . "\0");
hash_update_file($context, $source);
}
return hash_final($context);
}
}