From cda4cac27ff065eb68ce4bf5e4b8c2c74eeb3346 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 21:29:59 +0000 Subject: [PATCH] perf(cfg): bound the reaching-defs fixpoint with a per-block visit ceiling (#2195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-language verification swarm reproduced, AT PRODUCTION DEFAULTS, a reaching-defs blow-up: a machine-generated ~2000-line all-loops function (under DEFAULT_PDG_MAX_FUNCTION_LINES) reaches ~10k basic blocks because loops emit ~5 blocks/line, and the dataflow fixpoint is O(blocks^2.3) on deep loop nests — measured 62s (C/C++) and 2.05s + 810MB (Go) for ONE function. maxFacts does not help: the fact count stays LINEAR, so it never fires. Iterative reaching-defs on a reducible CFG converges in O(loop-nesting-depth) passes, so a worklist re-visits each block a small multiple of times for real code. Add a maxBlockVisits ceiling (emit passes blocks.length × 64 — far beyond any hand-written nesting depth, ~15) that bails when the fixpoint has not converged. An unconverged fixpoint's in/out sets are not sound, so it returns NO facts (status 'truncated', like the existing 'overflow' guard) — a per- function coverage gap, never wrong facts. Real code is byte-identical: full cfg suites 725 passed, bench --check fingerprints unchanged. NOTE: computeControlDependence's O(N²) up-walk on deep post-dom chains is the sibling concern but stays ~13ms in production (bounded by the line cap + the CDG materialization cap); a CDG work-budget is a documented follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/src/core/ingestion/cfg/emit.ts | 20 +++++++++++++- .../src/core/ingestion/cfg/reaching-defs.ts | 26 +++++++++++++++++++ gitnexus/test/unit/cfg/reaching-defs.test.ts | 24 +++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/cfg/emit.ts b/gitnexus/src/core/ingestion/cfg/emit.ts index 0b5b6d130..2e4dcd8cd 100644 --- a/gitnexus/src/core/ingestion/cfg/emit.ts +++ b/gitnexus/src/core/ingestion/cfg/emit.ts @@ -95,6 +95,21 @@ export const REACHING_DEF_FACTS_PER_EDGE_CAP = 4; export const DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION = REACHING_DEF_FACTS_PER_EDGE_CAP * DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION; +/** + * Fixpoint-iteration budget for {@link computeReachingDefs}, as a multiple of + * the function's block count ({@link emitFileReachingDefs} passes + * `blocks.length × this` as `maxBlockVisits`). Iterative reaching-defs on a + * reducible CFG converges in O(loop-nesting-depth) passes, so a worklist + * re-visits each block a small multiple of times for real code; this budget + * tolerates a nesting depth far beyond any hand-written function (real code is + * ≤ ~15 deep) while truncating the pathological deep nest that otherwise drives + * the solver to O(blocks²) — measured at seconds + GB on a machine-generated + * 2000-line all-loops function whose fact count stays linear (so `maxFacts` + * never fires). Truncation degrades to a sound empty REACHING_DEF for that one + * function (status `truncated`), never wrong facts. + */ +export const DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS = 64; + export interface CfgEmitResult { blocks: number; edges: number; @@ -359,7 +374,10 @@ export function emitFileReachingDefs( ); continue; } - const r = computeReachingDefs(cfg, { maxFacts }); + const r = computeReachingDefs(cfg, { + maxFacts, + maxBlockVisits: cfg.blocks.length * DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS, + }); if (r.status === 'no-facts') continue; result.facts += r.facts.length; diff --git a/gitnexus/src/core/ingestion/cfg/reaching-defs.ts b/gitnexus/src/core/ingestion/cfg/reaching-defs.ts index 7b7f7233c..a0e617b9d 100644 --- a/gitnexus/src/core/ingestion/cfg/reaching-defs.ts +++ b/gitnexus/src/core/ingestion/cfg/reaching-defs.ts @@ -67,6 +67,19 @@ export interface ReachingDefsLimits { * `status: 'truncated'`. `undefined`/0 ⇒ unlimited. */ readonly maxFacts?: number; + /** + * Maximum total block dequeues in the dataflow fixpoint. Iterative + * reaching-defs on a reducible CFG converges in O(loop-nesting-depth) passes, + * so a worklist visits each block a small multiple of times for real code; a + * pathologically deep loop nest (machine-generated / obfuscated) drives the + * pass count — and thus the visit total — to O(blocks²) and the solver to + * seconds + GB of heap (`maxFacts` does not help: fact count stays linear). + * When the visit total exceeds this budget the fixpoint has NOT converged, so + * any facts would be unsound — the solver bails to a sound empty + * `status: 'truncated'` (like the `overflow` guard). `undefined`/0 ⇒ unlimited + * (the default for direct callers; the emit path sets a per-function budget). + */ + readonly maxBlockVisits?: number; } export interface FunctionDefUse { @@ -222,11 +235,24 @@ export function computeReachingDefs(cfg: FunctionCfg, limits?: ReachingDefsLimit const inWorklist = new Array(n).fill(true); let pending = n; + // Fixpoint-iteration ceiling (see ReachingDefsLimits.maxBlockVisits): bound the + // total block dequeues so a pathologically deep loop nest can't drive the + // worklist to O(blocks²). undefined/0 ⇒ unlimited. + const maxBlockVisits = + limits?.maxBlockVisits && limits.maxBlockVisits > 0 ? limits.maxBlockVisits : Infinity; + let blockVisits = 0; while (pending > 0) { for (const b of order) { if (!inWorklist[b]) continue; inWorklist[b] = false; pending -= 1; + if (++blockVisits > maxBlockVisits) { + // Did NOT converge within the budget — the in/out sets are not at the + // fixpoint, so any facts would be unsound. Bail to a sound empty + // `truncated` result (a coverage gap, not an error), carrying the def/use + // telemetry already gathered. + return { status: 'truncated', bindings: cfg.bindings, facts: [], defCount, useCount }; + } const p = preds[b]; const inB: Lattice = diff --git a/gitnexus/test/unit/cfg/reaching-defs.test.ts b/gitnexus/test/unit/cfg/reaching-defs.test.ts index 01dc598ed..244bc8f58 100644 --- a/gitnexus/test/unit/cfg/reaching-defs.test.ts +++ b/gitnexus/test/unit/cfg/reaching-defs.test.ts @@ -347,6 +347,30 @@ describe('computeReachingDefs — determinism and convergence', () => { expect(capped.defCount).toBe(full.defCount); expect(capped.useCount).toBe(full.useCount); }); + + it('maxBlockVisits ceiling: a budget below convergence bails to a sound empty truncated', () => { + // entry → body (self-loop, forces re-processing) → exit; body defs+uses x. + const blocks: BlockSpec[] = [{}, {}, { stmts: [stmt(3, [0], [0])] }]; + const edges: [number, number][] = [ + [0, 2], + [2, 2], // self-loop → the fixpoint re-visits block 2 + [2, 1], + ]; + // Unbounded (and a generous budget) converge with the loop-carried fact. + const full = computeReachingDefs(mkCfg(blocks, edges, ['x'])); + expect(full.status).toBe('computed'); + expect(full.facts.length).toBeGreaterThan(0); + const budgeted = computeReachingDefs(mkCfg(blocks, edges, ['x']), { maxBlockVisits: 1000 }); + expect(budgeted.status).toBe('computed'); + expect(render(budgeted.facts)).toEqual(render(full.facts)); // byte-identical for normal code + + // A budget below convergence cannot reach the fixpoint, so facts would be + // unsound → return NONE (sound), status 'truncated', telemetry preserved. + const capped = computeReachingDefs(mkCfg(blocks, edges, ['x']), { maxBlockVisits: 1 }); + expect(capped.status).toBe('truncated'); + expect(capped.facts).toEqual([]); + expect(capped.defCount).toBe(full.defCount); + }); }); describe('computeReachingDefs — parser-direct acceptance (with U1/U2)', () => {