Files
WRNexusJS/packages/compiler/src/cache.ts
T
2026-07-27 12:42:18 +05:30

160 lines
4.6 KiB
TypeScript

import { createHash } from "node:crypto";
import { diagnose, parse, ParseError } from "@wrnexus/syntax";
import { generate } from "./codegen.ts";
import type { CompileResult } from "./index.ts";
function compileSource(source: string, filePath: string): CompileResult {
const richDiagnostics = diagnose(source, { file: filePath, accessibility: true });
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
if (errors.length > 0) {
throw new ParseError(
errors.map((diagnostic) => diagnostic.message).join("\n"),
errors[0]!.code,
);
}
const ast = parse(source);
return {
code: `// compiled from .wrn\n${generate(ast)}`,
ast,
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
richDiagnostics,
};
}
export interface CompilationCacheEntry extends CompileResult {
key: string;
file: string;
sourceHash: string;
createdAt: number;
}
export interface CompilationCacheOptions {
maxEntries?: number;
now?: () => number;
}
export interface CompilationCache {
compile(source: string, file?: string, salt?: string): CompilationCacheEntry;
get(key: string): CompilationCacheEntry | undefined;
invalidate(file?: string): number;
clear(): void;
size(): number;
stats(): { hits: number; misses: number; entries: number };
}
export function compilationKey(source: string, file = "<inline .wrn>", salt = ""): string {
return createHash("sha256")
.update(file)
.update("\0")
.update(salt)
.update("\0")
.update(source)
.digest("hex");
}
export function createCompilationCache(options: CompilationCacheOptions = {}): CompilationCache {
const maxEntries = options.maxEntries ?? 500;
if (!Number.isInteger(maxEntries) || maxEntries < 1)
throw new RangeError("maxEntries must be positive");
const now = options.now ?? Date.now;
const entries = new Map<string, CompilationCacheEntry>();
let hits = 0;
let misses = 0;
function touch(key: string, value: CompilationCacheEntry): void {
entries.delete(key);
entries.set(key, value);
while (entries.size > maxEntries) entries.delete(entries.keys().next().value!);
}
return {
compile(source, file = "<inline .wrn>", salt = "") {
const key = compilationKey(source, file, salt);
const existing = entries.get(key);
if (existing) {
hits++;
touch(key, existing);
return existing;
}
misses++;
const result = compileSource(source, file);
const entry: CompilationCacheEntry = {
...result,
key,
file,
sourceHash: createHash("sha256").update(source).digest("hex"),
createdAt: now(),
};
touch(key, entry);
return entry;
},
get(key) {
const entry = entries.get(key);
if (entry) touch(key, entry);
return entry;
},
invalidate(file) {
let removed = 0;
for (const [key, entry] of entries) {
if (!file || entry.file === file) {
entries.delete(key);
removed++;
}
}
return removed;
},
clear() {
entries.clear();
},
size: () => entries.size,
stats: () => ({ hits, misses, entries: entries.size }),
};
}
export class DependencyGraph {
readonly #dependencies = new Map<string, Set<string>>();
readonly #dependents = new Map<string, Set<string>>();
set(file: string, dependencies: Iterable<string>): void {
this.remove(file);
const values = new Set(dependencies);
this.#dependencies.set(file, values);
for (const dependency of values) {
const set = this.#dependents.get(dependency) ?? new Set<string>();
set.add(file);
this.#dependents.set(dependency, set);
}
}
remove(file: string): void {
for (const dependency of this.#dependencies.get(file) ?? []) {
const set = this.#dependents.get(dependency);
set?.delete(file);
if (set?.size === 0) this.#dependents.delete(dependency);
}
this.#dependencies.delete(file);
}
dependencies(file: string): string[] {
return [...(this.#dependencies.get(file) ?? [])].sort();
}
dependents(file: string): string[] {
return [...(this.#dependents.get(file) ?? [])].sort();
}
affected(file: string): string[] {
const found = new Set<string>();
const queue = [file];
while (queue.length) {
const current = queue.shift()!;
for (const dependent of this.#dependents.get(current) ?? []) {
if (found.has(dependent)) continue;
found.add(dependent);
queue.push(dependent);
}
}
return [...found].sort();
}
}