mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
refactor(emit-core): promote MRO walk + populateClassOwnedMembers
G-Units 4-5 of the emit-pipeline generalization plan. - emit-core/build-mro.ts — generic buildMro takes a LinearizeStrategy hook receiving (classDefId, directParents, parentsByDefId). Three shared steps (collect EXTENDS, build defId-by-graphId, walk per class) + parametric linearization. Default strategy is BFS-with- visited (Python's depth-first first-seen, also correct for single-inheritance languages). - emit-core/scope-walkers.ts: + populateClassOwnedMembers — generic OO ownership rule (methods + class-body fields). Both rules ship together because every OO language migrated so far (Python; planned TS/JS/Java/Kotlin) wants both. Languages that need different rules can compose with this as a base step. python-scope-emit.ts shrinks 384 → 255 lines. Verification: - REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191. - REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191. - tsc --noEmit clean.
This commit is contained in:
parent
46fe4c0a39
commit
fb824a6877
4 changed files with 161 additions and 128 deletions
107
gitnexus/src/core/ingestion/emit-core/build-mro.ts
Normal file
107
gitnexus/src/core/ingestion/emit-core/build-mro.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Generic MRO (method-resolution-order) builder.
|
||||
*
|
||||
* Walks the graph's `EXTENDS` edges to recover an inheritance map,
|
||||
* then asks the per-language `LinearizeStrategy` to order each class's
|
||||
* ancestors. Returns `Map<classDefId, ancestorDefId[]>` ready to plug
|
||||
* into `MethodDispatchIndex` via `buildPopulatedMethodDispatch`.
|
||||
*
|
||||
* **Why a strategy hook:** linearization differs across languages.
|
||||
* - Python (depth-first first-seen, single inheritance): trivially
|
||||
* correct; multi-inheritance falls back to BFS dedup. Real C3
|
||||
* would handle diamond hierarchies — defer until we hit one.
|
||||
* - Java (single-inheritance only): walk one parent.
|
||||
* - C++ (multiple inheritance): C3-like or BFS depending on how
|
||||
* strict the consumer needs to be.
|
||||
* - Languages without inheritance (COBOL): return empty list.
|
||||
*
|
||||
* The strategy receives the FULL ancestry context (`directParents` +
|
||||
* `parentsByDefId`) so C3 implementations have what they need.
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../graph/types.js';
|
||||
import type { GraphNodeLookup } from './graph-node-lookup.js';
|
||||
import type { LinearizeStrategy } from './emit-provider.js';
|
||||
import { resolveDefGraphId } from './graph-id.js';
|
||||
|
||||
/**
|
||||
* Build an MRO map keyed by scope-resolution Class `DefId`.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Collect EXTENDS edges from the graph → `parentsByGraphId`.
|
||||
* 2. Collect Class defs from `parsedFiles` and translate to graph
|
||||
* ids via `nodeLookup` → `defIdByGraphId` (the bridge between
|
||||
* scope-resolution DefId and the legacy graph node id).
|
||||
* 3. For each Class def, ask `linearize` for its ancestor order.
|
||||
*/
|
||||
export function buildMro(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
linearize: LinearizeStrategy,
|
||||
): Map<string /* DefId */, string[] /* DefId[] */> {
|
||||
// Step 1: parentsByGraphId.
|
||||
const parentsByGraphId = new Map<string, string[]>();
|
||||
for (const rel of graph.iterRelationships()) {
|
||||
if (rel.type !== 'EXTENDS') continue;
|
||||
let list = parentsByGraphId.get(rel.sourceId);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
parentsByGraphId.set(rel.sourceId, list);
|
||||
}
|
||||
list.push(rel.targetId);
|
||||
}
|
||||
|
||||
// Step 2: defIdByGraphId — translate graph ids to scope-resolution DefIds.
|
||||
const defIdByGraphId = new Map<string, string>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (def.type !== 'Class') continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId !== undefined) defIdByGraphId.set(graphId, def.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2b: invert parentsByGraphId into parentsByDefId — the
|
||||
// strategy works in DefId space.
|
||||
const parentsByDefId = new Map<string, string[]>();
|
||||
for (const [childGraphId, parents] of parentsByGraphId) {
|
||||
const childDefId = defIdByGraphId.get(childGraphId);
|
||||
if (childDefId === undefined) continue;
|
||||
const parentDefIds: string[] = [];
|
||||
for (const p of parents) {
|
||||
const pd = defIdByGraphId.get(p);
|
||||
if (pd !== undefined) parentDefIds.push(pd);
|
||||
}
|
||||
parentsByDefId.set(childDefId, parentDefIds);
|
||||
}
|
||||
|
||||
// Step 3: linearize per class.
|
||||
const mroByDefId = new Map<string, string[]>();
|
||||
for (const defId of defIdByGraphId.values()) {
|
||||
const directParents = parentsByDefId.get(defId) ?? [];
|
||||
mroByDefId.set(defId, linearize(defId, directParents, parentsByDefId));
|
||||
}
|
||||
return mroByDefId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default linearization: depth-first BFS-with-visited, first-seen
|
||||
* wins. Correct for single-inheritance languages and for Python's
|
||||
* simplified MRO. Multi-inheritance diamond hierarchies need a real
|
||||
* C3 implementation; per-language overrides land here.
|
||||
*/
|
||||
export const defaultLinearize: LinearizeStrategy = (_classDefId, directParents, parentsByDefId) => {
|
||||
const ancestors: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
const queue: string[] = [...directParents];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.shift()!;
|
||||
if (visited.has(cur)) continue;
|
||||
visited.add(cur);
|
||||
ancestors.push(cur);
|
||||
for (const p of parentsByDefId.get(cur) ?? []) queue.push(p);
|
||||
}
|
||||
return ancestors;
|
||||
};
|
||||
|
|
@ -36,6 +36,7 @@ export {
|
|||
findExportedDefByName,
|
||||
findOwnedMember,
|
||||
findExportedDef,
|
||||
populateClassOwnedMembers,
|
||||
} from './scope-walkers.js';
|
||||
export { emitFreeCallFallback } from './emit-free-call.js';
|
||||
export { emitReceiverBoundCalls } from './emit-receiver-bound.js';
|
||||
|
|
@ -48,3 +49,4 @@ export { followChainPostFinalize, propagateImportedReturnTypes } from './propaga
|
|||
export { collectNamespaceTargets } from './namespace-targets.js';
|
||||
export { buildPopulatedMethodDispatch } from './method-dispatch-bridge.js';
|
||||
export type { ArityVerdict, EmitProvider, LinearizeStrategy } from './emit-provider.js';
|
||||
export { buildMro, defaultLinearize } from './build-mro.js';
|
||||
|
|
|
|||
|
|
@ -137,6 +137,52 @@ export function findCallableBindingInScope(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate `ownerId` on every def structurally owned by a Class
|
||||
* scope — methods (defs in Function scopes whose parent is Class)
|
||||
* and class-body fields (defs directly in Class scopes).
|
||||
*
|
||||
* Generic OO ownership rule. Languages that want richer ownership
|
||||
* (e.g. inner-class qualification) can compose with this as a base
|
||||
* step.
|
||||
*
|
||||
* Mutates `parsed.localDefs` in place via type cast — `SymbolDefinition`
|
||||
* is `readonly` for consumers but the extractor returns plain objects.
|
||||
* Defs are shared by reference between `localDefs` and `Scope.ownedDefs`,
|
||||
* so this single mutation is visible from both sides.
|
||||
*/
|
||||
export function populateClassOwnedMembers(parsed: ParsedFile): void {
|
||||
const scopesById = new Map<ScopeId, ParsedFile['scopes'][number]>();
|
||||
for (const scope of parsed.scopes) scopesById.set(scope.id, scope);
|
||||
|
||||
for (const scope of parsed.scopes) {
|
||||
// Methods: function scope whose parent is a Class scope. Owner is
|
||||
// the parent's Class def.
|
||||
if (scope.parent !== null) {
|
||||
const parentScope = scopesById.get(scope.parent);
|
||||
if (parentScope !== undefined && parentScope.kind === 'Class') {
|
||||
const classDef = parentScope.ownedDefs.find((d) => d.type === 'Class');
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
(def as { ownerId?: string }).ownerId = classDef.nodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Class-body fields: defs directly owned by a Class scope (the
|
||||
// class def itself excluded).
|
||||
if (scope.kind === 'Class') {
|
||||
const classDef = scope.ownedDefs.find((d) => d.type === 'Class');
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def === classDef) continue;
|
||||
(def as { ownerId?: string }).ownerId = classDef.nodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a scope chain upward looking for the innermost enclosing
|
||||
* Class scope and return that class's def. Used by per-language
|
||||
|
|
|
|||
|
|
@ -26,13 +26,7 @@
|
|||
* not here — this function is "what to do" once we've decided to do it.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ParsedFile,
|
||||
RegistryProviders,
|
||||
Scope,
|
||||
ScopeId,
|
||||
WorkspaceIndex,
|
||||
} from 'gitnexus-shared';
|
||||
import type { ParsedFile, RegistryProviders, Scope, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../graph/types.js';
|
||||
import { extractParsedFile } from './scope-extractor-bridge.js';
|
||||
import { finalizeScopeModel } from './finalize-orchestrator.js';
|
||||
|
|
@ -46,15 +40,16 @@ import {
|
|||
} from './languages/python/index.js';
|
||||
import {
|
||||
buildGraphNodeLookup,
|
||||
buildMro,
|
||||
buildPopulatedMethodDispatch,
|
||||
defaultLinearize,
|
||||
emitFreeCallFallback,
|
||||
emitImportEdges,
|
||||
emitReceiverBoundCalls,
|
||||
emitReferencesViaLookup,
|
||||
populateClassOwnedMembers,
|
||||
propagateImportedReturnTypes,
|
||||
resolveDefGraphId,
|
||||
type EmitProvider,
|
||||
type GraphNodeLookup,
|
||||
} from './emit-core/index.js';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
||||
|
|
@ -99,7 +94,7 @@ export function runPythonScopeResolution(
|
|||
filesSkipped++;
|
||||
continue;
|
||||
}
|
||||
populateMethodOwnerIds(parsed);
|
||||
populateClassOwnedMembers(parsed);
|
||||
parsedFiles.push(parsed);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +117,7 @@ export function runPythonScopeResolution(
|
|||
// mirror them into a `MethodDispatchIndex` so receiver-typed
|
||||
// resolution can walk inherited methods.
|
||||
const nodeLookup = buildGraphNodeLookup(graph);
|
||||
const mroByClassDefId = buildPythonMro(graph, parsedFiles, nodeLookup);
|
||||
const mroByClassDefId = buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
|
||||
|
||||
const indexes = finalizeScopeModel(parsedFiles, {
|
||||
hooks: {
|
||||
|
|
@ -240,123 +235,6 @@ export function runPythonScopeResolution(
|
|||
};
|
||||
}
|
||||
|
||||
// ─── Python-specific internals (move to languages/python/emit/ in Unit 11) ──
|
||||
|
||||
/**
|
||||
* Build a Python MRO map keyed by scope-resolution Class `DefId`.
|
||||
*
|
||||
* The legacy `parse` phase has already emitted EXTENDS edges into the
|
||||
* graph (via the heritage processor in `parsing-processor.ts`) by the
|
||||
* time this orchestrator runs (we depend on `parse`). We mirror those
|
||||
* edges into a `DefId → ancestor DefId[]` map so receiver-typed
|
||||
* `MethodRegistry.lookup` can walk inherited methods.
|
||||
*
|
||||
* MRO ordering: this is a **simple linear walk** (depth-first parent
|
||||
* chain, dedup by first-seen). Full Python C3 linearization lives in
|
||||
* the legacy heritage processor; replicating it here is out of scope
|
||||
* for the first cut. The single-inheritance case — which covers the
|
||||
* existing fixture suite (`User → BaseModel`, `Child → Parent`,
|
||||
* `Grandchild → Child → Parent`) — is identical to C3, so the
|
||||
* difference only surfaces with diamond hierarchies. Tracked as a
|
||||
* follow-up alongside generalizing this orchestrator across languages.
|
||||
*/
|
||||
function buildPythonMro(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): Map<string /* DefId */, string[] /* DefId[] */> {
|
||||
// Step 1: build (graph node id) → (parent graph node id[]) from
|
||||
// EXTENDS edges. Python only has class inheritance via `class
|
||||
// Child(Parent)`, which the heritage processor maps to EXTENDS
|
||||
// (not IMPLEMENTS).
|
||||
const parentsByGraphId = new Map<string, string[]>();
|
||||
for (const rel of graph.iterRelationships()) {
|
||||
if (rel.type !== 'EXTENDS') continue;
|
||||
let list = parentsByGraphId.get(rel.sourceId);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
parentsByGraphId.set(rel.sourceId, list);
|
||||
}
|
||||
list.push(rel.targetId);
|
||||
}
|
||||
|
||||
// Step 2: collect every Class def from the parsed scope model and
|
||||
// build a graph-node → DefId reverse map.
|
||||
const defIdByGraphId = new Map<string, string>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (def.type !== 'Class') continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId !== undefined) defIdByGraphId.set(graphId, def.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: for each Class def, walk parents transitively (depth-first,
|
||||
// first-seen-wins) and translate each ancestor back to its DefId.
|
||||
const mroByDefId = new Map<string, string[]>();
|
||||
for (const [graphId, defId] of defIdByGraphId) {
|
||||
const ancestors: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
const queue: string[] = [...(parentsByGraphId.get(graphId) ?? [])];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.shift()!;
|
||||
if (visited.has(cur)) continue;
|
||||
visited.add(cur);
|
||||
const ancDefId = defIdByGraphId.get(cur);
|
||||
if (ancDefId !== undefined) ancestors.push(ancDefId);
|
||||
for (const p of parentsByGraphId.get(cur) ?? []) queue.push(p);
|
||||
}
|
||||
mroByDefId.set(defId, ancestors);
|
||||
}
|
||||
return mroByDefId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate `ownerId` on Method/Function/Field defs that live structurally
|
||||
* inside a `Class` scope.
|
||||
*
|
||||
* Python's ownership rule: methods belong to the lexically enclosing
|
||||
* class. Applied before finalize so `MethodRegistry.lookup` Step 2
|
||||
* (`collectOwnedMembers`) finds candidates by class owner.
|
||||
*
|
||||
* Mutates `parsed.localDefs` in place via type cast — `SymbolDefinition`
|
||||
* is `readonly` for consumers but the extractor returns plain objects.
|
||||
* Defs are shared by reference between `localDefs` and `Scope.ownedDefs`,
|
||||
* so this single mutation is visible from both sides.
|
||||
*/
|
||||
function populateMethodOwnerIds(parsed: ParsedFile): void {
|
||||
const scopesById = new Map<ScopeId, Scope>();
|
||||
for (const scope of parsed.scopes) scopesById.set(scope.id, scope);
|
||||
|
||||
for (const scope of parsed.scopes) {
|
||||
// Methods (Function scopes whose PARENT is Class): set ownerId
|
||||
// on every def in the function's own scope.
|
||||
if (scope.parent !== null) {
|
||||
const parentScope = scopesById.get(scope.parent);
|
||||
if (parentScope !== undefined && parentScope.kind === 'Class') {
|
||||
const classDef = parentScope.ownedDefs.find((d) => d.type === 'Class');
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
(def as { ownerId?: string }).ownerId = classDef.nodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Class-body fields (defs structurally owned by the Class scope
|
||||
// itself — class-body annotations like `name: str`): set ownerId
|
||||
// on every def except the class itself.
|
||||
if (scope.kind === 'Class') {
|
||||
const classDef = scope.ownedDefs.find((d) => d.type === 'Class');
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def === classDef) continue;
|
||||
(def as { ownerId?: string }).ownerId = classDef.nodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal `EmitProvider` carrying only the hooks the receiver-bound
|
||||
* pass currently consults. The full provider (with mergeBindings,
|
||||
* resolveImportTarget, arityCompatibility, buildMro, populateOwners)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue