Skip to content

Commit 893af0f

Browse files
committed
fix: eliminate super-dispatch self-loops in CHA post-pass
runChaPostPass (helpers.ts, WASM path) was emitting self-loop edges like B.m → B.m when expanding class A's implementors: it found B.m via findMethodStmt but had no guard against the case where methodNode.id equals the source_id (the calling method itself). Add the missing guard. runPostNativeThisDispatch (native-orchestrator.ts) similarly lacked a self-loop guard in its targets loop — add it to keep the native path consistent. Also aligns runPostNativeCha to use file-pair-aware confidence (computeConfidence - CHA_DISPATCH_PENALTY) instead of a hardcoded 0.8, matching the WASM path. Gate B now checks the same kind set as Gate A for future-proofing. Remaining jelly-micro divergences filed as separate issues: - #1510: receiver-callee-mixup attribution (findCaller tie-break for same-line defs) - #1511: dynamic pts cross-file edges (fun.js, classes2.js apply/call) - #1512: native missing f.h→f.g this-dispatch for func-prop methods - #1513: prototypes.js receiver edge non-determinism docs check acknowledged: no language, feature, or architecture changes. Closes #1472
1 parent 37bb91a commit 893af0f

2 files changed

Lines changed: 16 additions & 8 deletions

File tree

src/domain/graph/builder/helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ export function runChaPostPass(db: BetterSqlite3Database): number {
511511
const qualifiedName = `${cls}.${methodSuffix}`;
512512
const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
513513
for (const methodNode of methodNodes) {
514+
if (methodNode.id === source_id) continue; // skip self-loops
514515
const key = `${source_id}|${methodNode.id}`;
515516
if (seen.has(key)) continue;
516517
seen.add(key);

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,10 @@ function runPostNativeCha(
523523
if (row) gateAFired = true;
524524
}
525525

526-
// Gate B: calls from changed-file sources to class-kind targets?
526+
// Gate B: calls from changed-file sources to instantiable-kind targets?
527+
// Checks the same kind set as Gate A (class/interface/trait/struct/record)
528+
// so that future CHA extensions to struct/record kinds correctly trigger
529+
// the full scan when RTA evidence grows in a changed file.
527530
let gateBFired = false;
528531
if (!gateAFired) {
529532
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
@@ -534,7 +537,8 @@ function runPostNativeCha(
534537
`SELECT 1 FROM edges e
535538
JOIN nodes src ON e.source_id = src.id
536539
JOIN nodes tgt ON e.target_id = tgt.id
537-
WHERE e.kind = 'calls' AND tgt.kind = 'class'
540+
WHERE e.kind = 'calls'
541+
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record')
538542
AND src.file IN (${ph})
539543
LIMIT 1`,
540544
)
@@ -556,8 +560,8 @@ function runPostNativeCha(
556560
}
557561

558562
// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
559-
// Include caller_file and method_file so affectedFiles can be populated for
560-
// incremental role reclassification; confidence is hardcoded 0.8 matching runChaPostPass.
563+
// Include the caller node's file so confidence can be computed file-pair-aware,
564+
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
561565
// When scopeToChangedFiles is true, restrict to call sites in the changed files
562566
// (safe because no hierarchy or RTA evidence changed outside those files).
563567
let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>;
@@ -653,10 +657,12 @@ function runPostNativeCha(
653657
const key = `${source_id}|${methodNode.id}`;
654658
if (seen.has(key)) continue;
655659
seen.add(key);
656-
// Use the same hardcoded 0.8 that runChaPostPass (helpers.ts) uses for
657-
// DB-level CHA dispatch edges. This aligns the native orchestrator path
658-
// with the WASM and hybrid paths, which both go through runChaPostPass.
659-
const conf = 0.8;
660+
// Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY)
661+
// Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
662+
const conf =
663+
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
664+
CHA_DISPATCH_PENALTY;
665+
if (conf <= 0) continue;
660666
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
661667
newEdgeCount++;
662668
if (caller_file) affectedFiles.add(caller_file);
@@ -900,6 +906,7 @@ async function runPostNativeThisDispatch(
900906
);
901907

902908
for (const t of targets) {
909+
if (t.id === callerRow.id) continue; // skip self-loops
903910
const key = `${callerRow.id}|${t.id}`;
904911
if (seen.has(key)) continue;
905912
seen.add(key);

0 commit comments

Comments
 (0)