release: WRNexusJS 0.4.0
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user