160 lines
4.6 KiB
TypeScript
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();
|
|
}
|
|
}
|