gitnexus 1.6.5-rc.48 → 1.6.5-rc.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * C++ conversion-rank scoring for overload resolution (#1578).
3
+ *
4
+ * Operates on **normalized** type strings (output of
5
+ * `normalizeCppParamType` in `arity-metadata.ts`). After normalization:
6
+ * - int/long/short/unsigned → 'int'
7
+ * - float/double → 'double'
8
+ * - char → 'char', bool → 'bool'
9
+ *
10
+ * Because the normalizer collapses promotion pairs (int↔long,
11
+ * float↔double) to the same string, those promotions are invisible at
12
+ * this layer — they appear as exact matches (rank 0).
13
+ *
14
+ * Post-normalization ranking:
15
+ * - rank 0 — exact (same normalized type)
16
+ * - rank 1 — integral promotion (char→int, bool→int)
17
+ * - rank 2 — standard arithmetic conversion (int↔double, char→double,
18
+ * bool→double)
19
+ * - Infinity — mismatch (string↔int, user types, pointers, etc.)
20
+ *
21
+ * This function is intentionally C++-specific (issue #1578 pitfall:
22
+ * keep conversion-rank tables out of shared overload-narrowing). Other
23
+ * languages may define their own `ConversionRankFn` in the future.
24
+ */
25
+ /**
26
+ * Return the conversion rank from `argType` to `paramType`.
27
+ *
28
+ * @returns 0 for exact match, 1 for integral promotion (char/bool→int),
29
+ * 2 for standard arithmetic conversion, Infinity for mismatch.
30
+ */
31
+ export declare function cppConversionRank(argType: string, paramType: string): number;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * C++ conversion-rank scoring for overload resolution (#1578).
3
+ *
4
+ * Operates on **normalized** type strings (output of
5
+ * `normalizeCppParamType` in `arity-metadata.ts`). After normalization:
6
+ * - int/long/short/unsigned → 'int'
7
+ * - float/double → 'double'
8
+ * - char → 'char', bool → 'bool'
9
+ *
10
+ * Because the normalizer collapses promotion pairs (int↔long,
11
+ * float↔double) to the same string, those promotions are invisible at
12
+ * this layer — they appear as exact matches (rank 0).
13
+ *
14
+ * Post-normalization ranking:
15
+ * - rank 0 — exact (same normalized type)
16
+ * - rank 1 — integral promotion (char→int, bool→int)
17
+ * - rank 2 — standard arithmetic conversion (int↔double, char→double,
18
+ * bool→double)
19
+ * - Infinity — mismatch (string↔int, user types, pointers, etc.)
20
+ *
21
+ * This function is intentionally C++-specific (issue #1578 pitfall:
22
+ * keep conversion-rank tables out of shared overload-narrowing). Other
23
+ * languages may define their own `ConversionRankFn` in the future.
24
+ */
25
+ /** Set of normalized arithmetic types that support implicit conversion. */
26
+ const ARITHMETIC = new Set(['int', 'double', 'char', 'bool']);
27
+ /** Integral promotion targets: char→int and bool→int are rank 1. */
28
+ const INTEGRAL_PROMOTION = new Map([
29
+ ['char', 'int'],
30
+ ['bool', 'int'],
31
+ ]);
32
+ /**
33
+ * Return the conversion rank from `argType` to `paramType`.
34
+ *
35
+ * @returns 0 for exact match, 1 for integral promotion (char/bool→int),
36
+ * 2 for standard arithmetic conversion, Infinity for mismatch.
37
+ */
38
+ export function cppConversionRank(argType, paramType) {
39
+ if (argType === paramType)
40
+ return 0;
41
+ // Integral promotions: char→int, bool→int (ISO C++ [conv.prom])
42
+ if (INTEGRAL_PROMOTION.get(argType) === paramType)
43
+ return 1;
44
+ if (ARITHMETIC.has(argType) && ARITHMETIC.has(paramType))
45
+ return 2;
46
+ return Infinity;
47
+ }
@@ -4,6 +4,7 @@ import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js
4
4
  import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
5
5
  import { cppProvider } from '../c-cpp.js';
6
6
  import { cppArityCompatibility } from './arity.js';
7
+ import { cppConversionRank } from './conversion-rank.js';
7
8
  import { cppMergeBindings } from './merge-bindings.js';
8
9
  import { resolveCppImportTarget } from './import-target.js';
9
10
  import { scanCppHeaderFiles } from './header-scan.js';
@@ -140,6 +141,10 @@ export const cppScopeResolver = {
140
141
  propagatesReturnTypesAcrossImports: true,
141
142
  // C++ #include brings in all symbols — enable global free call fallback
142
143
  allowGlobalFreeCallFallback: true,
144
+ // C++ standard-conversion-sequence ranking for overload resolution (#1578).
145
+ // Disambiguates `f(int)` vs `f(double)` called with `f(2.5)` by scoring
146
+ // each candidate's conversion cost; exact match wins over standard conversion.
147
+ conversionRankFn: cppConversionRank,
143
148
  // Range-for element type inference: for (auto& user : users) → bind user to User
144
149
  populateRangeBindings: populateCppRangeBindings,
145
150
  // C++ method return-type bindings need to be visible from module scope
@@ -256,6 +256,7 @@ import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
256
256
  import { LanguageProvider } from '../../language-provider.js';
257
257
  import { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
258
258
  import type { SemanticModel } from '../../model/semantic-model.js';
259
+ import type { ConversionRankFn } from '../passes/overload-narrowing.js';
259
260
  /** A LinearizeStrategy receives the full ancestor map so C3-style
260
261
  * algorithms (which need to merge each parent's MRO) can implement
261
262
  * themselves. Python's depth-first first-seen only consumes
@@ -465,6 +466,19 @@ export interface ScopeResolver {
465
466
  * but is too loose as a default for strict module systems.
466
467
  */
467
468
  readonly allowGlobalFreeCallFallback?: boolean;
469
+ /**
470
+ * Optional per-slot conversion-rank function for overload resolution.
471
+ * When provided, `narrowOverloadCandidates` uses ranked scoring as a
472
+ * fallback when the exact-type filter produces no match. The function
473
+ * returns a numeric cost (0 = exact, 1 = promotion, 2 = standard
474
+ * conversion, Infinity = incompatible) for converting an argument
475
+ * type to a parameter type.
476
+ *
477
+ * The conversion-rank table is language-specific (issue #1578 pitfall:
478
+ * keep it out of shared overload-narrowing). C++ provides
479
+ * `cppConversionRank`; other languages define their own if needed.
480
+ */
481
+ readonly conversionRankFn?: ConversionRankFn;
468
482
  /**
469
483
  * Optional predicate to identify definitions with file-local linkage
470
484
  * (e.g. C `static` functions). When provided, `pickUniqueGlobalCallable`
@@ -22,6 +22,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
22
22
  import type { SemanticModel } from '../../model/semantic-model.js';
23
23
  import type { WorkspaceResolutionIndex } from '../workspace-index.js';
24
24
  import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
25
+ import { type ConversionRankFn } from './overload-narrowing.js';
25
26
  export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, _referenceIndex: {
26
27
  readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
27
28
  }, handledSites: Set<string>, model: SemanticModel, workspaceIndex: WorkspaceResolutionIndex, options?: {
@@ -42,6 +43,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
42
43
  readonly startCol: number;
43
44
  };
44
45
  }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
46
+ readonly conversionRankFn?: ConversionRankFn;
45
47
  }): number;
46
48
  /** Walk up from the call-site scope to the enclosing class scope,
47
49
  * pick a method member by name with overload narrowing on arity +
@@ -61,4 +63,4 @@ export declare function pickImplicitThisOverload(site: {
61
63
  readonly name: string;
62
64
  readonly arity?: number;
63
65
  readonly argumentTypes?: readonly string[];
64
- }, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel): SymbolDefinition | undefined;
66
+ }, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel, conversionRankFn?: ConversionRankFn): SymbolDefinition | undefined;
@@ -17,7 +17,7 @@
17
17
  * generalization plan.
18
18
  */
19
19
  import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
20
- import { findCallableBindingInScope, findCallableBindingsAndAdlBlocker, findClassBindingInScope, } from '../scope/walkers.js';
20
+ import { findAllCallableBindingsInScope, findCallableBindingInScope, findCallableBindingsAndAdlBlocker, findClassBindingInScope, } from '../scope/walkers.js';
21
21
  import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from './overload-narrowing.js';
22
22
  export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _referenceIndex, handledSites, model, workspaceIndex, options = {}) {
23
23
  let emitted = 0;
@@ -45,17 +45,48 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
45
45
  // the same name in a single class, choose the best match by
46
46
  // arity + argument types.
47
47
  if (fnDef === undefined) {
48
- fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model);
48
+ fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, options.conversionRankFn);
49
49
  }
50
+ // Scope-chain callable lookup. First-match preserves scope-chain
51
+ // precedence (local shadows import). When a conversion-rank function
52
+ // is available AND the binding scope contains multiple overloads,
53
+ // refine with `narrowOverloadCandidates` to pick the best overload
54
+ // by argument types (#1578). The first-match result is kept as a
55
+ // fallback when narrowing is indeterminate.
50
56
  if (fnDef === undefined) {
51
57
  if (options.resolveAdlCandidates === undefined) {
58
+ // Non-ADL path: first-match preserves scope-chain precedence
59
+ // (local shadows import). When a conversion-rank function is
60
+ // available AND the binding scope contains multiple overloads,
61
+ // refine with narrowOverloadCandidates (#1578).
52
62
  fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
63
+ if (fnDef !== undefined && options.conversionRankFn !== undefined) {
64
+ const allCallables = findAllCallableBindingsInScope(site.inScope, site.name, scopes);
65
+ if (allCallables.length > 1) {
66
+ const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, options.conversionRankFn);
67
+ if (narrowed.length === 1) {
68
+ fnDef = narrowed[0];
69
+ }
70
+ else if (narrowed.length > 1) {
71
+ // Multiple survivors after conversion-rank scoring.
72
+ // Suppress when all candidates share the same file (true
73
+ // overloads) — mirrors ADL merged-candidate path behavior.
74
+ // Cross-file candidates are shadowing; keep first-match.
75
+ const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
76
+ if (sameFile) {
77
+ handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
78
+ continue;
79
+ }
80
+ }
81
+ // narrowed.length === 0: keep the first-match fnDef —
82
+ // preserves local-shadows-import.
83
+ }
84
+ }
53
85
  }
54
86
  else {
55
- // ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when
56
- // ordinary lookup finds a non-function name (variable, class, enum)
57
- // or a block-scope function declaration (not via using-declaration)
58
- // at the nearest scope where the name exists.
87
+ // ADL path: ISO C++ `[basic.lookup.unqual]` §7 — ADL is suppressed
88
+ // when ordinary lookup finds a non-function name or a block-scope
89
+ // function declaration.
59
90
  const { callables: ordinary, nonCallableFound, blockScopeDeclFound, } = findCallableBindingsAndAdlBlocker(site.inScope, site.name, scopes);
60
91
  const adlSuppressed = nonCallableFound || blockScopeDeclFound;
61
92
  const adl = adlSuppressed
@@ -66,46 +97,63 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
66
97
  argumentTypes: site.argumentTypes,
67
98
  atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
68
99
  }, parsed, scopes, parsedFiles);
69
- // Preserve existing ordinary-lookup behavior when ADL contributed
70
- // no candidates.
100
+ // When ADL contributed no candidates, narrow ordinary candidates
101
+ // with conversion-rank scoring when multiple overloads exist.
102
+ // Single candidate or empty falls through to first-match.
71
103
  if (adl === undefined || adl.length === 0) {
72
- fnDef = ordinary[0];
104
+ if (ordinary.length <= 1 || options.conversionRankFn === undefined) {
105
+ fnDef = ordinary[0];
106
+ }
107
+ else {
108
+ const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
109
+ const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, options.conversionRankFn);
110
+ if (narrowed.length === 1) {
111
+ fnDef = narrowed[0];
112
+ }
113
+ else if (narrowed.length > 1) {
114
+ // Multiple survivors — suppress when same-file (true
115
+ // overloads), mirrors ADL merged-candidate behavior.
116
+ const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
117
+ if (sameFile) {
118
+ handledSites.add(siteKey);
119
+ continue;
120
+ }
121
+ fnDef = ordinary[0]; // cross-file shadowing → first-match
122
+ }
123
+ else {
124
+ fnDef = ordinary[0]; // narrowed empty → first-match
125
+ }
126
+ }
73
127
  }
