gitnexus 1.6.5 → 1.6.6-rc.1
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/_shared/index.d.ts +1 -1
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +7 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/core/ingestion/language-provider.d.ts +29 -0
- package/dist/core/ingestion/languages/c-cpp.js +46 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.d.ts +8 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +1 -1
- package/dist/core/ingestion/languages/cpp/arity.d.ts +4 -1
- package/dist/core/ingestion/languages/cpp/arity.js +4 -1
- package/dist/core/ingestion/languages/cpp/captures.js +70 -0
- package/dist/core/ingestion/languages/cpp/constraint-extractor.d.ts +73 -0
- package/dist/core/ingestion/languages/cpp/constraint-extractor.js +308 -0
- package/dist/core/ingestion/languages/cpp/constraint-filter.d.ts +31 -0
- package/dist/core/ingestion/languages/cpp/constraint-filter.js +135 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +6 -0
- package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +26 -0
- package/dist/core/ingestion/languages/cpp/type-classifier.js +49 -0
- package/dist/core/ingestion/parsing-processor.js +33 -2
- package/dist/core/ingestion/scope-extractor.js +17 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +50 -21
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
- 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 +10 -4
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +1 -0
- package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
- package/dist/core/ingestion/utils/template-arguments.js +30 -0
- package/package.json +1 -1
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
* Plan that introduced most of these invariants:
|
|
251
251
|
* `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
|
|
252
252
|
*/
|
|
253
|
-
import type { BindingRef, Callsite, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
|
|
253
|
+
import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
|
|
254
254
|
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
255
255
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
256
256
|
import { LanguageProvider } from '../../language-provider.js';
|
|
@@ -264,6 +264,9 @@ import type { ConversionRankFn } from '../passes/overload-narrowing.js';
|
|
|
264
264
|
export type LinearizeStrategy = (classDefId: string, directParents: readonly string[], parentsByDefId: ReadonlyMap<string, readonly string[]>) => string[];
|
|
265
265
|
/** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */
|
|
266
266
|
export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
|
|
267
|
+
/** Re-exported for ScopeResolver consumers — same shape as
|
|
268
|
+
* `RegistryProviders.constraintCompatibility`'s third parameter. */
|
|
269
|
+
export type { ConstraintContext } from '../../../../_shared/index.js';
|
|
267
270
|
export interface ScopeResolver {
|
|
268
271
|
/** Identity for telemetry + per-language flag check. */
|
|
269
272
|
readonly language: SupportedLanguages;
|
|
@@ -337,6 +340,23 @@ export interface ScopeResolver {
|
|
|
337
340
|
* `(def, callsite)` and need an adapter at the wiring site.
|
|
338
341
|
*/
|
|
339
342
|
arityCompatibility(callsite: Callsite, def: SymbolDefinition): ArityVerdict;
|
|
343
|
+
/**
|
|
344
|
+
* Per-language constraint compatibility between a callsite and a
|
|
345
|
+
* candidate `def` that carries `templateConstraints` metadata.
|
|
346
|
+
* Mirrors `arityCompatibility` semantics: the three-valued verdict
|
|
347
|
+
* MUST treat `'unknown'` as keep-candidate (monotonicity — adding
|
|
348
|
+
* a predicate can only narrow correctly, never produce a wrong
|
|
349
|
+
* edge). Consulted by `narrowOverloadCandidates` after the arity
|
|
350
|
+
* and parameter-type filters.
|
|
351
|
+
*
|
|
352
|
+
* Optional. Languages without constrained-overload semantics
|
|
353
|
+
* (SFINAE, `requires` clauses, trait bounds, conditional types)
|
|
354
|
+
* leave this undefined and the constraint filter is a pass-through.
|
|
355
|
+
*
|
|
356
|
+
* C++ is the first consumer; see `languages/cpp/constraint-filter.ts`
|
|
357
|
+
* for the Tier-A predicate registry and Kleene 3-valued evaluator.
|
|
358
|
+
*/
|
|
359
|
+
readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
|
|
340
360
|
/**
|
|
341
361
|
* Compute the method-dispatch order for every Class def in the
|
|
342
362
|
* workspace. Python uses depth-first first-seen via
|
|
@@ -41,6 +41,7 @@ export declare function resolveDefGraphId(filePath: string, def: {
|
|
|
41
41
|
type?: NodeLabel;
|
|
42
42
|
parameterTypes?: readonly string[];
|
|
43
43
|
templateArguments?: readonly string[];
|
|
44
|
+
templateConstraints?: unknown;
|
|
44
45
|
}, nodeLookup: GraphNodeLookup): string | undefined;
|
|
45
46
|
/** Derive the simple (unqualified) name of a def from its `qualifiedName`. */
|
|
46
47
|
export declare function simpleQualifiedName(def: SymbolDefinition): string | undefined;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { generateId } from '../../../../lib/utils.js';
|
|
20
20
|
import { qualifiedKey, simpleKey } from '../graph-bridge/node-lookup.js';
|
|
21
|
+
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
21
22
|
/**
|
|
22
23
|
* Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
|
|
23
24
|
* source ("caller"). A Variable / Property can be the TARGET of an
|
|
@@ -68,6 +69,19 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
|
|
|
68
69
|
if (qn === undefined || qn.length === 0)
|
|
69
70
|
return undefined;
|
|
70
71
|
if (def.type !== undefined) {
|
|
72
|
+
// SFINAE / `requires`-clause disambiguation (issue #1579) — try the
|
|
73
|
+
// constraint-fingerprinted key FIRST. Two function-template overloads
|
|
74
|
+
// with identical `parameterTypes` but mutually-exclusive SFINAE
|
|
75
|
+
// constraints route to their distinct graph nodes via this key.
|
|
76
|
+
// Must run before the parameter-types key because both overloads
|
|
77
|
+
// share the latter.
|
|
78
|
+
if ((def.type === 'Function' || def.type === 'Method') &&
|
|
79
|
+
def.templateConstraints !== undefined) {
|
|
80
|
+
const cKey = qualifiedKey(filePath, def.type, `${qn}${templateConstraintsIdTag(def.templateConstraints)}`);
|
|
81
|
+
const cHit = nodeLookup.get(cKey);
|
|
82
|
+
if (cHit !== undefined)
|
|
83
|
+
return cHit;
|
|
84
|
+
}
|
|
71
85
|
// Overload disambiguation: when the def carries parameter types,
|
|
72
86
|
// try the parameter-typed key first so same-name same-arity
|
|
73
87
|
// overloads route to their distinct graph nodes.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `SymbolDefinition.nodeId` values into the legacy graph-node ID
|
|
18
18
|
* format that downstream consumers (queries, edges, MCP) expect.
|
|
19
19
|
*/
|
|
20
|
+
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
20
21
|
/**
|
|
21
22
|
* Parse a qualified name out of a Function/Method node id.
|
|
22
23
|
*
|
|
@@ -86,6 +87,17 @@ export function buildGraphNodeLookup(graph) {
|
|
|
86
87
|
// Each overload is unique — set unconditionally.
|
|
87
88
|
lookup.set(pKey, node.id);
|
|
88
89
|
}
|
|
90
|
+
// SFINAE / `requires`-clause disambiguation (issue #1579) — register
|
|
91
|
+
// a constraint-fingerprinted key so resolveDefGraphId can locate the
|
|
92
|
+
// correct overload by hashing the def's `templateConstraints`. Mirrors
|
|
93
|
+
// the parameter-types key but keys on the opaque constraint payload
|
|
94
|
+
// instead, separating two `process<T>` overloads whose
|
|
95
|
+
// `parameterTypes=['T']` would otherwise collide.
|
|
96
|
+
const tConstraints = props.templateConstraints;
|
|
97
|
+
if (tConstraints !== undefined && (node.label === 'Function' || node.label === 'Method')) {
|
|
98
|
+
const cKey = qualifiedKey(props.filePath, node.label, `${qualified}${templateConstraintsIdTag(tConstraints)}`);
|
|
99
|
+
lookup.set(cKey, node.id);
|
|
100
|
+
}
|
|
89
101
|
if ((node.label === 'Class' ||
|
|
90
102
|
node.label === 'Struct' ||
|
|
91
103
|
node.label === 'Interface' ||
|
|
@@ -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 { ScopeResolver } from '../contract/scope-resolver.js';
|
|
25
26
|
import { type ConversionRankFn } from './overload-narrowing.js';
|
|
26
27
|
export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, _referenceIndex: {
|
|
27
28
|
readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
|
|
@@ -44,6 +45,12 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
|
|
|
44
45
|
};
|
|
45
46
|
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
|
|
46
47
|
readonly conversionRankFn?: ConversionRankFn;
|
|
48
|
+
/** Optional per-language constraint hook threaded into
|
|
49
|
+
* `narrowOverloadCandidates`. Drops candidates whose template
|
|
50
|
+
* constraints (e.g. C++ `enable_if_t`, C++20 `requires`) provably
|
|
51
|
+
* fail at the call site. Three-valued; `'unknown'` keeps the
|
|
52
|
+
* candidate (monotonicity). */
|
|
53
|
+
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
|
47
54
|
}): number;
|
|
48
55
|
/** Walk up from the call-site scope to the enclosing class scope,
|
|
49
56
|
* pick a method member by name with overload narrowing on arity +
|
|
@@ -63,4 +70,7 @@ export declare function pickImplicitThisOverload(site: {
|
|
|
63
70
|
readonly name: string;
|
|
64
71
|
readonly arity?: number;
|
|
65
72
|
readonly argumentTypes?: readonly string[];
|
|
66
|
-
}, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel,
|
|
73
|
+
}, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel, hookCtx?: {
|
|
74
|
+
readonly conversionRankFn?: ConversionRankFn;
|
|
75
|
+
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
|
76
|
+
}): SymbolDefinition | undefined;
|
|
@@ -45,7 +45,10 @@ 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, {
|
|
49
|
+
conversionRankFn: options.conversionRankFn,
|
|
50
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
// Scope-chain callable lookup. First-match preserves scope-chain
|
|
51
54
|
// precedence (local shadows import). When a conversion-rank function
|
|
@@ -63,7 +66,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
63
66
|
if (fnDef !== undefined && options.conversionRankFn !== undefined) {
|
|
64
67
|
const allCallables = findAllCallableBindingsInScope(site.inScope, site.name, scopes);
|
|
65
68
|
if (allCallables.length > 1) {
|
|
66
|
-
const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes,
|
|
69
|
+
const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, {
|
|
70
|
+
conversionRankFn: options.conversionRankFn,
|
|
71
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
72
|
+
});
|
|
67
73
|
if (narrowed.length === 1) {
|
|
68
74
|
fnDef = narrowed[0];
|
|
69
75
|
}
|
|
@@ -97,36 +103,49 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
97
103
|
argumentTypes: site.argumentTypes,
|
|
98
104
|
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
|
99
105
|
}, parsed, scopes, parsedFiles);
|
|
100
|
-
|
|
101
|
-
// with conversion-rank scoring when multiple overloads exist.
|
|
102
|
-
// Single candidate or empty falls through to first-match.
|
|
106
|
+
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
103
107
|
if (adl === undefined || adl.length === 0) {
|
|
104
|
-
|
|
108
|
+
// No ADL contribution. Default behavior: `ordinary[0]` —
|
|
109
|
+
// scope-chain walk preserves local-shadows-import precedence.
|
|
110
|
+
//
|
|
111
|
+
// Narrowing kicks in when either disambiguation signal is
|
|
112
|
+
// present: any candidate carries `templateConstraints`
|
|
113
|
+
// (SFINAE / `requires`-clause guarded templates, #1579), OR
|
|
114
|
+
// a conversion-rank function is provided (#1606 / #1578).
|
|
115
|
+
// Both hooks are threaded into `narrowOverloadCandidates`
|
|
116
|
+
// via the unified `OverloadNarrowingHookCtx`.
|
|
117
|
+
const hasConstraints = ordinary.some((d) => d.templateConstraints !== undefined);
|
|
118
|
+
const canNarrow = hasConstraints || options.conversionRankFn !== undefined;
|
|
119
|
+
if (ordinary.length <= 1 || !canNarrow) {
|
|
105
120
|
fnDef = ordinary[0];
|
|
106
121
|
}
|
|
107
122
|
else {
|
|
108
|
-
const
|
|
109
|
-
|
|
123
|
+
const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
|
|
124
|
+
conversionRankFn: options.conversionRankFn,
|
|
125
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
126
|
+
});
|
|
110
127
|
if (narrowed.length === 1) {
|
|
111
128
|
fnDef = narrowed[0];
|
|
112
129
|
}
|
|
113
|
-
else if (narrowed.length
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
else if (narrowed.length === 0) {
|
|
131
|
+
handledSites.add(siteKey);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// >1 survivors: same-file → suppress (true overloads,
|
|
136
|
+
// "degrade not lie" — no edge beats a wrong one, and
|
|
137
|
+
// SFINAE-ambiguous calls land here). Cross-file →
|
|
138
|
+
// first-match (shadowing semantics).
|
|
116
139
|
const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
|
|
117
140
|
if (sameFile) {
|
|
118
141
|
handledSites.add(siteKey);
|
|
119
142
|
continue;
|
|
120
143
|
}
|
|
121
|
-
fnDef = ordinary[0];
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
fnDef = ordinary[0]; // narrowed empty → first-match
|
|
144
|
+
fnDef = ordinary[0];
|
|
125
145
|
}
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
else {
|
|
129
|
-
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
130
149
|
const merged = [];
|
|
131
150
|
const seenMerge = new Set();
|
|
132
151
|
const push = (defs) => {
|
|
@@ -139,7 +158,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
139
158
|
};
|
|
140
159
|
push(ordinary);
|
|
141
160
|
push(adl);
|
|
142
|
-
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes,
|
|
161
|
+
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
|
|
162
|
+
conversionRankFn: options.conversionRankFn,
|
|
163
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
164
|
+
});
|
|
143
165
|
if (narrowed.length === 1) {
|
|
144
166
|
fnDef = narrowed[0];
|
|
145
167
|
}
|
|
@@ -248,7 +270,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
|
|
|
248
270
|
// best-rank candidate when exact-type or conversion-rank scoring can
|
|
249
271
|
// disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
|
|
250
272
|
if (scopeDefs.length > 1) {
|
|
251
|
-
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes,
|
|
273
|
+
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
|
|
274
|
+
conversionRankFn,
|
|
275
|
+
});
|
|
252
276
|
if (narrowed.length === 1)
|
|
253
277
|
return narrowed[0];
|
|
254
278
|
}
|
|
@@ -286,7 +310,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
|
|
|
286
310
|
}
|
|
287
311
|
// Same argument-type + conversion-rank narrowing for the model pool.
|
|
288
312
|
if (defs.length > 1) {
|
|
289
|
-
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes,
|
|
313
|
+
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
|
|
314
|
+
conversionRankFn,
|
|
315
|
+
});
|
|
290
316
|
if (narrowed.length === 1)
|
|
291
317
|
return narrowed[0];
|
|
292
318
|
}
|
|
@@ -345,7 +371,7 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
|
|
|
345
371
|
* Exported for unit testing — language-agnostic logic, exercised
|
|
346
372
|
* via synthetic stubs in `pick-implicit-this-overload.test.ts`. The
|
|
347
373
|
* production call site is `applyFreeCallFallback` immediately above. */
|
|
348
|
-
export function pickImplicitThisOverload(site, scopes, workspaceIndex, model,
|
|
374
|
+
export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, hookCtx) {
|
|
349
375
|
// Find the enclosing Class scope by walking parents.
|
|
350
376
|
let curId = site.inScope;
|
|
351
377
|
let classScopeId;
|
|
@@ -374,7 +400,10 @@ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, co
|
|
|
374
400
|
// ambiguous narrowing (multiple compatible candidates with no
|
|
375
401
|
// disambiguating signal) leaves the call unresolved rather than
|
|
376
402
|
// routing to an arbitrary first overload by registration order.
|
|
377
|
-
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes,
|
|
403
|
+
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
|
404
|
+
conversionRankFn: hookCtx?.conversionRankFn,
|
|
405
|
+
constraintCompatibility: hookCtx?.constraintCompatibility,
|
|
406
|
+
});
|
|
378
407
|
if (candidates.length !== 1)
|
|
379
408
|
return undefined;
|
|
380
409
|
return candidates[0];
|
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
* counts as a match. Mismatches disqualify. A non-empty typed
|
|
26
26
|
* result wins; otherwise return the arity-filtered candidates.
|
|
27
27
|
* 4b. When the exact-type filter from step 4 returns empty AND a
|
|
28
|
-
* `conversionRankFn` is provided, rank candidates
|
|
29
|
-
* dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
-
* only when F1 is not worse for every arg and better
|
|
31
|
-
* least one. Non-dominated candidates are returned;
|
|
32
|
-
* survivors are genuinely ambiguous.
|
|
28
|
+
* `conversionRankFn` is provided (via `hookCtx`), rank candidates
|
|
29
|
+
* via pairwise dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
+
* F1 beats F2 only when F1 is not worse for every arg and better
|
|
31
|
+
* for at least one. Non-dominated candidates are returned;
|
|
32
|
+
* multiple survivors are genuinely ambiguous.
|
|
33
|
+
* 4c. Final per-candidate constraint filter (SFINAE / `requires`).
|
|
34
|
+
* When `constraintCompatibility` is provided via `hookCtx`, drop
|
|
35
|
+
* candidates whose template constraints provably fail at the
|
|
36
|
+
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
|
+
* (monotonicity).
|
|
33
38
|
* 5. Empty input returns empty output.
|
|
34
39
|
*/
|
|
35
|
-
import type { SymbolDefinition } from '../../../../_shared/index.js';
|
|
40
|
+
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from '../../../../_shared/index.js';
|
|
36
41
|
/**
|
|
37
42
|
* Per-slot conversion-rank function. Returns a numeric cost for
|
|
38
43
|
* converting `argType` to `paramType`:
|
|
@@ -45,7 +50,25 @@ import type { SymbolDefinition } from '../../../../_shared/index.js';
|
|
|
45
50
|
* on normalized type strings (output of the language's type normalizer).
|
|
46
51
|
*/
|
|
47
52
|
export type ConversionRankFn = (argType: string, paramType: string) => number;
|
|
48
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Optional hook bundle for narrowing extension points. Threaded in
|
|
55
|
+
* from `pickOverload` / `pickImplicitThisOverload` so per-language
|
|
56
|
+
* narrowing can layer in conversion-rank scoring (#1606) and
|
|
57
|
+
* constraint filtering (#1579) without changing the call signature
|
|
58
|
+
* at every site. Each hook is independently optional — leaving both
|
|
59
|
+
* undefined preserves the legacy arity + exact-type behavior.
|
|
60
|
+
*/
|
|
61
|
+
export interface OverloadNarrowingHookCtx {
|
|
62
|
+
/** Conversion-rank scoring fallback (step 4b). Engages when the
|
|
63
|
+
* exact-type filter rejects every candidate. */
|
|
64
|
+
readonly conversionRankFn?: ConversionRankFn;
|
|
65
|
+
/** Constraint filter (step 4c). Drops candidates whose template
|
|
66
|
+
* guards (SFINAE `enable_if_t`, C++20 `requires`, future Rust
|
|
67
|
+
* trait bounds, etc.) provably fail at the call site. Three-valued
|
|
68
|
+
* — `'unknown'` keeps the candidate (monotonicity). */
|
|
69
|
+
readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
|
|
70
|
+
}
|
|
71
|
+
export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined, hookCtx?: OverloadNarrowingHookCtx): readonly SymbolDefinition[];
|
|
49
72
|
/**
|
|
50
73
|
* Detect when >1 candidate share identical `parameterTypes` after the
|
|
51
74
|
* per-language normalizer has collapsed distinct underlying types. This
|
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
* counts as a match. Mismatches disqualify. A non-empty typed
|
|
26
26
|
* result wins; otherwise return the arity-filtered candidates.
|
|
27
27
|
* 4b. When the exact-type filter from step 4 returns empty AND a
|
|
28
|
-
* `conversionRankFn` is provided, rank candidates
|
|
29
|
-
* dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
-
* only when F1 is not worse for every arg and better
|
|
31
|
-
* least one. Non-dominated candidates are returned;
|
|
32
|
-
* survivors are genuinely ambiguous.
|
|
28
|
+
* `conversionRankFn` is provided (via `hookCtx`), rank candidates
|
|
29
|
+
* via pairwise dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
+
* F1 beats F2 only when F1 is not worse for every arg and better
|
|
31
|
+
* for at least one. Non-dominated candidates are returned;
|
|
32
|
+
* multiple survivors are genuinely ambiguous.
|
|
33
|
+
* 4c. Final per-candidate constraint filter (SFINAE / `requires`).
|
|
34
|
+
* When `constraintCompatibility` is provided via `hookCtx`, drop
|
|
35
|
+
* candidates whose template constraints provably fail at the
|
|
36
|
+
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
|
+
* (monotonicity).
|
|
33
38
|
* 5. Empty input returns empty output.
|
|
34
39
|
*/
|
|
35
|
-
export function narrowOverloadCandidates(overloads, argCount, argTypes,
|
|
40
|
+
export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx) {
|
|
36
41
|
if (overloads.length === 0)
|
|
37
42
|
return [];
|
|
38
43
|
const arityMatches = argCount === undefined
|
|
@@ -67,6 +72,7 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
|
|
|
67
72
|
// args).
|
|
68
73
|
const anyUnknownBounds = overloads.some((d) => d.parameterCount === undefined && d.requiredParameterCount === undefined);
|
|
69
74
|
const candidates = arityMatches.length > 0 ? arityMatches : anyUnknownBounds ? overloads : [];
|
|
75
|
+
let result = candidates;
|
|
70
76
|
if (argTypes !== undefined && argTypes.length > 0) {
|
|
71
77
|
const typed = candidates.filter((d) => {
|
|
72
78
|
const params = d.parameterTypes;
|
|
@@ -80,21 +86,46 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
|
|
|
80
86
|
}
|
|
81
87
|
return true;
|
|
82
88
|
});
|
|
83
|
-
if (typed.length > 0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
if (typed.length > 0) {
|
|
90
|
+
result = typed;
|
|
91
|
+
}
|
|
92
|
+
else if (hookCtx?.conversionRankFn !== undefined) {
|
|
93
|
+
// ── Conversion-rank scoring (step 4b) ──────────────────────────
|
|
94
|
+
// The exact-type filter rejected every candidate. Rank via
|
|
95
|
+
// pairwise dominance: F1 beats F2 only when F1 is not worse for
|
|
96
|
+
// every arg and better for at least one. Non-dominated candidates
|
|
97
|
+
// are returned; multiple survivors are genuinely ambiguous. When
|
|
98
|
+
// ranking also yields empty, fall through to the arity-filtered
|
|
99
|
+
// `candidates` set — matches pre-#1606 behavior.
|
|
100
|
+
const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn);
|
|
93
101
|
if (ranked.length > 0)
|
|
94
|
-
|
|
102
|
+
result = ranked;
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
|
-
|
|
105
|
+
// Constraint filter (step 4c; Tier-A — SFINAE / `requires` clauses).
|
|
106
|
+
// Runs after arity, exact-type, and conversion-rank filters so the
|
|
107
|
+
// hook only sees candidates already viable on the other axes.
|
|
108
|
+
// Three-valued: `'compatible'` and `'unknown'` keep the candidate
|
|
109
|
+
// (monotonicity — adding a predicate must never cause a wrong edge);
|
|
110
|
+
// only `'incompatible'` drops it. Candidates without
|
|
111
|
+
// `templateConstraints` are always kept.
|
|
112
|
+
//
|
|
113
|
+
// No fallback to the unconstrained set when this filter empties the
|
|
114
|
+
// candidate list: a fully-`'incompatible'` verdict is authoritative.
|
|
115
|
+
// The downstream `OVERLOAD_AMBIGUOUS` sentinel still guards the empty
|
|
116
|
+
// case, so a buggy hook that wrongly returns `'incompatible'` for
|
|
117
|
+
// every candidate degrades to today's "suppress edge" behavior rather
|
|
118
|
+
// than emitting a wrong edge.
|
|
119
|
+
if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
|
|
120
|
+
const callsite = { arity: argCount };
|
|
121
|
+
const ctx = argTypes !== undefined ? { argumentTypes: argTypes } : {};
|
|
122
|
+
result = result.filter((def) => {
|
|
123
|
+
if (def.templateConstraints === undefined)
|
|
124
|
+
return true;
|
|
125
|
+
return hookCtx.constraintCompatibility(callsite, def, ctx) !== 'incompatible';
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
98
129
|
}
|
|
99
130
|
/**
|
|
100
131
|
* Pairwise dominance comparison (ISO C++ [over.ics.rank]).
|
|
@@ -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' | 'conversionRankFn'>;
|
|
44
|
+
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility'>;
|
|
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,10 @@ 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, {
|
|
253
|
+
conversionRankFn: provider.conversionRankFn,
|
|
254
|
+
constraintCompatibility: provider.constraintCompatibility,
|
|
255
|
+
});
|
|
253
256
|
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
|
|
254
257
|
ambiguous = true;
|
|
255
258
|
break;
|
|
@@ -467,7 +470,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
467
470
|
let memberDef;
|
|
468
471
|
let ambiguous = false;
|
|
469
472
|
for (const ownerId of chain) {
|
|
470
|
-
const picked = pickOverload(ownerId, memberName, site, model, provider
|
|
473
|
+
const picked = pickOverload(ownerId, memberName, site, model, provider);
|
|
471
474
|
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
472
475
|
ambiguous = true;
|
|
473
476
|
break;
|
|
@@ -518,7 +521,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
518
521
|
* types when multiple overloads share the name. Falls back to the
|
|
519
522
|
* first-seen def (legacy `findOwnedMember` semantics) when there's
|
|
520
523
|
* no narrowing signal or when `argumentTypes` is unavailable. */
|
|
521
|
-
function pickOverload(ownerId, memberName, site, model,
|
|
524
|
+
function pickOverload(ownerId, memberName, site, model, provider) {
|
|
522
525
|
const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
|
|
523
526
|
if (overloads.length === 0) {
|
|
524
527
|
// Non-callable member (field / property / variable) — ACCESSES
|
|
@@ -528,7 +531,10 @@ function pickOverload(ownerId, memberName, site, model, conversionRankFn) {
|
|
|
528
531
|
}
|
|
529
532
|
if (overloads.length === 1)
|
|
530
533
|
return overloads[0];
|
|
531
|
-
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes,
|
|
534
|
+
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
|
535
|
+
conversionRankFn: provider.conversionRankFn,
|
|
536
|
+
constraintCompatibility: provider.constraintCompatibility,
|
|
537
|
+
});
|
|
532
538
|
// When narrowing leaves >1 candidate that share identical normalized
|
|
533
539
|
// parameter-types (e.g., C++ `f(int)` vs `f(long)` both collapsed to
|
|
534
540
|
// `['int']` by `normalizeCppParamType`), suppress the edge entirely.
|
|
@@ -250,6 +250,7 @@ export function runScopeResolution(input, provider) {
|
|
|
250
250
|
isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
|
|
251
251
|
resolveAdlCandidates: provider.resolveAdlCandidates,
|
|
252
252
|
conversionRankFn: provider.conversionRankFn,
|
|
253
|
+
constraintCompatibility: provider.constraintCompatibility,
|
|
253
254
|
});
|
|
254
255
|
const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
|
|
255
256
|
const importsEmitted = emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason);
|
|
@@ -9,3 +9,22 @@
|
|
|
9
9
|
export declare function extractTemplateArguments(text: string): string[] | undefined;
|
|
10
10
|
export declare function stripTemplateArguments(text: string): string;
|
|
11
11
|
export declare function templateArgumentsIdTag(templateArguments?: readonly string[]): string;
|
|
12
|
+
/**
|
|
13
|
+
* Stable short hash for the opaque `SymbolDefinition.templateConstraints`
|
|
14
|
+
* payload (issue #1579). Two function-template overloads with identical
|
|
15
|
+
* `parameterTypes` but mutually-exclusive SFINAE constraints
|
|
16
|
+
* (`enable_if_t<is_integral_v<T>>` vs `enable_if_t<is_floating_point_v<T>>`)
|
|
17
|
+
* must produce distinct graph node IDs so the constraint-filter step
|
|
18
|
+
* has two candidates to narrow between. Without this they collapse to
|
|
19
|
+
* a single Function node and the SFINAE golden case can only emit one
|
|
20
|
+
* edge regardless of resolver fixes.
|
|
21
|
+
*
|
|
22
|
+
* FNV-1a 32-bit, base36 encoded. Deterministic; non-cryptographic — the
|
|
23
|
+
* tag's job is collision-avoidance among same-name overloads in one
|
|
24
|
+
* file, not security.
|
|
25
|
+
*/
|
|
26
|
+
export declare function constraintsHash(jsonText: string): string;
|
|
27
|
+
/** Build the `~c:<hash>` ID suffix from an opaque constraint payload.
|
|
28
|
+
* Returns empty string when the payload is absent so callers can
|
|
29
|
+
* string-concatenate unconditionally. */
|
|
30
|
+
export declare function templateConstraintsIdTag(payload: unknown): string;
|
|
@@ -63,3 +63,33 @@ export function templateArgumentsIdTag(templateArguments) {
|
|
|
63
63
|
return '';
|
|
64
64
|
return `~${templateArguments.join(',')}`;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Stable short hash for the opaque `SymbolDefinition.templateConstraints`
|
|
68
|
+
* payload (issue #1579). Two function-template overloads with identical
|
|
69
|
+
* `parameterTypes` but mutually-exclusive SFINAE constraints
|
|
70
|
+
* (`enable_if_t<is_integral_v<T>>` vs `enable_if_t<is_floating_point_v<T>>`)
|
|
71
|
+
* must produce distinct graph node IDs so the constraint-filter step
|
|
72
|
+
* has two candidates to narrow between. Without this they collapse to
|
|
73
|
+
* a single Function node and the SFINAE golden case can only emit one
|
|
74
|
+
* edge regardless of resolver fixes.
|
|
75
|
+
*
|
|
76
|
+
* FNV-1a 32-bit, base36 encoded. Deterministic; non-cryptographic — the
|
|
77
|
+
* tag's job is collision-avoidance among same-name overloads in one
|
|
78
|
+
* file, not security.
|
|
79
|
+
*/
|
|
80
|
+
export function constraintsHash(jsonText) {
|
|
81
|
+
let h = 0x811c9dc5;
|
|
82
|
+
for (let i = 0; i < jsonText.length; i++) {
|
|
83
|
+
h ^= jsonText.charCodeAt(i);
|
|
84
|
+
h = Math.imul(h, 0x01000193);
|
|
85
|
+
}
|
|
86
|
+
return (h >>> 0).toString(36);
|
|
87
|
+
}
|
|
88
|
+
/** Build the `~c:<hash>` ID suffix from an opaque constraint payload.
|
|
89
|
+
* Returns empty string when the payload is absent so callers can
|
|
90
|
+
* string-concatenate unconditionally. */
|
|
91
|
+
export function templateConstraintsIdTag(payload) {
|
|
92
|
+
if (payload === undefined || payload === null)
|
|
93
|
+
return '';
|
|
94
|
+
return `~c:${constraintsHash(JSON.stringify(payload))}`;
|
|
95
|
+
}
|
package/package.json
CHANGED