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.
136 lines
4.5 KiB
136 lines
4.5 KiB
<?php
|
|
|
|
namespace TypePhp\Build;
|
|
|
|
/**
|
|
* Append-only IDs used by generated translation units.
|
|
*
|
|
* IDs are deliberately never recycled. An object file from an earlier
|
|
* incremental build can therefore keep referring to its old slot while other
|
|
* source files are regenerated independently.
|
|
*/
|
|
final class StableIdRegistry
|
|
{
|
|
private const int SCHEMA_VERSION = 2;
|
|
|
|
/** @var array<string, array<string, int>> */
|
|
private array $domains = [];
|
|
/** @var array<string, int> */
|
|
private array $nextIds = [];
|
|
private bool $dirty = false;
|
|
|
|
public function __construct(private readonly string $file)
|
|
{
|
|
$this->restore();
|
|
}
|
|
|
|
public function allocate(string $domain, string $key): int
|
|
{
|
|
// Prefix prevents a base64 result made only of digits from being cast
|
|
// to an integer array key by PHP.
|
|
$encoded = 'b:' . base64_encode($key);
|
|
if (isset($this->domains[$domain][$encoded])) {
|
|
return $this->domains[$domain][$encoded];
|
|
}
|
|
$id = $this->nextIds[$domain] ?? 0;
|
|
$this->domains[$domain][$encoded] = $id;
|
|
$this->nextIds[$domain] = $id + 1;
|
|
$this->dirty = true;
|
|
return $id;
|
|
}
|
|
|
|
public function capacity(string $domain): int
|
|
{
|
|
return $this->nextIds[$domain] ?? 0;
|
|
}
|
|
|
|
/** @return array<string, int> */
|
|
public function entries(string $domain): array
|
|
{
|
|
$entries = [];
|
|
foreach ($this->domains[$domain] ?? [] as $key => $id) {
|
|
$decoded = str_starts_with($key, 'b:')
|
|
? base64_decode(substr($key, 2), true)
|
|
: false;
|
|
if ($decoded !== false) {
|
|
$entries[$decoded] = $id;
|
|
}
|
|
}
|
|
asort($entries, SORT_NUMERIC);
|
|
return $entries;
|
|
}
|
|
|
|
public function flush(): void
|
|
{
|
|
if (!$this->dirty || $this->file === '') {
|
|
return;
|
|
}
|
|
$contents = json_encode([
|
|
'schema' => self::SCHEMA_VERSION,
|
|
'domains' => $this->domains,
|
|
'nextIds' => $this->nextIds,
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL;
|
|
AtomicFile::write($this->file, $contents, '.ids-');
|
|
$this->dirty = false;
|
|
}
|
|
|
|
private function restore(): void
|
|
{
|
|
if ($this->file === '' || !is_file($this->file)) {
|
|
return;
|
|
}
|
|
try {
|
|
$contents = file_get_contents($this->file);
|
|
$payload = is_string($contents)
|
|
? json_decode($contents, true, flags: JSON_THROW_ON_ERROR)
|
|
: null;
|
|
if (!is_array($payload)
|
|
|| ($payload['schema'] ?? null) !== self::SCHEMA_VERSION
|
|
|| !is_array($payload['domains'] ?? null)
|
|
|| !is_array($payload['nextIds'] ?? null)
|
|
) {
|
|
return;
|
|
}
|
|
$domains = [];
|
|
$nextIds = [];
|
|
foreach ($payload['domains'] as $domain => $entries) {
|
|
if (!is_string($domain) || !is_array($entries)) {
|
|
return;
|
|
}
|
|
$usedIds = [];
|
|
foreach ($entries as $key => $id) {
|
|
if (!is_string($key)
|
|
|| !str_starts_with($key, 'b:')
|
|
|| base64_decode(substr($key, 2), true) === false
|
|
|| !is_int($id)
|
|
|| $id < 0
|
|
|| isset($usedIds[$id])) {
|
|
return;
|
|
}
|
|
$domains[$domain][$key] = $id;
|
|
$usedIds[$id] = true;
|
|
}
|
|
}
|
|
foreach ($payload['nextIds'] as $domain => $nextId) {
|
|
if (is_string($domain) && is_int($nextId) && $nextId >= 0) {
|
|
$nextIds[$domain] = $nextId;
|
|
}
|
|
}
|
|
// Never trust a persisted cursor to be smaller than an existing
|
|
// entry. A truncated/manual cache edit must produce holes at worst,
|
|
// never duplicate an ID already embedded in an object file.
|
|
foreach ($domains as $domain => $entries) {
|
|
$minimumNextId = max($entries) + 1;
|
|
$nextIds[$domain] = max(
|
|
$nextIds[$domain] ?? 0,
|
|
$minimumNextId,
|
|
);
|
|
}
|
|
$this->domains = $domains;
|
|
$this->nextIds = $nextIds;
|
|
} catch (\Throwable) {
|
|
$this->domains = [];
|
|
$this->nextIds = [];
|
|
}
|
|
}
|
|
}
|
|
|