74
128
  else {
75
129
  const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
76
130
  const merged = [];
77
- const seen = new Set();
131
+ const seenMerge = new Set();
78
132
  const push = (defs) => {
79
133
  for (const d of defs) {
80
- if (seen.has(d.nodeId))
134
+ if (seenMerge.has(d.nodeId))
81
135
  continue;
82
- seen.add(d.nodeId);
136
+ seenMerge.add(d.nodeId);
83
137
  merged.push(d);
84
138
  }
85
139
  };
86
140
  push(ordinary);
87
141
  push(adl);
88
- const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes);
142
+ const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, options.conversionRankFn);
89
143
  if (narrowed.length === 1) {
90
144
  fnDef = narrowed[0];
91
145
  }
92
146
  else if (narrowed.length === 0) {
93
- // ADL contributed candidates, but none survived arity/type
94
- // narrowing. Treat as handled to avoid global-name fallback
95
- // binding to the same mismatched symbol by simple-name
96
- // uniqueness.
97
147
  handledSites.add(siteKey);
98
148
  continue;
99
149
  }
100
150
  else if (narrowed.length > 1) {
101
- // Suppress ambiguous overload calls (emit zero edges) when
102
- // merged ordinary+ADL candidate sets cannot be disambiguated.
103
151
  if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
104
152
  handledSites.add(siteKey);
105
153
  continue;
106
154
  }
107
- // Multiple survivors remain but no conversion-ranking step
108
- // exists yet; suppress instead of picking arbitrarily.
155
+ // Multiple survivors remain after conversion-rank scoring;
156
+ // suppress instead of picking arbitrarily.
109
157
  handledSites.add(siteKey);
110
158
  continue;
111
159
  }
@@ -124,7 +172,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
124
172
  callerScope: site.inScope,
125
173
  scopes,
126
174
  })
127
- : undefined);
175
+ : undefined, site.argumentTypes, options.conversionRankFn);
128
176
  }
129
177
  if (fnDef === undefined)
130
178
  continue;
@@ -157,7 +205,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
157
205
  }
158
206
  return emitted;
159
207
  }
160
- function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLocalDef, callArity, isCallerVisible) {
208
+ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLocalDef, callArity, isCallerVisible, callArgTypes, conversionRankFn) {
161
209
  const scopeDefs = [];
162
210
  const scopeSeen = new Set();
163
211
  for (const def of scopes.defs.byId.values()) {
@@ -195,6 +243,15 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
195
243
  if (arityMatch !== undefined)
196
244
  return arityMatch;
197
245
  }
246
+ // When arity narrowing left >1 candidate, try overload narrowing with
247
+ // argument types + conversion ranking (#1578). This picks the unique
248
+ // best-rank candidate when exact-type or conversion-rank scoring can
249
+ // disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
250
+ if (scopeDefs.length > 1) {
251
+ const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, conversionRankFn);
252
+ if (narrowed.length === 1)
253
+ return narrowed[0];
254
+ }
198
255
  const defs = [];
199
256
  const seen = new Set();
200
257
  const push = (pool) => {
@@ -227,6 +284,12 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
227
284
  if (arityMatch !== undefined)
228
285
  return arityMatch;
229
286
  }
287
+ // Same argument-type + conversion-rank narrowing for the model pool.
288
+ if (defs.length > 1) {
289
+ const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, conversionRankFn);
290
+ if (narrowed.length === 1)
291
+ return narrowed[0];
292
+ }
230
293
  return undefined;
231
294
  }
232
295
  /**
@@ -282,7 +345,7 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
282
345
  * Exported for unit testing — language-agnostic logic, exercised
283
346
  * via synthetic stubs in `pick-implicit-this-overload.test.ts`. The
284
347
  * production call site is `applyFreeCallFallback` immediately above. */
285
- export function pickImplicitThisOverload(site, scopes, workspaceIndex, model) {
348
+ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, conversionRankFn) {
286
349
  // Find the enclosing Class scope by walking parents.
287
350
  let curId = site.inScope;
288
351
  let classScopeId;
@@ -311,7 +374,7 @@ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model) {
311
374
  // ambiguous narrowing (multiple compatible candidates with no
312
375
  // disambiguating signal) leaves the call unresolved rather than
313
376
  // routing to an arbitrary first overload by registration order.
314
- const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes);
377
+ const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, conversionRankFn);
315
378
  if (candidates.length !== 1)
316
379
  return undefined;
317
380
  return candidates[0];
@@ -24,10 +24,28 @@
24
24
  * equality. An empty string in `argTypes[i]` means "unknown" and
25
25
  * counts as a match. Mismatches disqualify. A non-empty typed
26
26
  * result wins; otherwise return the arity-filtered candidates.
27
+ * 4b. When the exact-type filter from step 4 returns empty AND a
28
+ * `conversionRankFn` is provided, rank candidates via pairwise
29
+ * dominance comparison (ISO C++ [over.ics.rank]): F1 beats F2
30
+ * only when F1 is not worse for every arg and better for at
31
+ * least one. Non-dominated candidates are returned; multiple
32
+ * survivors are genuinely ambiguous.
27
33
  * 5. Empty input returns empty output.
28
34
  */
29
35
  import type { SymbolDefinition } from '../../../../_shared/index.js';
30
- export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined): readonly SymbolDefinition[];
36
+ /**
37
+ * Per-slot conversion-rank function. Returns a numeric cost for
38
+ * converting `argType` to `paramType`:
39
+ * - 0 = exact match (no conversion)
40
+ * - 1 = promotion (e.g. char→int, bool→int in C++)
41
+ * - 2 = standard conversion (e.g. int→double)
42
+ * - Infinity = incompatible types
43
+ *
44
+ * Each language provides its own implementation. The function operates
45
+ * on normalized type strings (output of the language's type normalizer).
46
+ */
47
+ export type ConversionRankFn = (argType: string, paramType: string) => number;
48
+ export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined, conversionRankFn?: ConversionRankFn): readonly SymbolDefinition[];
31
49
  /**
32
50
  * Detect when >1 candidate share identical `parameterTypes` after the
33
51
  * per-language normalizer has collapsed distinct underlying types. This
@@ -24,9 +24,15 @@
24
24
  * equality. An empty string in `argTypes[i]` means "unknown" and
25
25
  * counts as a match. Mismatches disqualify. A non-empty typed
26
26
  * result wins; otherwise return the arity-filtered candidates.
27
+ * 4b. When the exact-type filter from step 4 returns empty AND a
28
+ * `conversionRankFn` is provided, rank candidates via pairwise
29
+ * dominance comparison (ISO C++ [over.ics.rank]): F1 beats F2
30
+ * only when F1 is not worse for every arg and better for at
31
+ * least one. Non-dominated candidates are returned; multiple
32
+ * survivors are genuinely ambiguous.
27
33
  * 5. Empty input returns empty output.
28
34
  */
29
- export function narrowOverloadCandidates(overloads, argCount, argTypes) {
35
+ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversionRankFn) {
30
36
  if (overloads.length === 0)
31
37
  return [];
32
38
  const arityMatches = argCount === undefined
@@ -76,9 +82,100 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes) {
76
82
  });
77
83
  if (typed.length > 0)
78
84
  return typed;
