mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
602 lines
23 KiB
JavaScript
602 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
|
/* Finds lowering state whose result depends on the order reachable bodies
|
|
* lower in.
|
|
*
|
|
* Each program is lowered to IR in discovery order and with every seed of
|
|
* SCRIPTC_LOWER_SHUFFLE_SEED (lowerer.ts), and the IR is compared function
|
|
* by function. Output assembly does not depend on the order, so every
|
|
* difference is state one body's lowering leaves for another. The
|
|
* discovery-order run also audits the lowerer's Map and Set fields: a key
|
|
* that two bodies touch, at least one of them writing it, makes the field
|
|
* order-sensitive for both bodies. A function whose IR differs between
|
|
* orders is attributed to the order-sensitive fields of the body that
|
|
* produced it.
|
|
*
|
|
* node scripts/lowering-order.mjs [--stride 7] [--offset 0] [--limit N]
|
|
* [--seeds 1,2,3] [--jobs 8] [--entry <file.ts>]... [--out <dir>]
|
|
* [--fail-on names|content]
|
|
*
|
|
* Needs a built compiler (pnpm build). Writes <out>/results.json and prints
|
|
* a ranking of fields by the programs whose IR they change. With --fail-on,
|
|
* it exits with status 1 when some program's IR differs between orders:
|
|
* in any way (names), or beyond names and ids (content). */
|
|
import { spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import { cpus, tmpdir } from "node:os";
|
|
import { basename, join, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
const dist = join(root, "packages/compiler/dist");
|
|
|
|
if (process.argv[2] === "--child") {
|
|
const [, , , entry, irPath, seed, auditPath, snapshotPath] = process.argv;
|
|
if (seed !== "-") process.env["SCRIPTC_LOWER_SHUFFLE_SEED"] = seed;
|
|
const module = (path) => import(pathToFileURL(join(dist, path)).href);
|
|
const audit =
|
|
auditPath === "-"
|
|
? null
|
|
: installAudit(
|
|
await module("frontend/lowering/lowerer.js"),
|
|
await module("frontend/lowering/class-dynamic-dispatch.js"),
|
|
await module("frontend/lowering/deferred-module-initializers.js"),
|
|
);
|
|
const snapshots = installSnapshot(await module("frontend/lowering/lowerer.js"));
|
|
const { compile } = await import(pathToFileURL(join(dist, "index.js")).href);
|
|
const outDir = join(tmpdir(), `scriptc-lowering-order-${process.pid}`);
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: irPath,
|
|
backend: "llvm",
|
|
outputKind: "ir",
|
|
});
|
|
if (audit !== null) writeFileSync(auditPath, JSON.stringify(audit.report()));
|
|
writeFileSync(snapshotPath, JSON.stringify(snapshots.at(-1) ?? {}));
|
|
process.exit(result.ok ? 0 : 3);
|
|
}
|
|
|
|
/** Records the contents of the lowerer's Map and Set fields once reachable
|
|
* emit finishes, under keys that do not depend on the process: syntax
|
|
* nodes by file and span, symbols by name and declaration, IR values by id
|
|
* or name. Values are digested to a bounded depth. */
|
|
function installSnapshot({ Lowerer }) {
|
|
const snapshots = [];
|
|
const nodeKey = (node) => {
|
|
try {
|
|
return `node:${node.getSourceFile().fileName}:${node.kind}:${node.pos}:${node.end}`;
|
|
} catch {
|
|
return `node:?:${node.kind}:${node.pos}:${node.end}`;
|
|
}
|
|
};
|
|
const isNode = (value) =>
|
|
typeof value.kind === "number" &&
|
|
typeof value.pos === "number" &&
|
|
typeof value.end === "number" &&
|
|
typeof value.getSourceFile === "function";
|
|
const isSymbol = (value) =>
|
|
typeof value.escapedName === "string" && Array.isArray(value.declarationHandles);
|
|
const symbolKey = (symbol) => {
|
|
let declaration = "";
|
|
try {
|
|
const first = symbol.declarations[0];
|
|
if (first !== undefined) declaration = nodeKey(first);
|
|
} catch {}
|
|
return `symbol:${symbol.escapedName}:${declaration}`;
|
|
};
|
|
const key = (value) => {
|
|
if (value === null || (typeof value !== "object" && typeof value !== "function"))
|
|
return `${typeof value}:${String(value)}`;
|
|
if (isNode(value)) return nodeKey(value);
|
|
if (isSymbol(value)) return symbolKey(value);
|
|
for (const field of ["name", "id", "localId"])
|
|
if (typeof value[field] === "string" || typeof value[field] === "number")
|
|
return `${value.constructor?.name}:${field}:${value[field]}`;
|
|
if (value.def && typeof value.def.name === "string")
|
|
return `${value.constructor?.name}:def:${value.def.name}`;
|
|
return null;
|
|
};
|
|
const digest = (value, depth) => {
|
|
if (value === null || value === undefined || typeof value !== "object")
|
|
return typeof value === "function" ? "fn" : (JSON.stringify(value) ?? "undefined");
|
|
const stable = key(value);
|
|
if (depth === 0) return stable ?? `<${value.constructor?.name}>`;
|
|
if (isNode(value) || isSymbol(value)) return stable;
|
|
if (Array.isArray(value)) return `[${value.map((item) => digest(item, depth - 1)).join(",")}]`;
|
|
if (value instanceof Map)
|
|
return `Map{${[...value]
|
|
.map(([k, v]) => `${key(k) ?? "?"}=>${digest(v, depth - 1)}`)
|
|
.sort()
|
|
.join(",")}}`;
|
|
if (value instanceof Set)
|
|
return `Set{${[...value]
|
|
.map((item) => key(item) ?? digest(item, depth - 1))
|
|
.sort()
|
|
.join(",")}}`;
|
|
const fields = Object.keys(value).sort();
|
|
return `${value.constructor?.name ?? "Object"}{${fields.map((field) => `${field}:${digest(value[field], depth - 1)}`).join(",")}}`;
|
|
};
|
|
const emitReachable = Lowerer.prototype.emitReachable;
|
|
Lowerer.prototype.emitReachable = function (...args) {
|
|
const result = emitReachable.apply(this, args);
|
|
const snapshot = {};
|
|
for (const [field, value] of Object.entries(this)) {
|
|
if (!(value instanceof Map) && !(value instanceof Set)) continue;
|
|
const entries = {};
|
|
const unkeyed = [];
|
|
for (const item of value instanceof Map ? value : [...value].map((v) => [v, true])) {
|
|
const [k, v] = item;
|
|
const stable = key(k);
|
|
const text = digest(v, 3);
|
|
if (stable === null) unkeyed.push(text);
|
|
else entries[stable] = text;
|
|
}
|
|
snapshot[field] = { entries, unkeyed: unkeyed.sort() };
|
|
}
|
|
snapshots.push(snapshot);
|
|
return result;
|
|
};
|
|
return snapshots;
|
|
}
|
|
|
|
/** Wraps the lowering methods that produce bodies and the lowerer's Map and
|
|
* Set fields; records which bodies touch which keys. */
|
|
function installAudit({ Lowerer }, { ClassDynamicDispatch }, { DeferredModuleInitializers }) {
|
|
const proto = Lowerer.prototype;
|
|
const tokens = [];
|
|
const stack = [];
|
|
const fields = new Map();
|
|
const objectIds = new Map();
|
|
const keyId = (key) => {
|
|
if (key === null || (typeof key !== "object" && typeof key !== "function"))
|
|
return `${typeof key}:${String(key)}`;
|
|
let id = objectIds.get(key);
|
|
if (id === undefined) objectIds.set(key, (id = objectIds.size));
|
|
return `object:${id}`;
|
|
};
|
|
const current = () => (stack.length === 0 ? null : stack[stack.length - 1]);
|
|
const touch = (field, key, write) => {
|
|
const token = current();
|
|
if (token === null) return;
|
|
let keys = fields.get(field);
|
|
if (keys === undefined) fields.set(field, (keys = new Map()));
|
|
const id = key === ITERATE ? "*" : keyId(key);
|
|
let entry = keys.get(id);
|
|
if (entry === undefined) keys.set(id, (entry = { readers: new Set(), writers: new Set() }));
|
|
(write ? entry.writers : entry.readers).add(token.id);
|
|
};
|
|
const ITERATE = Symbol("iterate");
|
|
const instrument = (field, value) => {
|
|
const read = (key) => touch(field, key, false);
|
|
const write = (key) => {
|
|
touch(field, key, true);
|
|
// A write changes what every iteration of the field observes.
|
|
touch(field, ITERATE, true);
|
|
};
|
|
const iterate = () => touch(field, ITERATE, false);
|
|
const base = value instanceof Map ? Map.prototype : Set.prototype;
|
|
for (const name of ["get", "has"]) {
|
|
if (!(name in base)) continue;
|
|
const original = base[name];
|
|
value[name] = function (key) {
|
|
read(key);
|
|
return original.call(this, key);
|
|
};
|
|
}
|
|
for (const name of ["set", "add", "delete"]) {
|
|
if (!(name in base)) continue;
|
|
const original = base[name];
|
|
value[name] = function (key, ...rest) {
|
|
write(key);
|
|
return original.call(this, key, ...rest);
|
|
};
|
|
}
|
|
for (const name of ["forEach", "keys", "values", "entries", Symbol.iterator]) {
|
|
const original = base[name];
|
|
value[name] = function (...args) {
|
|
iterate();
|
|
return original.apply(this, args);
|
|
};
|
|
}
|
|
const size = Object.getOwnPropertyDescriptor(base, "size").get;
|
|
Object.defineProperty(value, "size", {
|
|
get() {
|
|
iterate();
|
|
return size.call(this);
|
|
},
|
|
});
|
|
};
|
|
const emitReachable = proto.emitReachable;
|
|
proto.emitReachable = function (...args) {
|
|
for (const [field, value] of Object.entries(this))
|
|
if (value instanceof Map || value instanceof Set) instrument(field, value);
|
|
return emitReachable.apply(this, args);
|
|
};
|
|
const produced = (result, into) => {
|
|
if (result === null || result === undefined) return;
|
|
if (Array.isArray(result)) for (const fn of result) produced(fn, into);
|
|
else if (typeof result === "object" && typeof result.name === "string") into.add(result.name);
|
|
};
|
|
// Passes over the lowered bodies after the worklist: the functions they
|
|
// create belong to the pass.
|
|
const passes = new Map();
|
|
const wrapPass = (owner, label, method) => {
|
|
const original = owner.prototype[method];
|
|
owner.prototype[method] = function (lowerer, ...rest) {
|
|
const implicit = lowerer.implicitFns?.length ?? 0;
|
|
const lifted = lowerer.liftedFns?.length ?? 0;
|
|
try {
|
|
return original.call(this, lowerer, ...rest);
|
|
} finally {
|
|
let names = passes.get(label);
|
|
if (names === undefined) passes.set(label, (names = new Set()));
|
|
for (const fn of (lowerer.implicitFns ?? []).slice(implicit)) names.add(fn.name);
|
|
for (const fn of (lowerer.liftedFns ?? []).slice(lifted)) names.add(fn.name);
|
|
}
|
|
};
|
|
};
|
|
wrapPass(ClassDynamicDispatch, "ClassDynamicDispatch.process", "process");
|
|
wrapPass(ClassDynamicDispatch, "ClassDynamicDispatch.finalize", "finalize");
|
|
wrapPass(DeferredModuleInitializers, "DeferredModuleInitializers.process", "process");
|
|
for (const name of [
|
|
"lowerFunction",
|
|
"lowerClassCtor",
|
|
"lowerClassMethodMember",
|
|
"lowerClassMembers",
|
|
"lowerFileInit",
|
|
"lowerGenericInstance",
|
|
"throwingSetterFn",
|
|
]) {
|
|
const original = proto[name];
|
|
if (typeof original !== "function") continue;
|
|
proto[name] = function (...args) {
|
|
const token = { id: tokens.length, names: new Set() };
|
|
tokens.push(token);
|
|
const implicit = this.implicitFns?.length ?? 0;
|
|
const lifted = this.liftedFns?.length ?? 0;
|
|
stack.push(token);
|
|
try {
|
|
const result = original.apply(this, args);
|
|
produced(result, token.names);
|
|
return result;
|
|
} finally {
|
|
stack.pop();
|
|
for (const fn of (this.implicitFns ?? []).slice(implicit)) token.names.add(fn.name);
|
|
for (const fn of (this.liftedFns ?? []).slice(lifted)) token.names.add(fn.name);
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
report() {
|
|
const out = {};
|
|
for (const [field, keys] of fields) {
|
|
const involved = new Set();
|
|
let sensitive = 0;
|
|
for (const { readers, writers } of keys.values()) {
|
|
const all = new Set([...readers, ...writers]);
|
|
if (writers.size === 0 || all.size < 2) continue;
|
|
sensitive++;
|
|
for (const id of all) for (const fn of tokens[id].names) involved.add(fn);
|
|
}
|
|
if (sensitive > 0) out[field] = { sensitiveKeys: sensitive, functions: [...involved] };
|
|
}
|
|
for (const [label, names] of passes)
|
|
if (names.size > 0) out[`pass ${label}`] = { sensitiveKeys: 0, functions: [...names] };
|
|
return out;
|
|
},
|
|
};
|
|
}
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
stride: { type: "string", default: "7" },
|
|
offset: { type: "string", default: "0" },
|
|
limit: { type: "string", default: "100000" },
|
|
seeds: { type: "string", default: "1,2,3" },
|
|
jobs: { type: "string", default: String(Math.max(1, Math.floor(cpus().length / 2))) },
|
|
entry: { type: "string", multiple: true, default: [] },
|
|
out: { type: "string", default: join(tmpdir(), "scriptc-lowering-order") },
|
|
"no-audit": { type: "boolean", default: false },
|
|
"fail-on": { type: "string" },
|
|
},
|
|
});
|
|
if (values["fail-on"] !== undefined && !["names", "content"].includes(values["fail-on"]))
|
|
throw new Error(`--fail-on takes names or content, not ${values["fail-on"]}`);
|
|
const out = resolve(values.out);
|
|
mkdirSync(out, { recursive: true });
|
|
const seeds = values.seeds.split(",").filter(Boolean);
|
|
const corpus = join(root, "tests/corpus");
|
|
const programs = [
|
|
...readdirSync(corpus)
|
|
.filter((name) => /^\d+-.*\.ts$/.test(name))
|
|
.filter((name) => Number.parseInt(name, 10) % Number(values.stride) === Number(values.offset))
|
|
.sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10))
|
|
.slice(0, Number(values.limit))
|
|
.map((name) => join(corpus, name)),
|
|
...values.entry.map((entry) => resolve(entry)),
|
|
];
|
|
|
|
const child = (entry, irPath, seed, auditPath, snapshotPath) =>
|
|
new Promise((done) => {
|
|
const proc = spawn(
|
|
process.execPath,
|
|
[
|
|
"--max-old-space-size=8192",
|
|
fileURLToPath(import.meta.url),
|
|
"--child",
|
|
entry,
|
|
irPath,
|
|
seed,
|
|
auditPath,
|
|
snapshotPath,
|
|
],
|
|
{ stdio: ["ignore", "ignore", "pipe"], env: { ...process.env, SCRIPTC_NO_CACHE: "1" } },
|
|
);
|
|
let stderr = "";
|
|
proc.stderr.on("data", (chunk) => (stderr += chunk));
|
|
proc.on("close", (code) => done({ ok: code === 0, stderr: stderr.slice(-2000) }));
|
|
});
|
|
|
|
const loadIr = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
const firstDiff = (x, y, path = "") => {
|
|
if (JSON.stringify(x) === JSON.stringify(y)) return null;
|
|
if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return path;
|
|
for (const key of new Set([...Object.keys(x), ...Object.keys(y)])) {
|
|
const found = firstDiff(x[key], y[key], `${path}/${key}`);
|
|
if (found !== null) return found;
|
|
}
|
|
return path;
|
|
};
|
|
const differingFunctions = (a, b) => {
|
|
const left = new Map(a.functions.map((fn) => [fn.name, fn]));
|
|
const right = new Map(b.functions.map((fn) => [fn.name, fn]));
|
|
const names = new Set();
|
|
const paths = {};
|
|
for (const [name, fn] of left) {
|
|
const other = right.get(name);
|
|
if (other === undefined) names.add(name);
|
|
else if (JSON.stringify(other) !== JSON.stringify(fn)) {
|
|
names.add(name);
|
|
paths[name] = firstDiff(fn, other).replace(/\/\d+/g, "/#");
|
|
}
|
|
}
|
|
for (const name of right.keys()) if (!left.has(name)) names.add(name);
|
|
const topLevel = [];
|
|
for (const key of new Set([...Object.keys(a), ...Object.keys(b)]))
|
|
if (key !== "functions" && JSON.stringify(a[key]) !== JSON.stringify(b[key]))
|
|
topLevel.push(key);
|
|
if (a.functions.map((fn) => fn.name).join("\n") !== b.functions.map((fn) => fn.name).join("\n"))
|
|
topLevel.push("function order");
|
|
return { functions: [...names], topLevel, paths };
|
|
};
|
|
|
|
/** Keys whose strings name functions, locals, globals, shapes, unions,
|
|
* classes and labels. Lowering numbers these in creation order; every other
|
|
* string (literal values, field names, library functions) is compared as
|
|
* is. A `name` names a function, record or union when it is one of the
|
|
* module's numbered definitions or starts with `%`. */
|
|
const ID_KEYS = new Set([
|
|
"base",
|
|
"callee",
|
|
"captures",
|
|
"catchLocalId",
|
|
"className",
|
|
"fnName",
|
|
"fromId",
|
|
"globalId",
|
|
"id",
|
|
"int32Locals",
|
|
"int32Params",
|
|
"label",
|
|
"labels",
|
|
"localId",
|
|
"retag",
|
|
"shapeId",
|
|
"targets",
|
|
"unionId",
|
|
]);
|
|
|
|
/** The module with every numbered function, record and union renamed by a
|
|
* hash of its own definition (ids with their digits stripped), other
|
|
* numbered ids by first use within their function or table entry, source
|
|
* locations dropped, and tables sorted. Orders whose canonical modules
|
|
* match differ only in names and ids numbered in creation order. */
|
|
const canonicalModule = (mod) => {
|
|
const hash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 12);
|
|
const m = JSON.parse(JSON.stringify(mod, (k, v) => (k === "loc" ? undefined : v)));
|
|
const definitions = new Set([
|
|
...(m.functions ?? []).map((fn) => fn.name),
|
|
...["records", "unions"].flatMap((key) => (m[key] ?? []).map((item) => item.id)),
|
|
]);
|
|
const isId = (key, value) =>
|
|
ID_KEYS.has(key) || (key === "name" && (definitions.has(value) || value.startsWith("%")));
|
|
/** `v` with the strings under id keys passed through `id`. */
|
|
const mapIds = (v, id, key) => {
|
|
if (typeof v === "string") return isId(key, v) ? id(v) : v;
|
|
if (Array.isArray(v)) return v.map((x) => mapIds(x, id, key));
|
|
if (v && typeof v === "object")
|
|
return Object.fromEntries(Object.keys(v).map((k) => [k, mapIds(v[k], id, k)]));
|
|
return v;
|
|
};
|
|
const strip = (v) => JSON.stringify(mapIds(v, (id) => id.replace(/\d+/g, "#")));
|
|
const rename = new Map();
|
|
for (const fn of m.functions ?? []) {
|
|
if (!/\d/.test(fn.name)) continue;
|
|
const { name, ...rest } = fn;
|
|
rename.set(name, `${name.replace(/\d+/g, "#")}~${hash(strip(rest))}`);
|
|
}
|
|
for (const key of ["records", "unions"])
|
|
for (const item of m[key] ?? []) {
|
|
const { id, ...rest } = item;
|
|
if (typeof id === "string" && /\d/.test(id))
|
|
rename.set(id, `${id.replace(/\d+/g, "#")}~${hash(strip(rest))}`);
|
|
}
|
|
const canonical = (item) => {
|
|
const scope = new Map();
|
|
return mapIds(item, (value) => {
|
|
const renamed = rename.get(value);
|
|
if (renamed !== undefined) return renamed;
|
|
if (!/\d/.test(value)) return value;
|
|
let id = scope.get(value);
|
|
if (id === undefined) scope.set(value, (id = scope.size));
|
|
return `${value.replace(/\d+/g, "#")}@${id}`;
|
|
});
|
|
};
|
|
const out = {};
|
|
for (const key of Object.keys(m))
|
|
out[key] = Array.isArray(m[key])
|
|
? m[key].map((item) => JSON.stringify(canonical(item))).sort()
|
|
: JSON.stringify(canonical(m[key]));
|
|
return out;
|
|
};
|
|
/** Canonical entries present in one order only, by table. */
|
|
const contentDifferences = (a, b) => {
|
|
const found = {};
|
|
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
if (JSON.stringify(a[key]) === JSON.stringify(b[key])) continue;
|
|
if (!Array.isArray(a[key]) || !Array.isArray(b[key])) {
|
|
found[key] = ["(differs)"];
|
|
continue;
|
|
}
|
|
const other = new Set(b[key]);
|
|
found[key] = a[key]
|
|
.filter((item) => !other.has(item))
|
|
.map((item) => JSON.parse(item).name ?? JSON.parse(item).id ?? "?");
|
|
}
|
|
return found;
|
|
};
|
|
|
|
/** Fields whose contents after reachable emit differ between two orders. */
|
|
const differingState = (a, b) => {
|
|
const fields = [];
|
|
for (const field of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
const x = a[field] ?? { entries: {}, unkeyed: [] };
|
|
const y = b[field] ?? { entries: {}, unkeyed: [] };
|
|
const keys = new Set([...Object.keys(x.entries), ...Object.keys(y.entries)]);
|
|
let differs = [...keys].some((k) => x.entries[k] !== y.entries[k]);
|
|
if (!differs) differs = JSON.stringify(x.unkeyed) !== JSON.stringify(y.unkeyed);
|
|
if (differs) fields.push(field);
|
|
}
|
|
return fields;
|
|
};
|
|
|
|
const results = [];
|
|
let next = 0;
|
|
await Promise.all(
|
|
Array.from({ length: Number(values.jobs) }, async () => {
|
|
while (next < programs.length) {
|
|
const entry = programs[next++];
|
|
const name = basename(entry).replace(/\.ts$/, "");
|
|
const dir = join(out, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
const audit = values["no-audit"] ? "-" : join(dir, "audit.json");
|
|
const base = await child(
|
|
entry,
|
|
join(dir, "base.ir.json"),
|
|
"-",
|
|
audit,
|
|
join(dir, "base.state.json"),
|
|
);
|
|
if (!base.ok) {
|
|
results.push({ program: name, status: "does not compile" });
|
|
continue;
|
|
}
|
|
const diffs = {};
|
|
const stateDiffs = {};
|
|
let baseCanonical = null;
|
|
for (const seed of seeds) {
|
|
const run = await child(
|
|
entry,
|
|
join(dir, `seed${seed}.ir.json`),
|
|
seed,
|
|
"-",
|
|
join(dir, `seed${seed}.state.json`),
|
|
);
|
|
if (!run.ok) {
|
|
// A program that lowers in discovery order but not in this one
|
|
// differs in content.
|
|
diffs[seed] = { failed: run.stderr, content: { lowering: ["fails in this order"] } };
|
|
continue;
|
|
}
|
|
const diff = differingFunctions(
|
|
loadIr(join(dir, "base.ir.json")),
|
|
loadIr(join(dir, `seed${seed}.ir.json`)),
|
|
);
|
|
const state = differingState(
|
|
loadIr(join(dir, "base.state.json")),
|
|
loadIr(join(dir, `seed${seed}.state.json`)),
|
|
);
|
|
if (state.length > 0) stateDiffs[seed] = state;
|
|
if (diff.functions.length > 0 || diff.topLevel.length > 0) {
|
|
baseCanonical ??= canonicalModule(loadIr(join(dir, "base.ir.json")));
|
|
diff.content = contentDifferences(
|
|
baseCanonical,
|
|
canonicalModule(loadIr(join(dir, `seed${seed}.ir.json`))),
|
|
);
|
|
diffs[seed] = diff;
|
|
}
|
|
}
|
|
const auditReport = audit === "-" ? {} : JSON.parse(readFileSync(audit, "utf8"));
|
|
const changed = new Set(Object.values(diffs).flatMap((diff) => diff.functions ?? []));
|
|
const attribution = {};
|
|
for (const fn of changed) {
|
|
const fields = Object.entries(auditReport)
|
|
.filter(([, entry]) => entry.functions.includes(fn))
|
|
.map(([field]) => field);
|
|
attribution[fn] = fields;
|
|
}
|
|
results.push({
|
|
program: name,
|
|
status:
|
|
Object.keys(diffs).length === 0
|
|
? "order-independent"
|
|
: Object.values(diffs).some((diff) => Object.keys(diff.content ?? {}).length > 0)
|
|
? "order-dependent content"
|
|
: "order-dependent names",
|
|
diffs,
|
|
attribution,
|
|
sensitiveFields: Object.keys(auditReport),
|
|
stateFields: [...new Set(Object.values(stateDiffs).flat())],
|
|
});
|
|
process.stderr.write(
|
|
`${results.length}/${programs.length} ${name}: ${results.at(-1).status}\n`,
|
|
);
|
|
}
|
|
}),
|
|
);
|
|
writeFileSync(join(out, "results.json"), JSON.stringify(results, null, 2));
|
|
|
|
const dependent = results.filter((r) => r.status.startsWith("order-dependent"));
|
|
const content = results.filter((r) => r.status === "order-dependent content");
|
|
const byField = new Map();
|
|
let unattributed = 0;
|
|
for (const r of dependent) {
|
|
const fields = new Set(Object.values(r.attribution).flat());
|
|
if (fields.size === 0) unattributed++;
|
|
for (const field of fields) byField.set(field, (byField.get(field) ?? 0) + 1);
|
|
}
|
|
console.log(
|
|
`programs ${results.length}: ${dependent.length} order-dependent (${content.length} beyond names and ids), ${results.filter((r) => r.status === "does not compile").length} do not compile`,
|
|
);
|
|
for (const r of content)
|
|
console.log(
|
|
` ${r.program}: ${Object.values(r.diffs)
|
|
.flatMap((diff) =>
|
|
Object.entries(diff.content ?? {}).map(
|
|
([table, items]) => `${table}: ${items.slice(0, 4).join(", ")}`,
|
|
),
|
|
)
|
|
.slice(0, 3)
|
|
.join("; ")}`,
|
|
);
|
|
for (const [field, count] of [...byField].sort((a, b) => b[1] - a[1]))
|
|
console.log(`${String(count).padStart(5)} ${field}`);
|
|
console.log(`${String(unattributed).padStart(5)} (no audited field)`);
|
|
const failing =
|
|
values["fail-on"] === "names" ? dependent : values["fail-on"] === "content" ? content : [];
|
|
if (failing.length > 0) {
|
|
console.log(`${failing.length} programs lower to different IR in another order`);
|
|
process.exitCode = 1;
|
|
}
|