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.
- package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +31 -0
- package/dist/core/ingestion/languages/cpp/conversion-rank.js +47 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +5 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +14 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +3 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +88 -25
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +19 -1
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +98 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +16 -4
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
56
|
-
// ordinary lookup finds a non-function name
|
|
57
|
-
//
|
|
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
|
-
//
|
|
70
|
-
//
|
|
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
|
-
|
|
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
|
|
131
|
+
const seenMerge = new Set();
|
|
78
132
|
const push = (defs) => {
|
|
79
133
|
for (const d of defs) {
|
|
80
|
-
if (
|
|
134
|
+
if (seenMerge.has(d.nodeId))
|
|
81
135
|
continue;
|
|
82
|
-
|
|
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
|
|
108
|
-
//
|
|
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
|
-
|
|
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