85
+ // ── Conversion-rank scoring (step 4b) ──────────────────────────
86
+ // The exact-type filter above rejected every candidate. When a
87
+ // per-language conversion-rank function is available, rank via
88
+ // pairwise dominance: F1 beats F2 only when F1 is not worse for
89
+ // every arg and better for at least one. Non-dominated candidates
90
+ // are returned; multiple survivors are genuinely ambiguous.
91
+ if (conversionRankFn !== undefined) {
92
+ const ranked = rankByConversion(candidates, argTypes, conversionRankFn);
93
+ if (ranked.length > 0)
94
+ return ranked;
95
+ }
79
96
  }
80
97
  return candidates;
81
98
  }
99
+ /**
100
+ * Pairwise dominance comparison (ISO C++ [over.ics.rank]).
101
+ *
102
+ * F1 is a better match than F2 when F1's conversion rank is **not
103
+ * worse** for every argument AND **strictly better** for at least one.
104
+ * Candidates dominated by any other viable candidate are removed.
105
+ * If more than one non-dominated candidate remains, they are genuinely
106
+ * ambiguous — callers suppress the edge rather than picking arbitrarily.
107
+ *
108
+ * Candidates with at least one `Infinity`-ranked slot (incompatible
109
+ * type) are excluded before pairwise comparison begins.
110
+ */
111
+ function rankByConversion(candidates, argTypes, rankFn) {
112
+ // Step 1: compute per-slot ranks and exclude non-viable candidates.
113
+ const viable = [];
114
+ for (const d of candidates) {
115
+ const params = d.parameterTypes;
116
+ if (params === undefined)
117
+ continue;
118
+ const ranks = [];
119
+ let ok = true;
120
+ for (let i = 0; i < argTypes.length && i < params.length; i++) {
121
+ if (argTypes[i] === '') {
122
+ ranks.push(0); // unknown arg → any-match (rank 0)
123
+ continue;
124
+ }
125
+ const r = rankFn(argTypes[i], params[i]);
126
+ if (!isFinite(r)) {
127
+ ok = false;
128
+ break;
129
+ }
130
+ ranks.push(r);
131
+ }
132
+ if (!ok)
133
+ continue;
134
+ viable.push({ def: d, ranks });
135
+ }
136
+ if (viable.length <= 1)
137
+ return viable.map((v) => v.def);
138
+ // Step 2: pairwise dominance — remove candidates dominated by any other.
139
+ const dominated = new Set();
140
+ for (let i = 0; i < viable.length; i++) {
141
+ if (dominated.has(i))
142
+ continue;
143
+ for (let j = i + 1; j < viable.length; j++) {
144
+ if (dominated.has(j))
145
+ continue;
146
+ const cmp = pairwiseCompare(viable[i].ranks, viable[j].ranks);
147
+ if (cmp < 0)
148
+ dominated.add(j); // i dominates j
149
+ else if (cmp > 0)
150
+ dominated.add(i); // j dominates i
151
+ }
152
+ }
153
+ return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
154
+ }
155
+ /**
156
+ * Compare two per-slot rank vectors.
157
+ * Returns -1 if `a` dominates `b` (not worse everywhere, better somewhere),
158
+ * +1 if `b` dominates `a`,
159
+ * 0 if neither dominates (incomparable or equal).
160
+ */
161
+ function pairwiseCompare(a, b) {
162
+ let aBetter = false;
163
+ let bBetter = false;
164
+ const len = Math.min(a.length, b.length);
165
+ for (let i = 0; i < len; i++) {
166
+ if (a[i] < b[i])
167
+ aBetter = true;
168
+ else if (b[i] < a[i])
169
+ bBetter = true;
170
+ if (aBetter && bBetter)
171
+ return 0; // incomparable — early exit
172
+ }
173
+ if (aBetter && !bBetter)
174
+ return -1;
175
+ if (bBetter && !aBetter)
176
+ return 1;
177
+ return 0;
178
+ }
82
179
  /**
83
180
  * Detect when >1 candidate share identical `parameterTypes` after the
84
181
  * per-language normalizer has collapsed distinct underlying types. This
@@ -41,7 +41,7 @@ import type { WorkspaceResolutionIndex } from '../workspace-index.js';
41
41
  /** Subset of `ScopeResolver` consumed by this pass. Accepting the
42
42
  * subset rather than the full provider keeps tests and partial
43
43
  * refactors lighter — callers only need to populate what we read. */
