mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* perf(cpp): index ADL candidates once instead of per-site rescans C++ scope-resolution `emit` dominated large-repo analysis (~6.76h on a 5,969-file repo — ~70% of the total run). `pickCppAdlCandidates` ran once per unresolved ADL-eligible call site and each time: - rescanned every parsed file (rebuilding a per-file scope map per call), - scanned every workspace def (`findCppClassDefBySimpleName`), and - used an O(scopes²) child-scope walk for hidden friends. That is O(unresolved sites × files); with hundreds of thousands of unresolved C++ sites the emit phase went super-linear. `resolve` (registry lookup) was only 3.5s — the cost was entirely in fallback edge emission. Build an `AdlCandidateIndex` once per run (lazy, guarded by `parsedFiles` identity, reset in `clearCppAdlState`) and query it per site: - `classDefsBySimple` — preserves `defs.byId` order so first-match / ambiguous semantics are identical to the legacy linear scan. - `nsCandidates` — namespace-owned callables, with inline-namespace transparency. - `friendCandidates` — hidden-friend + class-member callables; a parent→children scope index replaces the O(scopes²) walk. - `nsFunctionsByQName` / `nsFunctionsBySimple` — function-reference ADL path. A monotonic `seqByNodeId` (file-major; namespace defs before friend/member defs within a file) lets the per-site query merge candidates across associated namespaces, dedup by nodeId, and sort — reproducing the exact legacy candidate set and order. Per-site cost drops from O(sites × files) to O(associated namespaces); the emit phase goes from linear-in-sites to flat. Benchmark (files=80): emit at 1000 sites 232ms → 9ms, 2000 sites flat at 17ms; the eliminated term scales with file count, so the speedup is ~1000×+ on the real 5,969-file repo. Behavior is unchanged: synthetic candidate output is byte-identical before/after, all 270 C++ integration resolver tests and 4/4 resolver-parity-expected-failures pass, and tsc + eslint are clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cpp): correct ADL state-lifecycle and cache-guard comments The header lifecycle block listed three module-level maps and named clearFileLocalNames as the reset caller; both became inaccurate when the candidate index was added. Enumerate all five state pieces, name the real caller (loadResolutionConfig), and document that ensureAdlIndex's staleness guard keys on parsedFiles identity while the index also depends on scopes and classToNamespaceQualifiedName. Addresses PR #1990 tri-review (U1, U3). Doc-only; no behavior change. * test(cpp): guard the ADL seq-coverage invariant in dev/test pickCppAdlCandidates sorts merged candidates by seqByNodeId with a `?? 0` fallback. That fallback is unreachable today (every bucketed def is seq-assigned in the same build block), but a future regression could break it and silently collapse two seq-0 candidates, dropping a CALLS edge with no error. Add validateAdlSeqCoverage and run it from buildAdlIndex under the resolver's opt-in validation gate (NODE_ENV!=production && VALIDATE_SEMANTIC_MODEL!=0), so a broken invariant throws loudly in dev/CI instead. Production behavior and the hot path are unchanged. Unit-tested; 270/270 cpp integration tests pass with the guard active. Addresses PR #1990 tri-review (U2). * test(cpp): parity fixture for ADL hidden-friend + namespace-callable merge pickCppAdlCandidates merges friendCandidates (hidden friends of associated classes) and nsCandidates (namespace-owned callables) for a single associated namespace. The byte-identical-parity claim rested only on an uncommitted harness. Add a fixture that reaches one callable through each bucket — combine only via a hidden friend, process only via a namespace member — so dropping either bucket from the merge fails the suite. Candidate order is not observable (narrowing resolves a unique survivor or suppresses), so the guard is on the set. Addresses PR #1990 tri-review (U4). * test(cpp): add ADL emit-scaling benchmark Guards the PR #1990 optimization against reintroducing the O(sites x files) ADL candidate scan. Generates many UNRESOLVED ADL sites (class-typed arg + a callee declared nowhere) and co-scales files and sites with N, so the old cost is O(N^2) and the new cost O(N). Isolates the scope-resolution emit ms from parse-dominated wall time via the logger test destination (capture verified) and asserts the end-to-end emit ratio stays under fileRatio^1.5. Gated by GITNEXUS_BENCH=1; runs build-free (workerPoolSize: 0). Addresses the benchmark request alongside PR #1990 (U5). * test(cpp): add cpp pipeline file-count benchmark Fills the one missing per-language pipeline benchmark (cobol/csharp/go/php/ ruby/rust already have one); modeled on cobol-pipeline-benchmark.test.ts. Generates synthetic C++ with constant per-file work and constant header fan-out, sweeps file count through the full pipeline, and guards linearity with a coarse time-ratio bound plus a deterministic node-ratio bound (the non-flaky guard against reintroducing O(fileCount^2) work). Gated by GITNEXUS_BENCH=1; runs build-free (workerPoolSize: 0). Addresses the benchmark request alongside PR #1990 (U6). * style(cpp): prettier-format adl benchmark * test(cpp): rebaseline scope-capture fingerprint for new ADL fixture The U4 parity fixture (cpp-adl-ns-plus-hidden-friend-same-name) lives under test/fixtures/lang-resolution/cpp-*, so its lib.h + app.cpp join the cpp scope-capture bench corpus (bench/scope-capture/measure.mjs). That is pure fixture-corpus growth — no scope-extractor change, existing fixtures' captures byte-identical — so the cpp fingerprint legitimately drifts (fixture_count 265->267). Rebaseline cpp to match, as #1965/#1975 did for earlier fixture additions. Verified: --check PASS for all 14 languages. Addresses PR #1990 tri-review (U4 follow-on). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
3 KiB
TypeScript
79 lines
3 KiB
TypeScript
/**
|
|
* Unit tests for the C++ ADL seq-coverage invariant guard.
|
|
*
|
|
* `pickCppAdlCandidates` sorts merged candidates by `seqByNodeId`, falling back
|
|
* to `?? 0` if a bucketed def has no seq. That fallback is provably unreachable
|
|
* (every def pushed into `nsCandidates`/`friendCandidates` is seq-assigned in the
|
|
* same build block), but a future regression could break the invariant and
|
|
* silently collapse two seq-0 candidates into one. `validateAdlSeqCoverage`
|
|
* detects that break; `buildAdlIndex` runs it under the dev/test validation gate
|
|
* so a regression fails loudly in CI rather than dropping a CALLS edge in prod.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
validateAdlSeqCoverage,
|
|
type AdlCandidateIndex,
|
|
} from '../../../../src/core/ingestion/languages/cpp/adl.js';
|
|
import type { SymbolDefinition } from 'gitnexus-shared';
|
|
|
|
function def(nodeId: string): SymbolDefinition {
|
|
return { nodeId } as unknown as SymbolDefinition;
|
|
}
|
|
|
|
function makeIndex(
|
|
nsCandidates: Map<string, Map<string, SymbolDefinition[]>>,
|
|
friendCandidates: Map<string, Map<string, SymbolDefinition[]>>,
|
|
seqByNodeId: Map<string, number>,
|
|
): AdlCandidateIndex {
|
|
return {
|
|
classDefsBySimple: new Map(),
|
|
nsCandidates,
|
|
friendCandidates,
|
|
nsFunctionsByQName: new Map(),
|
|
nsFunctionsBySimple: new Map(),
|
|
seqByNodeId,
|
|
};
|
|
}
|
|
|
|
describe('validateAdlSeqCoverage', () => {
|
|
it('returns no missing ids when every bucketed def has a seq', () => {
|
|
const ns = new Map([['lib', new Map([['act', [def('A')]]])]]);
|
|
const friend = new Map([['lib', new Map([['swap', [def('B')]]])]]);
|
|
const seq = new Map([
|
|
['A', 0],
|
|
['B', 1],
|
|
]);
|
|
|
|
expect(validateAdlSeqCoverage(makeIndex(ns, friend, seq))).toEqual([]);
|
|
});
|
|
|
|
it('flags a namespace-candidate def missing from seqByNodeId', () => {
|
|
const ns = new Map([['lib', new Map([['act', [def('A'), def('C')]]])]]);
|
|
const friend = new Map<string, Map<string, SymbolDefinition[]>>();
|
|
const seq = new Map([['A', 0]]); // 'C' missing
|
|
|
|
expect(validateAdlSeqCoverage(makeIndex(ns, friend, seq))).toEqual(['C']);
|
|
});
|
|
|
|
it('flags a friend-candidate def missing from seqByNodeId', () => {
|
|
const ns = new Map<string, Map<string, SymbolDefinition[]>>();
|
|
const friend = new Map([['lib', new Map([['swap', [def('D')]]])]]);
|
|
const seq = new Map<string, number>(); // 'D' missing
|
|
|
|
expect(validateAdlSeqCoverage(makeIndex(ns, friend, seq))).toEqual(['D']);
|
|
});
|
|
|
|
it('reports each missing nodeId once even when bucketed under multiple keys', () => {
|
|
// Inline-namespace transparency registers the same def under its own and
|
|
// its parent QName; a missing seq should surface as a single entry.
|
|
const inner = new Map([['act', [def('E')]]]);
|
|
const ns = new Map([
|
|
['lib', inner],
|
|
['lib.inline', inner],
|
|
]);
|
|
const friend = new Map<string, Map<string, SymbolDefinition[]>>();
|
|
const seq = new Map<string, number>(); // 'E' missing
|
|
|
|
expect(validateAdlSeqCoverage(makeIndex(ns, friend, seq))).toEqual(['E']);
|
|
});
|
|
});
|