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 = "", 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(); 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 = "", 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>(); readonly #dependents = new Map>(); set(file: string, dependencies: Iterable): 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(); 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(); 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(); } }