Skip to content

Commit d07bebe

Browse files
committed
fix(cha): skip CHA expansion for super-dispatch edges in runChaPostPass and runPostNativeCha
When a method calls super.method(), the resolver creates an edge from the subclass method to the parent class method (e.g. PostMixin.m → A.m). runChaPostPass and runPostNativeCha then incorrectly CHA-expanded this edge to all sibling subclasses of A (e.g. B from other files that also extend A), producing false PostMixin.m → B.m edges across unrelated files. Fix: before BFS-expanding a caller→type.method edge, check whether the caller's class is itself a direct child of the target type. If so, the edge was produced by a super.method() call (going up the hierarchy) and must not be expanded downward to sibling subclasses. This applies to both runChaPostPass (WASM path) and runPostNativeCha (native orchestrator path), which share the same CHA expansion loop structure. Verified by node scripts/parity-compare.mjs --langs jelly-micro: the 6 PostMixin super4 false edges are eliminated; all 3048 tests pass.
1 parent ce510f3 commit d07bebe

2 files changed

Lines changed: 52 additions & 9 deletions

File tree

src/domain/graph/builder/helpers.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,14 @@ export function runChaPostPass(db: BetterSqlite3Database): number {
464464

465465
const callToMethods = db
466466
.prepare(
467-
`SELECT e.source_id, tgt.name AS method_name
467+
`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name
468468
FROM edges e
469469
JOIN nodes tgt ON e.target_id = tgt.id
470+
JOIN nodes src ON e.source_id = src.id
470471
WHERE e.kind = 'calls' AND tgt.kind = 'method'
471472
AND INSTR(tgt.name, '.') > 0`,
472473
)
473-
.all() as Array<{ source_id: number; method_name: string }>;
474+
.all() as Array<{ source_id: number; caller_name: string; method_name: string }>;
474475

475476
const seen = new Set<string>();
476477
// Scope deduplication to only the source_ids we are about to expand, avoiding
@@ -497,12 +498,25 @@ export function runChaPostPass(db: BetterSqlite3Database): number {
497498
const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
498499
const newEdges: Array<[number, number, string, number, number, string]> = [];
499500

500-
for (const { source_id, method_name } of callToMethods) {
501+
for (const { source_id, caller_name, method_name } of callToMethods) {
501502
const dotIdx = method_name.indexOf('.');
502503
if (dotIdx === -1) continue;
503504
const typeName = method_name.slice(0, dotIdx);
504505
const methodSuffix = method_name.slice(dotIdx + 1);
505506

507+
// Super-dispatch guard: if the caller's class is itself a direct child of
508+
// typeName (i.e. callerClass extends typeName), the existing edge is a
509+
// super.method() call going up the hierarchy — not an interface dispatch.
510+
// Expanding it to sibling subclasses of typeName would produce false edges:
511+
// those siblings are unrelated to the caller and would never be invoked by
512+
// that super call. Skip CHA expansion entirely for super-dispatch edges.
513+
const callerDotIdx = caller_name.indexOf('.');
514+
if (callerDotIdx !== -1) {
515+
const callerClass = caller_name.slice(0, callerDotIdx);
516+
const directChildrenOfType = implementors.get(typeName);
517+
if (directChildrenOfType?.includes(callerClass)) continue;
518+
}
519+
506520
// BFS over the implementors map — handles multi-level hierarchies where
507521
// abstract/non-instantiated classes sit between the call-site type and
508522
// the concrete leaf implementations (matches runPostNativeCha, issue #1311).

src/domain/graph/builder/stages/native-orchestrator.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -572,16 +572,26 @@ function runPostNativeCha(
572572
// incremental role reclassification; confidence uses CHA_TYPED_DISPATCH_CONFIDENCE matching runChaPostPass.
573573
// When scopeToChangedFiles is true, restrict to call sites in the changed files
574574
// (safe because no hierarchy or RTA evidence changed outside those files).
575-
let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>;
575+
let callToMethods: Array<{
576+
source_id: number;
577+
caller_name: string;
578+
method_name: string;
579+
caller_file: string | null;
580+
}>;
576581
if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
577582
const CHUNK_SIZE = 500;
578-
const rows: Array<{ source_id: number; method_name: string; caller_file: string | null }> = [];
583+
const rows: Array<{
584+
source_id: number;
585+
caller_name: string;
586+
method_name: string;
587+
caller_file: string | null;
588+
}> = [];
579589
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
580590
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
581591
const ph = chunk.map(() => '?').join(',');
582592
const chunkRows = db
583593
.prepare(
584-
`SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
594+
`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
585595
FROM edges e
586596
JOIN nodes tgt ON e.target_id = tgt.id
587597
JOIN nodes src ON e.source_id = src.id
@@ -591,6 +601,7 @@ function runPostNativeCha(
591601
)
592602
.all(...chunk) as Array<{
593603
source_id: number;
604+
caller_name: string;
594605
method_name: string;
595606
caller_file: string | null;
596607
}>;
@@ -600,14 +611,19 @@ function runPostNativeCha(
600611
} else {
601612
callToMethods = db
602613
.prepare(`
603-
SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
614+
SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
604615
FROM edges e
605616
JOIN nodes tgt ON e.target_id = tgt.id
606617
JOIN nodes src ON e.source_id = src.id
607618
WHERE e.kind = 'calls' AND tgt.kind = 'method'
608619
AND INSTR(tgt.name, '.') > 0
609620
`)
610-
.all() as Array<{ source_id: number; method_name: string; caller_file: string | null }>;
621+
.all() as Array<{
622+
source_id: number;
623+
caller_name: string;
624+
method_name: string;
625+
caller_file: string | null;
626+
}>;
611627
}
612628

613629
// Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
@@ -635,12 +651,25 @@ function runPostNativeCha(
635651
const newEdges: Array<[number, number, string, number, number, string]> = [];
636652
let newEdgeCount = 0;
637653

638-
for (const { source_id, method_name, caller_file } of callToMethods) {
654+
for (const { source_id, caller_name, method_name, caller_file } of callToMethods) {
639655
const dotIdx = method_name.indexOf('.');
640656
if (dotIdx === -1) continue;
641657
const typeName = method_name.slice(0, dotIdx);
642658
const methodSuffix = method_name.slice(dotIdx + 1);
643659

660+
// Super-dispatch guard: if the caller's class is itself a direct child of
661+
// typeName (i.e. callerClass extends typeName), the existing edge is a
662+
// super.method() call going up the hierarchy — not an interface dispatch.
663+
// Expanding it to sibling subclasses of typeName would produce false edges:
664+
// those siblings are unrelated to the caller and would never be invoked by
665+
// that super call. Skip CHA expansion entirely for super-dispatch edges.
666+
const callerDotIdx = caller_name.indexOf('.');
667+
if (callerDotIdx !== -1) {
668+
const callerClass = caller_name.slice(0, callerDotIdx);
669+
const directChildrenOfType = implementors.get(typeName);
670+
if (directChildrenOfType?.includes(callerClass)) continue;
671+
}
672+
644673
// BFS over the implementors map — handles multi-level hierarchies where
645674
// abstract/non-instantiated classes sit between the call-site type and
646675
// the concrete leaf implementations (issue #1311).

0 commit comments

Comments
 (0)