GitNexus/gitnexus/scripts/bench-scope-resolution.ts
Gergő Magyar 083aedbc41
refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023)
* refactor(ingestion): delete legacy call-resolution DAG + heritage processor (#942)

RING4-1: all 16 production languages (incl. Vue #940) are registry-primary, so
the legacy resolution legs only ran under the now-removed CI parity gate. Calls
and inheritance now resolve exclusively through scope-resolution
(Registry.lookup, preEmitInheritanceEdges, emitHeritageEdges, buildMro →
MethodDispatchIndex).

Removed:
- Call-resolution DAG: call-processor.ts legacy body (processCalls,
  processCallsFromExtracted, resolveCallTarget + all resolver/dispatch/chain
  helpers), model/resolve.ts MRO-via-HeritageMap, model/heritage-map.ts,
  type-env DAG types; inferImplicitReceiver/selectDispatch LanguageProvider
  hooks + Ruby impls; DispatchDecision/ImplicitReceiverOverride/ReceiverEnriched.
- Legacy heritage path: heritage-processor.ts, heritage-types.ts,
  heritage-extractors/, @heritage.* tree-sitter queries, heritageExtractor/
  heritageDefaultEdge/interfaceNamePattern wiring, worker + parse-impl heritage
  passes (parse-worker/parsing-processor lockstep), cross-file-impl DAG pass.
- Scope-parity infrastructure entirely (no legacy↔registry parity left to run):
  scripts/run-parity.ts, scripts/ci-list-migrated-languages.ts,
  ci-scope-parity.yml, test:parity, and the scope-parity ci.yml gate. Resolver
  integration tests still run via the normal tests job.

Kept (shared infra, NOT call-DAG-only): type-env.ts buildTypeEnv (field
extraction / structure phase / embeddings), model/resolve.ts c3Linearize +
gatherAncestors (mro-processor mroPhase), route/fetch/exported-type-map helpers
in call-processor.ts, preEmitInheritanceEdges (legacy-edge dedup simplified).

Acceptance: grep for resolveCallTarget/inferImplicitReceiver/selectDispatch/
buildHeritageMap/HeritageMap/processHeritage/heritageExtractor/@heritage. is zero
across src + test. tsc clean (both packages); resolver integration suite green
(bit-compatible EXTENDS/IMPLEMENTS/CALLS); scope-capture fingerprints unchanged
(python re-baselined: removed redundant ignored captures). ARCHITECTURE.md
updated to scope-resolution-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply autofix feedback (#942)

ce-code-review autofix pass on the RING4-1 deletion:
- parse-cache.ts: bump SCHEMA_BUMP 2→3 — ParseWorkerResult lost its `heritage`
  field, so stale on-disk caches must invalidate (prevents a rollback replaying
  a heritage-less cache into legacy code) [api-contract P2].
- parse-impl.ts: drop 3 now-unused type imports (ExtractedCall,
  ExtractedAssignment, FileConstructorBindings) left by the deferred-block
  removal — would fail the eslint CI gate [correctness+maintainability P1].
- AGENTS.md / CLAUDE.md / scope-resolver.ts contract doc: fix stale pointers to
  the deleted "§ Call-Resolution DAG" section + removed hooks; preserve the
  language-neutrality rule [project-standards P1].
- registry-primary-flag.ts / cross-file.ts / parse-impl.ts: refresh stale
  comments referencing deleted symbols (legacy DAG, runCrossFileBindingPropagation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ingestion): remove the vestigial isRegistryPrimary flag (#942)

With the legacy call-resolution DAG deleted, the per-language
`REGISTRY_PRIMARY_<LANG>` / `isRegistryPrimary` / `MIGRATED_LANGUAGES` flag had
only one meaningful state — every production language resolves via
scope-resolution — and an explicit `=0` override could only *disable*
resolution with no fallback (a footgun the review flagged). Removing it.

- Delete `registry-primary-flag.ts` and the now-dead `shadow-harness.ts`
  (legacy↔registry shadow-parity tool) + its test.
- Collapse the three flag gates to their behavior-preserving outcome
  (`SCOPE_RESOLVERS == MIGRATED_LANGUAGES`, so this is a no-op):
  - scope-resolution phase now runs for every registered `SCOPE_RESOLVERS`
    entry (was `∩ MIGRATED_LANGUAGES`).
  - import-processor `addImportGraphEdge` + parse-impl `shouldAccumulate`:
    the legacy emit/accumulate paths were already inert for migrated
    languages (scope-resolution owns IMPORTS via the imports-to-edges bridge);
    drop the flag term.
- Collapse flag-branching tests to the scope-resolution path and delete the
  csharp legacy-`=0`-leg describe blocks; remove the ruby/rust-scope env-forcing
  hooks (no-ops now).
- Refresh docs/comments (ARCHITECTURE.md "one registration", scope-resolver
  cookbook, phase deps) — adding a language is now a single `SCOPE_RESOLVERS`
  registration.

Verified: tsc clean (both packages); resolver integration tests green
(747 assertions across cobol/csharp/ruby/rust/typescript/go, IMPORTS edges
intact); grep for the flag symbols is zero across src + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(format): prettier formatting on #942 changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): drop legacy heritage-capture tests + re-baseline scope-capture fingerprints (#942)

Two CI failures from the #942 cleanup, surfaced by the tri-review + CI:

- tree-sitter-languages.test.ts: two tests asserted `@heritage.*` captures
  (Rust trait-impl, Dart extends/implements/with) that this PR removed. The
  acceptance grep used `@heritage\.` (with `@`); these reference the runtime
  capture name `heritage.trait` (no `@`), so they slipped the earlier sweep.
  Inheritance is now covered by the resolver integration suite. (fixed macos-latest)

- Re-baselined the scope-capture bench fingerprints for csharp/rust/ruby/java/
  javascript/kotlin (baselines.json) + python (python-scope/baseline-fingerprint.txt).
  The earlier test-cleanup reworded comments inside the lang-resolution fixture
  files (Shapes.cs, child.rs, derived.rb, IA.java/Plain.java, Service.js, F.kt,
  app.py) to scrub deleted-symbol references for the acceptance grep; those are
  the bench corpus, so capture node positions shifted. Capture LOGIC is
  unchanged — verified `--check` passes for all 14 langs + python. (fixed benchmarks)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs/chore: scrub remaining REGISTRY_PRIMARY + deleted-symbol references (#942)

Tri-review P3 follow-ups (verified):
- TESTING.md: rewrite the "Scope-resolution parity" section — the legacy
  dual-leg (REGISTRY_PRIMARY_<LANG>=0/1) and `npm run test:parity` no longer
  exist; resolver tests run once on the sole scope-resolution path in the
  normal tests job.
- scripts/bench-scope-resolution.ts: drop the inert `REGISTRY_PRIMARY_PYTHON=1`
  env set + usage hint (the flag is gone).
- ruby/scope-resolver.ts, php/captures.ts: re-point doc-comments off the
  deleted heritage-map.ts / heritage-processor.ts to the current behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): prettier format + regenerate scope-capture goldens (#942)

Two more CI failures, same root cause as the bench re-baseline (the
test-cleanup reworded comments in lang-resolution bench/golden-corpus fixtures):

- quality/format: prettier on tree-sitter-languages.test.ts (blank line left by
  the deleted heritage-capture tests) + TESTING.md (the rewritten section).
- tests/ubuntu/coverage: `csharp-captures-golden` (and python/ruby/rust) drifted
  because the edited fixtures feed the per-language capture-golden snapshots too
  (not just the bench). Regenerated via UPDATE_GOLDEN=1. Verified safe: only the
  edited-fixture entries changed; csharp `captureGroups` unchanged (38) — digest
  shifted from comment-position only; capture LOGIC untouched. 1168 scope-
  resolution tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(resolvers): drop createResolverParityIt wrapper, use vitest it directly

The parity-aware `it` wrapper became a no-op when #942 removed the legacy
call-resolution DAG (it just returned vitest's `it`). Remove it entirely so
the resolver tests call vitest's `it` directly instead of shadowing it with a
local `const it` (or `pit`/`rustParityIt`):

- helpers.ts: delete createResolverParityIt + its now-unused vitestIt import
  and VitestIt type.
- 16 files: drop `const it = createResolverParityIt('x')` and import `it`
  from vitest instead.
- ruby.test.ts (pit) + rust.test.ts (rustParityIt): rename calls to `it`.
- Scrub every comment that described the removed wrapper / dual-mode parity
  skip / legacy_skip gate (vue-scope, js/ts/dart/php/python headers, rust x2,
  cpp, swift x4, rust-coverage). Genuine test rationale is kept; only the
  vestigial two-leg framing is dropped. Accurate "legacy DAG (removed in
  #942)" historical notes are retained.

No fixtures touched (no bench/golden re-baseline). tsc clean; rust+ruby
resolver suites green (323 tests, incl. #1992 worker-path parity after a
local dist build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:07:37 +01:00

132 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Synthetic benchmark for scope-resolution. Builds a large in-memory
* Python workspace and times runScopeResolution against it directly,
* isolating the resolution cost from parse / heritage / pipeline
* overhead.
*
* Usage: npx tsx scripts/bench-scope-resolution.ts
*/
import { generateId } from '../src/lib/utils.js';
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
import { pythonScopeResolver } from '../src/core/ingestion/languages/python/scope-resolver.js';
const N_CLASSES = Number(process.env.BENCH_CLASSES ?? '60');
const N_USERS = Number(process.env.BENCH_USERS ?? '40');
const ITERS = Number(process.env.BENCH_ITERS ?? '5');
function buildWorkspace(): { path: string; content: string }[] {
const files: { path: string; content: string }[] = [];
// Build N_CLASSES "model" files, each defining a class with a few methods.
for (let i = 0; i < N_CLASSES; i++) {
const lines: string[] = [];
for (let j = 0; j < 5; j++) {
lines.push(`class Model${i}_${j}:`);
lines.push(` name: str`);
lines.push(` def save(self) -> bool:`);
lines.push(` return True`);
lines.push(` def update(self, name: str) -> "Model${i}_${j}":`);
lines.push(` self.name = name`);
lines.push(` return self`);
lines.push(` def get_other(self) -> "Model${i}_${(j + 1) % 5}":`);
lines.push(` return Model${i}_${(j + 1) % 5}()`);
lines.push('');
}
files.push({ path: `models/m${i}.py`, content: lines.join('\n') });
}
// Build N_USERS "user" files that import from a few model files
// and exercise the receiver-bound dispatcher heavily.
for (let u = 0; u < N_USERS; u++) {
const targets = [u % N_CLASSES, (u + 1) % N_CLASSES, (u + 2) % N_CLASSES];
const imports = targets
.map((t) => `from models.m${t} import Model${t}_0, Model${t}_1, Model${t}_2`)
.join('\n');
const calls: string[] = [];
for (let k = 0; k < 30; k++) {
const t = targets[k % 3]!;
const j = k % 3;
calls.push(` m${k} = Model${t}_${j}()`);
calls.push(` m${k}.save()`);
calls.push(` m${k}.update("x").save()`);
calls.push(` m${k}.get_other().save()`);
}
const content = `${imports}\n\ndef use_${u}() -> None:\n${calls.join('\n')}\n`;
files.push({ path: `app/u${u}.py`, content });
}
return files;
}
function buildGraph(files: { path: string; content: string }[]) {
const graph = createKnowledgeGraph();
// Pre-populate File / Class / Function nodes the resolver expects.
for (const f of files) {
const fileId = generateId('File', f.path);
graph.addNode({
id: fileId,
label: 'File',
properties: { name: f.path, filePath: f.path },
});
// Lightweight regex-extract class & def names so the lookup index
// has something to find. Real pipeline builds these via parse phase;
// for the bench this stand-in is enough to exercise the resolver.
const classRe = /^class (\w+)/gm;
const defRe = /^\s*def (\w+)/gm;
let m: RegExpExecArray | null;
while ((m = classRe.exec(f.content)) !== null) {
const name = m[1]!;
const id = generateId('Class', `${f.path}:${name}`);
graph.addNode({
id,
label: 'Class',
properties: { name, filePath: f.path, qualifiedName: name },
});
}
while ((m = defRe.exec(f.content)) !== null) {
const name = m[1]!;
const id = generateId('Function', `${f.path}:${name}`);
graph.addNode({
id,
label: 'Function',
properties: { name, filePath: f.path, qualifiedName: name },
});
}
}
return graph;
}
async function main() {
const files = buildWorkspace();
console.log(`bench: ${files.length} files (${N_CLASSES} models × 5 classes + ${N_USERS} users)`);
console.log(` × ${ITERS} iterations\n`);
// Warmup
for (let i = 0; i < 2; i++) {
const graph = buildGraph(files);
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
}
const samples: number[] = [];
for (let i = 0; i < ITERS; i++) {
const graph = buildGraph(files);
const start = process.hrtime.bigint();
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1_000_000;
samples.push(ms);
console.log(` iter ${i + 1}: ${ms.toFixed(0)} ms`);
}
samples.sort((a, b) => a - b);
const median = samples[Math.floor(samples.length / 2)]!;
const min = samples[0]!;
console.log(`\nmin: ${min.toFixed(0)} ms · median: ${median.toFixed(0)} ms`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});