// A copy of test/erase.mjs that never aborts: throws are tallied per input, with examples per class.
// Usage: node judge.mjs
[--list=path] (TYPESTRIP_ERASE_WASM selects the engine)
import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
const root = new URL("../..", import.meta.url).pathname.replace(/\/$/, "");
const ts = createRequire(root + "/package.json")("typescript");
process.env.TYPESTRIP_ERASE_WASM ??= "build/erase.wasm";
const { blankSourceFile } = await import(root + "/vendor/ts-blank-space/out/index.js");
const { transformSync } = await import(root + "/src/index.js");
const { cases } = await import(root + "/test/corpus.mjs");
const arg = process.argv[2];
const listPath = process.argv.find((a) => a.startsWith("--list="))?.slice(7);
const SK = ts.SyntaxKind;
const ALLOWED = new Set([" ", ";", "(", ")"]);
function* walk(dir) {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
if (e.name.startsWith(".")) continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) yield* walk(full);
else if (/\.[cm]?ts$/.test(e.name) && !e.name.endsWith(".d.ts.map")) yield full;
}
}
function* inputs() {
if (arg && fs.existsSync(arg) && fs.statSync(arg).isDirectory()) {
const seen = new Set();
for (const f of walk(arg)) {
let src;
try { src = fs.readFileSync(f, "utf8"); } catch { continue; }
if (seen.has(src)) continue;
seen.add(src);
const sf = ts.createSourceFile("input.ts", src, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
if (sf.parseDiagnostics.length === 0) yield { name: f, src, sf };
}
} else if (arg?.endsWith(".jsonl")) {
const lines = fs.readFileSync(arg, "utf8").split("\n").filter(Boolean);
for (const [i, line] of lines.entries()) yield { name: `${path.basename(arg)}/${i}`, src: JSON.parse(line) };
} else {
for (const c of cases({ only: arg })) yield c;
}
}
function jsAst(code) {
const sf = ts.createSourceFile("out.js", code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.JS);
const out = [];
(function visit(n) {
if (n.kind === SK.EmptyStatement || n.kind === SK.SemicolonClassElement) return;
out.push(n.kind);
if (ts.isIdentifier(n) || ts.isPrivateIdentifier(n) || ts.isLiteralExpression(n) || ts.isTemplateLiteralToken?.(n)) out.push(JSON.stringify(n.text));
ts.forEachChild(n, visit);
out.push(-1);
})(sf);
return { ok: sf.parseDiagnostics.length === 0, ast: out.join(",") };
}
function positional(src, out) {
if (out.length !== src.length) return `length ${out.length} != input ${src.length}`;
for (let i = 0; i < src.length; i++) {
const a = src[i], b = out[i];
if (a === b) continue;
if (a === "\n" || a === "\r") return `line terminator changed at ${i}`;
if (!ALLOWED.has(b)) return `invented ${JSON.stringify(b)} at ${i}`;
}
return null;
}
function* parts(src) {
let seed = src.length;
for (let i = 0; i < src.length; ) {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
const n = 1 + (seed % 4096);
yield src.slice(i, i + n);
i += n;
}
}
function chunked(src) {
let out = "";
for (const piece of transformSync(parts(src))) out += piece;
return out;
}
let pass = 0, n = 0;
const failures = [];
const tally = {};
const SHOW = Number(process.env.SHOW ?? 3);
const t0 = performance.now();
for (const { name, src, sf: parsed } of inputs()) {
n++;
const sf = parsed ?? ts.createSourceFile("input.ts", src, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
const wantErrors = [];
let want;
try { want = blankSourceFile(sf, (x) => wantErrors.push(x.getStart(sf))); } catch (e) { continue; }
const gotErrors = [];
let got;
let problem = null;
try {
got = transformSync(src, { onError: (e) => gotErrors.push(e.start) });
} catch (e) {
problem = "threw whole";
}
if (!problem) {
problem = positional(src, got);
if (!problem) {
const w = jsAst(want), g = jsAst(got);
if (w.ok && !g.ok) problem = "JS parse failed where ts-blank-space's output parses";
else if (w.ast !== g.ast) problem = "JS AST differs from ts-blank-space's output";
else if (wantErrors.join() !== gotErrors.join()) problem = `errors want [${wantErrors}] got [${gotErrors}]`;
else {
try {
if (chunked(src) !== got) problem = "streamed output differs from whole-input output";
} catch (e) {
problem = "threw chunked";
}
}
}
}
if (!problem) { pass++; continue; }
const kind = problem.startsWith("errors") ? "errors differ" : problem.split(" ").slice(0, 3).join(" ");
tally[kind] = (tally[kind] ?? 0) + 1;
let detail = "";
if (got !== undefined) {
let i = 0;
while (i < want.length && got[i] === want[i]) i++;
detail = `near ${i}\n src : ${JSON.stringify(src.slice(Math.max(0, i - 50), i + 50))}\n want: ${JSON.stringify(want.slice(Math.max(0, i - 50), i + 50))}\n got : ${JSON.stringify(got.slice(Math.max(0, i - 50), i + 50))}`;
}
failures.push({ name, kind, problem, detail });
if ((tally[kind] ?? 0) <= SHOW) console.log(`FAIL ${name}: ${problem}\n ${detail}\n`);
}
console.log(`judge: ${pass} pass, ${failures.length} fail of ${n} in ${((performance.now() - t0) / 1000).toFixed(0)}s`, JSON.stringify(tally));
if (listPath) fs.writeFileSync(listPath, failures.map((f) => `${f.kind}\t${f.name}\t${f.problem}`).join("\n") + "\n");