44
- type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass'>;
44
+ type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn'>;
45
45
  export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel): number;
46
46
  /**
47
47
  * Sentinel returned by `pickOverload` when narrowing leaves >1 candidate
@@ -249,7 +249,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
249
249
  for (const ownerId of chain) {
250
250
  const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
251
251
  if (methodOverloads.length > 0) {
252
- const narrowed = narrowOverloadCandidates(methodOverloads, site.arity, site.argumentTypes);
252
+ const narrowed = narrowOverloadCandidates(methodOverloads, site.arity, site.argumentTypes, provider.conversionRankFn);
253
253
  if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
254
254
  ambiguous = true;
255
255
  break;
@@ -262,6 +262,12 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
262
262
  hiddenByName = true;
263
263
  break;
264
264
  }
265
+ // Multiple tied survivors with distinct param types (e.g.
266
+ // h(int,double) vs h(double,int) both scoring 2) → ambiguous.
267
+ if (narrowed.length > 1) {
268
+ ambiguous = true;
269
+ break;
270
+ }
265
271
  memberDef = narrowed[0] ?? methodOverloads[0];
266
272
  break;
267
273
  }
@@ -461,7 +467,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
461
467
  let memberDef;
462
468
  let ambiguous = false;
463
469
  for (const ownerId of chain) {
464
- const picked = pickOverload(ownerId, memberName, site, model);
470
+ const picked = pickOverload(ownerId, memberName, site, model, provider.conversionRankFn);
465
471
  if (picked === OVERLOAD_AMBIGUOUS) {
466
472
  ambiguous = true;
467
473
  break;
@@ -512,7 +518,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
512
518
  * types when multiple overloads share the name. Falls back to the
513
519
  * first-seen def (legacy `findOwnedMember` semantics) when there's
514
520
  * no narrowing signal or when `argumentTypes` is unavailable. */
515
- function pickOverload(ownerId, memberName, site, model) {
521
+ function pickOverload(ownerId, memberName, site, model, conversionRankFn) {
516
522
  const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
517
523
  if (overloads.length === 0) {
518
524
  // Non-callable member (field / property / variable) — ACCESSES
@@ -522,7 +528,7 @@ function pickOverload(ownerId, memberName, site, model) {
522
528
  }
523
529
  if (overloads.length === 1)
524
530
  return overloads[0];
525
- const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes);
531
+ const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, conversionRankFn);
526
532
  // When narrowing leaves >1 candidate that share identical normalized
527
533
  // parameter-types (e.g., C++ `f(int)` vs `f(long)` both collapsed to
528
534
  // `['int']` by `normalizeCppParamType`), suppress the edge entirely.
@@ -531,6 +537,12 @@ function pickOverload(ownerId, memberName, site, model) {
531
537
  // PR #1520 review follow-up plan U2 / Claude review Finding 5.
532
538
  if (isOverloadAmbiguousAfterNormalization(candidates, site.arity))
533
539
  return OVERLOAD_AMBIGUOUS;
540
+ // When conversion-rank scoring leaves >1 tied candidate with distinct
541
+ // parameter types (e.g. h(int,double) vs h(double,int) both scoring 2),
542
+ // suppress rather than picking arbitrarily — C++ would call this
543
+ // ambiguous. Mirrors ADL merged-candidate suppression behavior.
544
+ if (candidates.length > 1)
545
+ return OVERLOAD_AMBIGUOUS;
534
546
  return candidates[0] ?? overloads[0];
535
547
  }
536
548
  /**
@@ -249,6 +249,7 @@ export function runScopeResolution(input, provider) {
249
249
  isFileLocalDef: provider.isFileLocalDef,
250
250
  isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
251
251
  resolveAdlCandidates: provider.resolveAdlCandidates,
252
+ conversionRankFn: provider.conversionRankFn,
252
253
  });
253
254
  const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
254
255
  const importsEmitted = emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.48",
3
+ "version": "1.6.5-rc.49",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",