mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat(shared): MethodDispatchIndex materialized view over HeritageMap (#914, RFC #909 Ring 2 SHARED) (#960)
Implements RFC §3.1 `MethodDispatchIndex`: a two-way materialized view
keyed by `DefId` for O(1) method-dispatch resolution:
- `mroByOwnerDefId` — owner class → full MRO ancestor chain
(excludes self, per-language strategy order)
- `implsByInterfaceDefId` — interface/trait → classes that implement it
**Not an MRO implementation.** `buildMethodDispatchIndex` is a pure
aggregator that calls back into caller-provided `computeMro` and
`implementsOf` functions. The five existing strategies (Python C3, Ruby
kind-aware, Java/Kotlin linear, Rust qualified-syntax, COBOL none) stay
where they are today (`model/resolve.ts`, `languages/ruby.ts`); this index
does not reimplement them.
Why callbacks rather than a shared registry: the strategies depend on the
CLI's `HeritageMap` + `SemanticModel`. Migrating both to `gitnexus-shared`
is out of scope for #914; callbacks let the shared build stay pure.
Module placement: `gitnexus-shared/src/scope-resolution/method-dispatch-index.ts`
for consistency with the other RFC §3.1 indexes (#913 DefIndex /
ModuleScopeIndex / QualifiedNameIndex; #916 resolveTypeRef).
Safety surface mirrors sibling indexes:
- First-write-wins on duplicate owners.
- Repeated (interface, owner) pairs deduplicated.
- Stored arrays are `Object.freeze`d; caller mutation of the source
array does not leak into the index.
- Miss returns a shared frozen empty array.
Tests (19, all passing): empty input, single-inheritance chain, Python
C3 diamond, Java BFS, Ruby kind-aware mixin, Rust qualified-syntax empty,
interface inversion (single, multiple, ordered), dedup within and across
callback calls, frozen miss + bucket arrays, callback-array isolation,
readonly Map iteration.
Closes part of #909.
This commit is contained in:
parent
56e32b310b
commit
5d76dbcfa2
3 changed files with 360 additions and 0 deletions
|
|
@ -75,6 +75,13 @@ export type { QualifiedNameIndex } from './scope-resolution/qualified-name-index
|
|||
export { resolveTypeRef } from './scope-resolution/resolve-type-ref.js';
|
||||
export type { ResolveTypeRefContext, ScopeLookup } from './scope-resolution/resolve-type-ref.js';
|
||||
|
||||
// Method-dispatch materialized view over HeritageMap (RFC §3.1; Ring 2 SHARED #914)
|
||||
export { buildMethodDispatchIndex } from './scope-resolution/method-dispatch-index.js';
|
||||
export type {
|
||||
MethodDispatchIndex,
|
||||
MethodDispatchInput,
|
||||
} from './scope-resolution/method-dispatch-index.js';
|
||||
|
||||
// Shadow-mode diff + aggregation (RFC §6.3; Ring 2 SHARED #918)
|
||||
export { diffResolutions } from './scope-resolution/shadow/diff.js';
|
||||
export type {
|
||||
|
|
|
|||
137
gitnexus-shared/src/scope-resolution/method-dispatch-index.ts
Normal file
137
gitnexus-shared/src/scope-resolution/method-dispatch-index.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* `MethodDispatchIndex` — materialized view of class hierarchies keyed by
|
||||
* `DefId` (RFC §3.1; Ring 2 SHARED #914).
|
||||
*
|
||||
* Two O(1)-access maps used by `Registry.lookupMethod` and interface-
|
||||
* dispatch callers:
|
||||
*
|
||||
* - `mroByOwnerDefId` : owner class → full MRO ancestor chain
|
||||
* (excludes the owner itself, in per-language
|
||||
* strategy order).
|
||||
* - `implsByInterfaceDefId` : interface/trait → classes that implement it.
|
||||
*
|
||||
* **Not an MRO implementation.** The build function is a pure aggregator: it
|
||||
* asks the caller (via `computeMro` and `implementsOf` callbacks) for the
|
||||
* per-language answers and materializes the two-way index. MRO strategies
|
||||
* live where they already do today (`model/resolve.ts § c3Linearize`,
|
||||
* `languages/ruby.ts § selectDispatch`, etc.) — this index does not
|
||||
* reimplement them.
|
||||
*
|
||||
* Why callbacks and not a shared strategy registry: the five strategies
|
||||
* (Python C3, Ruby kind-aware, Java/Kotlin linear, Rust qualified-syntax,
|
||||
* COBOL none) already exist in the CLI package and depend on the CLI's
|
||||
* `HeritageMap` + `SemanticModel`. Pulling them into `gitnexus-shared` would
|
||||
* require migrating both — out of scope for #914. Callbacks let the shared
|
||||
* build stay pure while honoring existing strategies verbatim.
|
||||
*
|
||||
* Consumed by: #917 (`Registry.lookupMethod` MRO fast path, interface
|
||||
* dispatch resolver).
|
||||
*/
|
||||
|
||||
import type { DefId } from './types.js';
|
||||
|
||||
// ─── Public contracts ───────────────────────────────────────────────────────
|
||||
|
||||
export interface MethodDispatchIndex {
|
||||
/**
|
||||
* Full MRO ancestor chain per owner class (excludes the owner itself).
|
||||
* Order reflects the per-language strategy used by `computeMro`.
|
||||
*/
|
||||
readonly mroByOwnerDefId: ReadonlyMap<DefId, readonly DefId[]>;
|
||||
/** Interfaces / traits → classes that implement them. */
|
||||
readonly implsByInterfaceDefId: ReadonlyMap<DefId, readonly DefId[]>;
|
||||
|
||||
/** `mroByOwnerDefId.get`, with an empty frozen array on miss. */
|
||||
mroFor(ownerDefId: DefId): readonly DefId[];
|
||||
/** `implsByInterfaceDefId.get`, with an empty frozen array on miss. */
|
||||
implementorsOf(interfaceDefId: DefId): readonly DefId[];
|
||||
}
|
||||
|
||||
export interface MethodDispatchInput {
|
||||
/**
|
||||
* Owner defs to index (classes, structs, traits, interfaces — any kind
|
||||
* that can appear on the owner side of a method-dispatch graph).
|
||||
*/
|
||||
readonly owners: readonly DefId[];
|
||||
/**
|
||||
* Return the full MRO ancestor chain for `ownerDefId`, **excluding the
|
||||
* owner itself**, in the order dictated by the owner's language-specific
|
||||
* MRO strategy.
|
||||
*
|
||||
* Contract:
|
||||
* - Pure (no side effects).
|
||||
* - Deterministic per input.
|
||||
* - `undefined` not allowed — return `[]` when the owner has no parents.
|
||||
*/
|
||||
readonly computeMro: (ownerDefId: DefId) => readonly DefId[];
|
||||
/**
|
||||
* Return the set of interface/trait defs that `ownerDefId` implements.
|
||||
* Transitive inclusion (e.g., `implements` on a parent class) is the
|
||||
* caller's choice — the build function simply inverts whatever is
|
||||
* returned.
|
||||
*
|
||||
* Repeated IDs in the output are deduplicated automatically.
|
||||
*/
|
||||
readonly implementsOf: (ownerDefId: DefId) => readonly DefId[];
|
||||
}
|
||||
|
||||
// ─── Builder ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildMethodDispatchIndex(input: MethodDispatchInput): MethodDispatchIndex {
|
||||
const mroByOwnerDefId = new Map<DefId, readonly DefId[]>();
|
||||
const implsBuilding = new Map<DefId, DefId[]>();
|
||||
const implsSeen = new Map<DefId, Set<DefId>>();
|
||||
|
||||
for (const ownerId of input.owners) {
|
||||
// First-write-wins on duplicate owner ids: a stable policy consistent
|
||||
// with sibling indexes (#913 DefIndex / ModuleScopeIndex).
|
||||
if (!mroByOwnerDefId.has(ownerId)) {
|
||||
const chain = input.computeMro(ownerId);
|
||||
mroByOwnerDefId.set(ownerId, Object.freeze(chain.slice()));
|
||||
}
|
||||
|
||||
for (const ifaceId of input.implementsOf(ownerId)) {
|
||||
let seen = implsSeen.get(ifaceId);
|
||||
if (seen === undefined) {
|
||||
seen = new Set<DefId>();
|
||||
implsSeen.set(ifaceId, seen);
|
||||
}
|
||||
if (seen.has(ownerId)) continue;
|
||||
seen.add(ownerId);
|
||||
|
||||
let bucket = implsBuilding.get(ifaceId);
|
||||
if (bucket === undefined) {
|
||||
bucket = [];
|
||||
implsBuilding.set(ifaceId, bucket);
|
||||
}
|
||||
bucket.push(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
const implsByInterfaceDefId = new Map<DefId, readonly DefId[]>();
|
||||
for (const [ifaceId, owners] of implsBuilding) {
|
||||
implsByInterfaceDefId.set(ifaceId, Object.freeze(owners.slice()));
|
||||
}
|
||||
|
||||
return freezeIndex(mroByOwnerDefId, implsByInterfaceDefId);
|
||||
}
|
||||
|
||||
// ─── Internal ───────────────────────────────────────────────────────────────
|
||||
|
||||
const EMPTY: readonly DefId[] = Object.freeze([]);
|
||||
|
||||
function freezeIndex(
|
||||
mroByOwnerDefId: Map<DefId, readonly DefId[]>,
|
||||
implsByInterfaceDefId: Map<DefId, readonly DefId[]>,
|
||||
): MethodDispatchIndex {
|
||||
return {
|
||||
mroByOwnerDefId,
|
||||
implsByInterfaceDefId,
|
||||
mroFor(ownerDefId: DefId): readonly DefId[] {
|
||||
return mroByOwnerDefId.get(ownerDefId) ?? EMPTY;
|
||||
},
|
||||
implementorsOf(interfaceDefId: DefId): readonly DefId[] {
|
||||
return implsByInterfaceDefId.get(interfaceDefId) ?? EMPTY;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* Unit tests for `buildMethodDispatchIndex` / `MethodDispatchIndex`
|
||||
* (RFC #909 Ring 2 SHARED #914).
|
||||
*
|
||||
* Covers: empty input, single-inheritance chain, diamond inheritance (caller-
|
||||
* determined MRO order), interface-only dispatch, multiple implementors,
|
||||
* dedup, first-write-wins, C3 vs BFS strategy parity (both honored verbatim),
|
||||
* readonly surface + frozen output.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildMethodDispatchIndex, type MethodDispatchInput, type DefId } from 'gitnexus-shared';
|
||||
|
||||
// ─── Test helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
const input = (
|
||||
owners: readonly DefId[],
|
||||
mroByOwner: Record<DefId, readonly DefId[]>,
|
||||
implementsByOwner: Record<DefId, readonly DefId[]> = {},
|
||||
): MethodDispatchInput => ({
|
||||
owners,
|
||||
computeMro: (owner) => mroByOwner[owner] ?? [],
|
||||
implementsOf: (owner) => implementsByOwner[owner] ?? [],
|
||||
});
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('buildMethodDispatchIndex', () => {
|
||||
describe('empty / degenerate inputs', () => {
|
||||
it('builds an empty index from no owners', () => {
|
||||
const idx = buildMethodDispatchIndex(input([], {}));
|
||||
expect(idx.mroByOwnerDefId.size).toBe(0);
|
||||
expect(idx.implsByInterfaceDefId.size).toBe(0);
|
||||
expect(idx.mroFor('anything')).toEqual([]);
|
||||
expect(idx.implementorsOf('anything')).toEqual([]);
|
||||
});
|
||||
|
||||
it('indexes an owner with no parents and no interfaces', () => {
|
||||
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
|
||||
expect(idx.mroByOwnerDefId.size).toBe(1);
|
||||
expect(idx.implsByInterfaceDefId.size).toBe(0);
|
||||
expect(idx.mroFor('def:A')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MRO materialization (single / multi inheritance)', () => {
|
||||
it('records a single-inheritance chain verbatim from the callback', () => {
|
||||
// A extends B extends C
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:A', 'def:B', 'def:C'], {
|
||||
'def:A': ['def:B', 'def:C'],
|
||||
'def:B': ['def:C'],
|
||||
'def:C': [],
|
||||
}),
|
||||
);
|
||||
expect(idx.mroFor('def:A')).toEqual(['def:B', 'def:C']);
|
||||
expect(idx.mroFor('def:B')).toEqual(['def:C']);
|
||||
expect(idx.mroFor('def:C')).toEqual([]);
|
||||
});
|
||||
|
||||
it('records a C3 linearization verbatim (Python diamond)', () => {
|
||||
// D(B, C) where B(A), C(A). Classical C3: D, B, C, A.
|
||||
// Our index stores mro excluding self: [B, C, A].
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:D'], { 'def:D': ['def:B', 'def:C', 'def:A'] }),
|
||||
);
|
||||
expect(idx.mroFor('def:D')).toEqual(['def:B', 'def:C', 'def:A']);
|
||||
});
|
||||
|
||||
it('records a BFS linearization verbatim (Java-style first-wins)', () => {
|
||||
// D extends B, C; B extends A; C extends A. BFS: B, C, A.
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:D'], { 'def:D': ['def:B', 'def:C', 'def:A'] }),
|
||||
);
|
||||
expect(idx.mroFor('def:D')).toEqual(['def:B', 'def:C', 'def:A']);
|
||||
});
|
||||
|
||||
it('records a Ruby-style kind-aware ancestry verbatim', () => {
|
||||
// class C prepend P1 prepend P2; include M1 include M2
|
||||
// ruby-mixin walk order (per callback): [P2, P1, M2, M1]
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:C'], { 'def:C': ['def:P2', 'def:P1', 'def:M2', 'def:M1'] }),
|
||||
);
|
||||
expect(idx.mroFor('def:C')).toEqual(['def:P2', 'def:P1', 'def:M2', 'def:M1']);
|
||||
});
|
||||
|
||||
it('records an empty chain for Rust qualified-syntax owners', () => {
|
||||
// Rust: no auto-MRO; callback returns []
|
||||
const idx = buildMethodDispatchIndex(input(['def:RustStruct'], { 'def:RustStruct': [] }));
|
||||
expect(idx.mroFor('def:RustStruct')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('implements inversion', () => {
|
||||
it('inverts a single class → interface mapping', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:Impl'], { 'def:Impl': [] }, { 'def:Impl': ['def:IFace'] }),
|
||||
);
|
||||
expect(idx.implementorsOf('def:IFace')).toEqual(['def:Impl']);
|
||||
});
|
||||
|
||||
it('aggregates multiple classes implementing the same interface', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(
|
||||
['def:A', 'def:B', 'def:C'],
|
||||
{ 'def:A': [], 'def:B': [], 'def:C': [] },
|
||||
{ 'def:A': ['def:I'], 'def:B': ['def:I'], 'def:C': ['def:J'] },
|
||||
),
|
||||
);
|
||||
expect(idx.implementorsOf('def:I')).toEqual(['def:A', 'def:B']);
|
||||
expect(idx.implementorsOf('def:J')).toEqual(['def:C']);
|
||||
});
|
||||
|
||||
it('preserves iteration order of owners in each implementors bucket', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(
|
||||
['def:Z', 'def:Y', 'def:X'],
|
||||
{ 'def:Z': [], 'def:Y': [], 'def:X': [] },
|
||||
{ 'def:Z': ['def:I'], 'def:Y': ['def:I'], 'def:X': ['def:I'] },
|
||||
),
|
||||
);
|
||||
expect(idx.implementorsOf('def:I')).toEqual(['def:Z', 'def:Y', 'def:X']);
|
||||
});
|
||||
|
||||
it('deduplicates repeated (interface, owner) pairs within a single callback call', () => {
|
||||
// Caller may legally return the same interface twice (e.g., a class that
|
||||
// both `implements IFace` and inherits from a parent that also does).
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:Impl'], { 'def:Impl': [] }, { 'def:Impl': ['def:I', 'def:I', 'def:I'] }),
|
||||
);
|
||||
expect(idx.implementorsOf('def:I')).toEqual(['def:Impl']);
|
||||
});
|
||||
|
||||
it('deduplicates when the same owner is listed in `owners` twice (first-write-wins)', () => {
|
||||
// First-write-wins parity with sibling indexes; subsequent owner entries
|
||||
// should not re-invoke callbacks for existing MRO, and should not create
|
||||
// duplicate implementor entries.
|
||||
let mroCalls = 0;
|
||||
const impls: Record<DefId, readonly DefId[]> = { 'def:A': ['def:I'] };
|
||||
const idx = buildMethodDispatchIndex({
|
||||
owners: ['def:A', 'def:A'],
|
||||
computeMro: (_) => {
|
||||
mroCalls++;
|
||||
return ['def:B'];
|
||||
},
|
||||
implementsOf: (o) => impls[o] ?? [],
|
||||
});
|
||||
expect(mroCalls).toBe(1);
|
||||
expect(idx.mroFor('def:A')).toEqual(['def:B']);
|
||||
expect(idx.implementorsOf('def:I')).toEqual(['def:A']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookup miss / safety surface', () => {
|
||||
it('returns a frozen empty array on MRO miss', () => {
|
||||
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
|
||||
const miss = idx.mroFor('def:Missing');
|
||||
expect(miss).toEqual([]);
|
||||
expect(() => (miss as unknown as DefId[]).push('x')).toThrow();
|
||||
});
|
||||
|
||||
it('returns a frozen empty array on implementors miss', () => {
|
||||
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
|
||||
const miss = idx.implementorsOf('def:Missing');
|
||||
expect(miss).toEqual([]);
|
||||
expect(() => (miss as unknown as DefId[]).push('x')).toThrow();
|
||||
});
|
||||
|
||||
it('freezes stored MRO arrays (readonly surface)', () => {
|
||||
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': ['def:B'] }));
|
||||
const chain = idx.mroFor('def:A');
|
||||
expect(() => (chain as unknown as DefId[]).push('x')).toThrow();
|
||||
});
|
||||
|
||||
it('freezes stored implementors arrays (readonly surface)', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:A'], { 'def:A': [] }, { 'def:A': ['def:I'] }),
|
||||
);
|
||||
const impls = idx.implementorsOf('def:I');
|
||||
expect(() => (impls as unknown as DefId[]).push('x')).toThrow();
|
||||
});
|
||||
|
||||
it('isolates stored MRO from later mutation of the callback-returned array', () => {
|
||||
const mutable = ['def:B', 'def:C'];
|
||||
const idx = buildMethodDispatchIndex({
|
||||
owners: ['def:A'],
|
||||
computeMro: () => mutable,
|
||||
implementsOf: () => [],
|
||||
});
|
||||
mutable.push('def:D');
|
||||
expect(idx.mroFor('def:A')).toEqual(['def:B', 'def:C']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readonly surface', () => {
|
||||
it('exposes `mroByOwnerDefId` as a read-only Map for direct iteration', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(['def:A', 'def:B'], { 'def:A': [], 'def:B': ['def:A'] }),
|
||||
);
|
||||
const owners = Array.from(idx.mroByOwnerDefId.keys()).sort();
|
||||
expect(owners).toEqual(['def:A', 'def:B']);
|
||||
});
|
||||
|
||||
it('exposes `implsByInterfaceDefId` as a read-only Map for direct iteration', () => {
|
||||
const idx = buildMethodDispatchIndex(
|
||||
input(
|
||||
['def:A', 'def:B'],
|
||||
{ 'def:A': [], 'def:B': [] },
|
||||
{ 'def:A': ['def:I'], 'def:B': ['def:J'] },
|
||||
),
|
||||
);
|
||||
const keys = Array.from(idx.implsByInterfaceDefId.keys()).sort();
|
||||
expect(keys).toEqual(['def:I', 'def:J']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue