mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
test(cfg): retain dense reaching-defs as differential oracle + fuzz harness (#2201 U1)
This commit is contained in:
parent
6932e7a9fd
commit
e31faa8f4f
2 changed files with 511 additions and 0 deletions
|
|
@ -127,8 +127,32 @@ const EMPTY_LATTICE: Lattice = new Map();
|
|||
/**
|
||||
* Compute reaching definitions for one function. See the module doc for the
|
||||
* purity/determinism/sharing contract.
|
||||
*
|
||||
* This is the production entry point. As of #2201 it delegates to the
|
||||
* SSA-sparse solver ({@link computeReachingDefsSparse}); the dense GEN/KILL
|
||||
* worklist ({@link computeReachingDefsDense}) is retained as the differential
|
||||
* equivalence oracle the fuzz suite checks the sparse path against — the two
|
||||
* MUST be byte-identical (status, bindings, sorted facts, def/use telemetry).
|
||||
*/
|
||||
export function computeReachingDefs(cfg: FunctionCfg, limits?: ReachingDefsLimits): FunctionDefUse {
|
||||
// #2201 U1: production still runs the dense solver; the swap to sparse lands
|
||||
// in U5 once the differential fuzz is byte-identical green.
|
||||
return computeReachingDefsDense(cfg, limits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dense GEN/KILL monotone worklist — the original (#2082 M2) reaching-defs
|
||||
* solver. As of #2201 this is RETAINED AS A TEST/BENCH-ONLY DIFFERENTIAL
|
||||
* ORACLE, not a production code path: {@link computeReachingDefs} runs the
|
||||
* SSA-sparse solver, and the equivalence fuzz asserts the two are byte-identical
|
||||
* across a random-CFG corpus. Keep it behavior-frozen — it is the ground truth.
|
||||
*
|
||||
* @internal exported only for the equivalence fuzz harness and the cfg bench.
|
||||
*/
|
||||
export function computeReachingDefsDense(
|
||||
cfg: FunctionCfg,
|
||||
limits?: ReachingDefsLimits,
|
||||
): FunctionDefUse {
|
||||
if (!cfg.bindings) {
|
||||
return { status: 'no-facts', bindings: [], facts: [], defCount: 0, useCount: 0 };
|
||||
}
|
||||
|
|
|
|||
487
gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts
Normal file
487
gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
/**
|
||||
* #2201 — differential equivalence harness for the reaching-defs solvers.
|
||||
*
|
||||
* The SSA-sparse rewrite must be BYTE-IDENTICAL to the retained dense GEN/KILL
|
||||
* oracle ({@link computeReachingDefsDense}). This file is the permanent gate:
|
||||
* a seeded random-CFG generator drives both solvers and a structural comparator
|
||||
* asserts identical status / bindings / sorted facts / def-use telemetry.
|
||||
*
|
||||
* In U1 both sides run the dense oracle (self-equivalence + corpus-coverage
|
||||
* sanity); U5 flips the second solver to {@link computeReachingDefs} (sparse)
|
||||
* — the single change that turns this into the real equivalence gate.
|
||||
*
|
||||
* The corpus deliberately covers the shapes where a may-reaching-defs rewrite
|
||||
* is most likely to diverge: loops + irreducible (goto) topology, throw edges
|
||||
* (IN∪allDefs handler semantics), may-defs (gen-without-kill), shadowed
|
||||
* bindings, unreachable blocks, multi-predecessor joins, and the maxFacts /
|
||||
* maxBlockVisits truncation postures (KTD6 — the truncated SUBSET depends on
|
||||
* pre-sort emission order, so it must match too).
|
||||
*
|
||||
* Default corpus is CI-fast; GITNEXUS_RD_FUZZ_N raises it (the ≥1M run the
|
||||
* plan calls for) for a deep local/CI-shard pass.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
computeReachingDefs,
|
||||
computeReachingDefsDense,
|
||||
type FunctionDefUse,
|
||||
type ReachingDefsLimits,
|
||||
} from '../../../src/core/ingestion/cfg/reaching-defs.js';
|
||||
import type {
|
||||
BindingEntry,
|
||||
BasicBlockData,
|
||||
CfgEdgeData,
|
||||
CfgEdgeKind,
|
||||
FunctionCfg,
|
||||
StatementFacts,
|
||||
} from '../../../src/core/ingestion/cfg/types.js';
|
||||
|
||||
type Solver = (cfg: FunctionCfg, limits?: ReachingDefsLimits) => FunctionDefUse;
|
||||
|
||||
// ── deterministic PRNG (mulberry32) ───────────────────────────────────────
|
||||
function mulberry32(seed: number): () => number {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const NON_THROW_KINDS: CfgEdgeKind[] = [
|
||||
'seq',
|
||||
'cond-true',
|
||||
'cond-false',
|
||||
'loop-back',
|
||||
'break',
|
||||
'continue',
|
||||
'return',
|
||||
'switch-case',
|
||||
'fallthrough',
|
||||
];
|
||||
|
||||
// ── random CFG generator ───────────────────────────────────────────────────
|
||||
interface GenOpts {
|
||||
maxBlocks: number;
|
||||
maxBindings: number;
|
||||
maxStmtsPerBlock: number;
|
||||
pNoBindings: number; // chance the whole CFG has bindings:undefined (→ no-facts)
|
||||
pThrowEdge: number;
|
||||
pMayDef: number;
|
||||
pExtraEdge: number; // per-block chance of an extra random edge
|
||||
pShadowName: number;
|
||||
}
|
||||
|
||||
const DEFAULT_GEN: GenOpts = {
|
||||
maxBlocks: 14,
|
||||
maxBindings: 8,
|
||||
maxStmtsPerBlock: 4,
|
||||
pNoBindings: 0.03,
|
||||
pThrowEdge: 0.12,
|
||||
pMayDef: 0.18,
|
||||
pExtraEdge: 0.9,
|
||||
pShadowName: 0.4,
|
||||
};
|
||||
|
||||
function genCfg(seed: number, opts: GenOpts = DEFAULT_GEN): FunctionCfg {
|
||||
const rnd = mulberry32(seed);
|
||||
const int = (n: number) => Math.floor(rnd() * n);
|
||||
const n = 1 + int(opts.maxBlocks); // ≥1 block (entry)
|
||||
|
||||
// bindings — small name pool so shadowing collisions happen; distinct
|
||||
// declLine/declColumn keep non-synthetic bindings' keys distinct.
|
||||
const noBindings = rnd() < opts.pNoBindings;
|
||||
const nBindings = noBindings ? 0 : int(opts.maxBindings + 1);
|
||||
const namePool = ['a', 'b', 'c', 'd', 'e'];
|
||||
const kinds: BindingEntry['kind'][] = ['var', 'let', 'const', 'param', 'catch'];
|
||||
const bindings: BindingEntry[] = [];
|
||||
for (let i = 0; i < nBindings; i++) {
|
||||
const shadow = rnd() < opts.pShadowName;
|
||||
bindings.push({
|
||||
name: shadow ? namePool[int(namePool.length)] : `v${i}`,
|
||||
declLine: 100 + i,
|
||||
declColumn: i,
|
||||
kind: kinds[int(kinds.length)],
|
||||
...(rnd() < 0.08 ? { synthetic: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const pickBindings = (max: number): number[] => {
|
||||
if (nBindings === 0) return [];
|
||||
const out: number[] = [];
|
||||
const count = int(max + 1);
|
||||
for (let k = 0; k < count; k++) out.push(int(nBindings));
|
||||
return out;
|
||||
};
|
||||
|
||||
// blocks (block 0 = entry; some blocks get no statements like synthetic
|
||||
// ENTRY/EXIT to exercise the skip paths).
|
||||
const blocks: BasicBlockData[] = [];
|
||||
for (let b = 0; b < n; b++) {
|
||||
const stmtCount = b === 0 && rnd() < 0.5 ? int(2) : int(opts.maxStmtsPerBlock + 1);
|
||||
const statements: StatementFacts[] = [];
|
||||
for (let i = 0; i < stmtCount; i++) {
|
||||
const defs = pickBindings(2);
|
||||
const uses = pickBindings(3);
|
||||
const mayDefs = rnd() < opts.pMayDef ? pickBindings(1) : [];
|
||||
statements.push({
|
||||
line: b * 100 + i + 1,
|
||||
defs,
|
||||
uses,
|
||||
...(mayDefs.length ? { mayDefs } : {}),
|
||||
});
|
||||
}
|
||||
blocks.push({
|
||||
index: b,
|
||||
startLine: b * 100,
|
||||
endLine: b * 100 + stmtCount,
|
||||
text: `B${b}`,
|
||||
kind: b === 0 ? 'entry' : b === n - 1 ? 'exit' : 'normal',
|
||||
// bindings:undefined ⇒ no-facts: drop statements entirely so it mirrors a
|
||||
// pre-M2 CFG (the solver keys no-facts off cfg.bindings, but a realistic
|
||||
// no-facts CFG also lacks statements).
|
||||
...(noBindings ? {} : { statements }),
|
||||
});
|
||||
}
|
||||
|
||||
// edges — a probabilistic spine (entry chain) for reachability + random
|
||||
// extra edges that produce loops, irreducible topology, and unreachable
|
||||
// blocks. Throw edges target a random handler block.
|
||||
const edges: CfgEdgeData[] = [];
|
||||
const addEdge = (from: number, to: number, kind: CfgEdgeKind) => {
|
||||
if (from >= 0 && from < n && to >= 0 && to < n) edges.push({ from, to, kind });
|
||||
};
|
||||
for (let b = 0; b < n - 1; b++) {
|
||||
if (rnd() < 0.75) addEdge(b, b + 1, 'seq');
|
||||
}
|
||||
for (let b = 0; b < n; b++) {
|
||||
if (rnd() < opts.pExtraEdge) {
|
||||
const to = int(n); // any target → forward / back / self / cross edges
|
||||
const throwIt = rnd() < opts.pThrowEdge;
|
||||
addEdge(b, to, throwIt ? 'throw' : NON_THROW_KINDS[int(NON_THROW_KINDS.length)]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filePath: 'fuzz.ts',
|
||||
functionStartLine: 1,
|
||||
functionEndLine: n * 100,
|
||||
functionStartColumn: 0,
|
||||
entryIndex: 0,
|
||||
exitIndex: n - 1,
|
||||
blocks,
|
||||
edges,
|
||||
...(noBindings ? {} : { bindings }),
|
||||
};
|
||||
}
|
||||
|
||||
// ── hand-built canonical hard CFGs (guaranteed shape coverage) ─────────────
|
||||
// These pin the gnarly shapes the random generator hits only probabilistically.
|
||||
function canonicalHardCfgs(): FunctionCfg[] {
|
||||
const mk = (
|
||||
blocks: BasicBlockData[],
|
||||
edges: CfgEdgeData[],
|
||||
bindings: BindingEntry[],
|
||||
): FunctionCfg => ({
|
||||
filePath: 'canon.ts',
|
||||
functionStartLine: 1,
|
||||
functionEndLine: 999,
|
||||
functionStartColumn: 0,
|
||||
entryIndex: 0,
|
||||
exitIndex: blocks.length - 1,
|
||||
blocks,
|
||||
edges,
|
||||
bindings,
|
||||
});
|
||||
const bind = (name: string, line: number): BindingEntry => ({
|
||||
name,
|
||||
declLine: line,
|
||||
declColumn: 0,
|
||||
kind: 'let',
|
||||
});
|
||||
const blk = (index: number, statements: StatementFacts[]): BasicBlockData => ({
|
||||
index,
|
||||
startLine: index * 10,
|
||||
endLine: index * 10 + statements.length,
|
||||
text: `B${index}`,
|
||||
kind: index === 0 ? 'entry' : 'normal',
|
||||
statements,
|
||||
});
|
||||
const st = (
|
||||
line: number,
|
||||
defs: number[],
|
||||
uses: number[],
|
||||
mayDefs?: number[],
|
||||
): StatementFacts => ({
|
||||
line,
|
||||
defs,
|
||||
uses,
|
||||
...(mayDefs ? { mayDefs } : {}),
|
||||
});
|
||||
|
||||
const out: FunctionCfg[] = [];
|
||||
|
||||
// (1) Irreducible two-entry loop: 0→1, 0→2, 1→2, 2→1. binding x def in 1, use in 2 & 1.
|
||||
out.push(
|
||||
mk(
|
||||
[blk(0, [st(1, [0], [])]), blk(1, [st(2, [0], [0])]), blk(2, [st(3, [], [0])])],
|
||||
[
|
||||
{ from: 0, to: 1, kind: 'cond-true' },
|
||||
{ from: 0, to: 2, kind: 'cond-false' },
|
||||
{ from: 1, to: 2, kind: 'seq' },
|
||||
{ from: 2, to: 1, kind: 'loop-back' },
|
||||
],
|
||||
[bind('x', 1)],
|
||||
),
|
||||
);
|
||||
|
||||
// (2) Self-loop with may-def: block 1 loops to itself; x may-def + use.
|
||||
out.push(
|
||||
mk(
|
||||
[blk(0, [st(1, [0], [])]), blk(1, [st(2, [], [0], [0])])],
|
||||
[
|
||||
{ from: 0, to: 1, kind: 'seq' },
|
||||
{ from: 1, to: 1, kind: 'loop-back' },
|
||||
],
|
||||
[bind('x', 1)],
|
||||
),
|
||||
);
|
||||
|
||||
// (3) try/catch throw edge: 0 (x=1), 1 (x=parse; x=normalize) -throw-> 2 (use x).
|
||||
out.push(
|
||||
mk(
|
||||
[
|
||||
blk(0, [st(1, [0], [])]),
|
||||
blk(1, [st(2, [0], []), st(3, [0], [0])]),
|
||||
blk(2, [st(4, [], [0])]),
|
||||
],
|
||||
[
|
||||
{ from: 0, to: 1, kind: 'seq' },
|
||||
{ from: 1, to: 2, kind: 'seq' },
|
||||
{ from: 1, to: 2, kind: 'throw' },
|
||||
],
|
||||
[bind('x', 1)],
|
||||
),
|
||||
);
|
||||
|
||||
// (4) Diamond merge: both arm defs reach the join use.
|
||||
out.push(
|
||||
mk(
|
||||
[
|
||||
blk(0, [st(1, [], [])]),
|
||||
blk(1, [st(2, [0], [])]),
|
||||
blk(2, [st(3, [0], [])]),
|
||||
blk(3, [st(4, [], [0])]),
|
||||
],
|
||||
[
|
||||
{ from: 0, to: 1, kind: 'cond-true' },
|
||||
{ from: 0, to: 2, kind: 'cond-false' },
|
||||
{ from: 1, to: 3, kind: 'seq' },
|
||||
{ from: 2, to: 3, kind: 'seq' },
|
||||
],
|
||||
[bind('x', 1)],
|
||||
),
|
||||
);
|
||||
|
||||
// (5) Unreachable block carrying a def (block 2 not reachable from entry).
|
||||
out.push(
|
||||
mk(
|
||||
[blk(0, [st(1, [0], [0])]), blk(1, [st(2, [], [0])]), blk(2, [st(3, [0], [0])])],
|
||||
[{ from: 0, to: 1, kind: 'seq' }],
|
||||
[bind('x', 1)],
|
||||
),
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── structural comparator ──────────────────────────────────────────────────
|
||||
function serializeFact(f: FunctionDefUse['facts'][number]): string {
|
||||
return (
|
||||
`${f.def.blockIndex}:${f.def.stmtIndex}@${f.def.line}` +
|
||||
`->${f.use.blockIndex}:${f.use.stmtIndex}@${f.use.line}#${f.bindingIdx}`
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns null when byte-identical, else a human-readable first divergence. */
|
||||
function diffDefUse(a: FunctionDefUse, b: FunctionDefUse): string | null {
|
||||
if (a.status !== b.status) return `status: ${a.status} vs ${b.status}`;
|
||||
if (a.defCount !== b.defCount) return `defCount: ${a.defCount} vs ${b.defCount}`;
|
||||
if (a.useCount !== b.useCount) return `useCount: ${a.useCount} vs ${b.useCount}`;
|
||||
if (a.bindings.length !== b.bindings.length) {
|
||||
return `bindings.length: ${a.bindings.length} vs ${b.bindings.length}`;
|
||||
}
|
||||
if (a.facts.length !== b.facts.length) {
|
||||
return `facts.length: ${a.facts.length} vs ${b.facts.length}`;
|
||||
}
|
||||
for (let i = 0; i < a.facts.length; i++) {
|
||||
const fa = serializeFact(a.facts[i]);
|
||||
const fb = serializeFact(b.facts[i]);
|
||||
if (fa !== fb) return `fact[${i}]: ${fa} vs ${fb}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── corpus shape classifier (coverage guard) ───────────────────────────────
|
||||
interface ShapeFlags {
|
||||
hasLoop: boolean;
|
||||
hasThrow: boolean;
|
||||
hasMayDef: boolean;
|
||||
hasShadow: boolean;
|
||||
hasMultiPred: boolean;
|
||||
hasUnreachable: boolean;
|
||||
hadComputed: boolean;
|
||||
hadTruncated: boolean;
|
||||
hadNoFacts: boolean;
|
||||
}
|
||||
|
||||
function classify(cfg: FunctionCfg, flags: ShapeFlags): void {
|
||||
const n = cfg.blocks.length;
|
||||
if (cfg.edges.some((e) => e.kind === 'throw')) flags.hasThrow = true;
|
||||
if (cfg.blocks.some((b) => b.statements?.some((s) => s.mayDefs?.length))) flags.hasMayDef = true;
|
||||
if (cfg.bindings) {
|
||||
const names = cfg.bindings.map((b) => b.name);
|
||||
if (new Set(names).size < names.length) flags.hasShadow = true;
|
||||
}
|
||||
const predCount = new Array(n).fill(0);
|
||||
for (const e of cfg.edges) if (e.to >= 0 && e.to < n) predCount[e.to]++;
|
||||
if (predCount.some((c) => c >= 2)) flags.hasMultiPred = true;
|
||||
|
||||
// cycle detection (DFS rec-stack) over the whole graph
|
||||
const succ: number[][] = Array.from({ length: n }, () => []);
|
||||
for (const e of cfg.edges)
|
||||
if (e.from >= 0 && e.from < n && e.to >= 0 && e.to < n) succ[e.from].push(e.to);
|
||||
const color = new Array(n).fill(0); // 0=white 1=gray 2=black
|
||||
const hasCycleFrom = (start: number): boolean => {
|
||||
const stack: { node: number; idx: number }[] = [{ node: start, idx: 0 }];
|
||||
color[start] = 1;
|
||||
while (stack.length) {
|
||||
const top = stack[stack.length - 1];
|
||||
if (top.idx < succ[top.node].length) {
|
||||
const nx = succ[top.node][top.idx++];
|
||||
if (color[nx] === 1) return true;
|
||||
if (color[nx] === 0) {
|
||||
color[nx] = 1;
|
||||
stack.push({ node: nx, idx: 0 });
|
||||
}
|
||||
} else {
|
||||
color[top.node] = 2;
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (let s = 0; s < n; s++) if (color[s] === 0 && hasCycleFrom(s)) flags.hasLoop = true;
|
||||
|
||||
// reachability from entry
|
||||
const seen = new Array(n).fill(false);
|
||||
const q = [cfg.entryIndex];
|
||||
seen[cfg.entryIndex] = true;
|
||||
while (q.length) {
|
||||
const x = q.pop()!;
|
||||
for (const y of succ[x]) if (!seen[y]) ((seen[y] = true), q.push(y));
|
||||
}
|
||||
if (seen.some((v, i) => !v && i < n)) flags.hasUnreachable = true;
|
||||
}
|
||||
|
||||
// ── corpus runner ──────────────────────────────────────────────────────────
|
||||
interface CorpusResult {
|
||||
checked: number;
|
||||
flags: ShapeFlags;
|
||||
firstFailure: string | null;
|
||||
}
|
||||
|
||||
function runCorpus(left: Solver, right: Solver, count: number, baseSeed: number): CorpusResult {
|
||||
const flags: ShapeFlags = {
|
||||
hasLoop: false,
|
||||
hasThrow: false,
|
||||
hasMayDef: false,
|
||||
hasShadow: false,
|
||||
hasMultiPred: false,
|
||||
hasUnreachable: false,
|
||||
hadComputed: false,
|
||||
hadTruncated: false,
|
||||
hadNoFacts: false,
|
||||
};
|
||||
let firstFailure: string | null = null;
|
||||
let checked = 0;
|
||||
|
||||
const check = (cfg: FunctionCfg, limits: ReachingDefsLimits | undefined, label: string): void => {
|
||||
const a = left(cfg, limits);
|
||||
const b = right(cfg, limits);
|
||||
const d = diffDefUse(a, b);
|
||||
checked++;
|
||||
if (a.status === 'computed') flags.hadComputed = true;
|
||||
if (a.status === 'truncated') flags.hadTruncated = true;
|
||||
if (a.status === 'no-facts') flags.hadNoFacts = true;
|
||||
if (d && !firstFailure) firstFailure = `${label}: ${d}`;
|
||||
};
|
||||
|
||||
// canonical hard CFGs first (under several limit postures)
|
||||
for (const [i, cfg] of canonicalHardCfgs().entries()) {
|
||||
classify(cfg, flags);
|
||||
check(cfg, undefined, `canon[${i}]`);
|
||||
check(cfg, { maxFacts: 1 }, `canon[${i}]/maxFacts=1`);
|
||||
check(cfg, { maxFacts: 2 }, `canon[${i}]/maxFacts=2`);
|
||||
check(cfg, { maxBlockVisits: 2 }, `canon[${i}]/maxBlockVisits=2`);
|
||||
}
|
||||
|
||||
// random corpus
|
||||
for (let i = 0; i < count; i++) {
|
||||
const seed = baseSeed + i;
|
||||
const cfg = genCfg(seed);
|
||||
classify(cfg, flags);
|
||||
check(cfg, undefined, `seed=${seed}`);
|
||||
// exercise truncation on ~1/4 of cases (small maxFacts) and the block-visit
|
||||
// ceiling on ~1/8 — both must match byte-for-byte (KTD6).
|
||||
if (i % 4 === 0) check(cfg, { maxFacts: 1 + (i % 3) }, `seed=${seed}/maxFacts`);
|
||||
if (i % 8 === 0) check(cfg, { maxBlockVisits: 1 + (i % 4) }, `seed=${seed}/maxBlockVisits`);
|
||||
}
|
||||
|
||||
return { checked, flags, firstFailure };
|
||||
}
|
||||
|
||||
const CORPUS_N = Number(process.env.GITNEXUS_RD_FUZZ_N ?? 1500);
|
||||
|
||||
describe('#2201 reaching-defs differential equivalence', () => {
|
||||
it('dense oracle is self-consistent and the comparator + generator are sound', () => {
|
||||
// U1 baseline: dense-vs-dense MUST be byte-identical (proves the harness).
|
||||
const r = runCorpus(computeReachingDefsDense, computeReachingDefsDense, CORPUS_N, 0x2201);
|
||||
expect(r.firstFailure).toBeNull();
|
||||
expect(r.checked).toBeGreaterThan(CORPUS_N);
|
||||
});
|
||||
|
||||
it('the corpus exercises every divergence-prone shape (coverage guard)', () => {
|
||||
const r = runCorpus(computeReachingDefsDense, computeReachingDefsDense, CORPUS_N, 0x2201);
|
||||
const f = r.flags;
|
||||
expect(f.hasLoop, 'loops').toBe(true);
|
||||
expect(f.hasThrow, 'throw edges').toBe(true);
|
||||
expect(f.hasMayDef, 'may-defs').toBe(true);
|
||||
expect(f.hasShadow, 'shadowed bindings').toBe(true);
|
||||
expect(f.hasMultiPred, 'multi-pred joins').toBe(true);
|
||||
expect(f.hasUnreachable, 'unreachable blocks').toBe(true);
|
||||
expect(f.hadComputed, 'computed results').toBe(true);
|
||||
expect(f.hadTruncated, 'truncated results').toBe(true);
|
||||
expect(f.hadNoFacts, 'no-facts results').toBe(true);
|
||||
});
|
||||
|
||||
it('is deterministic — a fixed seed yields a byte-identical corpus across runs', () => {
|
||||
const a = runCorpus(computeReachingDefsDense, computeReachingDefsDense, 200, 0xfeed);
|
||||
const b = runCorpus(computeReachingDefsDense, computeReachingDefsDense, 200, 0xfeed);
|
||||
expect(a.checked).toBe(b.checked);
|
||||
expect(a.flags).toEqual(b.flags);
|
||||
});
|
||||
|
||||
it('PRODUCTION computeReachingDefs is byte-identical to the dense oracle', () => {
|
||||
// U1: computeReachingDefs still delegates to dense, so this is trivially
|
||||
// green. U5 swaps it to the sparse solver — this becomes the real gate.
|
||||
const r = runCorpus(computeReachingDefs, computeReachingDefsDense, CORPUS_N, 0x5eed);
|
||||
expect(r.firstFailure).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// Re-exported for U5 and future harness reuse.
|
||||
export { genCfg, canonicalHardCfgs, diffDefUse, runCorpus, classify };
|
||||
export type { Solver, ShapeFlags, CorpusResult };
|
||||
Loading…
Add table
Reference in a new issue