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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract C++ template constraint expressions for SFINAE-aware overload
|
|
3
|
+
* narrowing (issue #1579). Recognizes 3 AST shapes:
|
|
4
|
+
*
|
|
5
|
+
* F1 — unqualified non-type template param default:
|
|
6
|
+
* `template<class T, enable_if_t<P, int> = 0> void f(T);`
|
|
7
|
+
* F2 — `std::`-qualified variant (canonical ticket form):
|
|
8
|
+
* `template<class T, std::enable_if_t<P, int> = 0> void f(T);`
|
|
9
|
+
* F4 — C++20 leading requires-clause:
|
|
10
|
+
* `template<class T> requires P void f(T);`
|
|
11
|
+
*
|
|
12
|
+
* Deferred (return `{kind:'unknown'}`):
|
|
13
|
+
* F3 — void-default `typename = enable_if_t<P>` (cppref labels this
|
|
14
|
+
* `/* WRONG *\/` because adjacent overloads collapse to redeclarations)
|
|
15
|
+
* F5 — trailing requires (`void f(T) requires P;`)
|
|
16
|
+
* `requires_expression` blocks (`requires { typename T::U; }`)
|
|
17
|
+
* `decltype(...)`, fold-expressions, user-defined `_v` aliases.
|
|
18
|
+
*
|
|
19
|
+
* The output payload is opaque to shared code — only
|
|
20
|
+
* `constraint-filter.ts` consumes it. See ISO `[temp.constr.normal]` /
|
|
21
|
+
* `<https://en.cppreference.com/w/cpp/language/constraints>` for the
|
|
22
|
+
* normalization the Kleene 3-valued evaluator implements.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Walk a `template_declaration` AST node and extract its constraint
|
|
26
|
+
* payload. Caller is responsible for passing the OUTER `template_declaration`
|
|
27
|
+
* — for class-member template functions, that means the enclosing
|
|
28
|
+
* template_declaration of the class OR of the method, whichever
|
|
29
|
+
* directly precedes the function definition.
|
|
30
|
+
*
|
|
31
|
+
* Returns `undefined` when the template_declaration declares no
|
|
32
|
+
* constraints worth tracking (no enable_if default, no requires clause).
|
|
33
|
+
* Returns a payload whose `expr.kind === 'unknown'` when constraints are
|
|
34
|
+
* present but the extractor cannot model them — monotonicity guarantees
|
|
35
|
+
* the filter keeps the candidate in that case.
|
|
36
|
+
*/
|
|
37
|
+
export function extractCppTemplateConstraints(templateDecl, funcDeclarator) {
|
|
38
|
+
const paramList = childOfType(templateDecl, 'template_parameter_list');
|
|
39
|
+
if (paramList === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
const templateParams = [];
|
|
42
|
+
const exprs = [];
|
|
43
|
+
for (let i = 0; i < paramList.namedChildCount; i++) {
|
|
44
|
+
const param = paramList.namedChild(i);
|
|
45
|
+
if (param === null)
|
|
46
|
+
continue;
|
|
47
|
+
if (param.type === 'type_parameter_declaration' ||
|
|
48
|
+
param.type === 'optional_type_parameter_declaration' ||
|
|
49
|
+
param.type === 'variadic_type_parameter_declaration') {
|
|
50
|
+
const id = firstDescendantOfType(param, 'type_identifier');
|
|
51
|
+
if (id !== null)
|
|
52
|
+
templateParams.push(id.text);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Non-type parameter — F1 / F2 default-value carries the enable_if
|
|
56
|
+
// predicate. Shape: `optional_parameter_declaration` with field
|
|
57
|
+
// `default_value`, whose value is a `template_type` named
|
|
58
|
+
// `enable_if_t` (F1) or a qualified version (F2).
|
|
59
|
+
if (param.type === 'optional_parameter_declaration') {
|
|
60
|
+
const defaultVal = param.childForFieldName('default_value');
|
|
61
|
+
const typeNode = param.childForFieldName('type');
|
|
62
|
+
const candidate = extractEnableIfPredicate(typeNode);
|
|
63
|
+
if (candidate !== undefined) {
|
|
64
|
+
exprs.push(candidate);
|
|
65
|
+
}
|
|
66
|
+
else if (defaultVal !== null) {
|
|
67
|
+
// Default-value-as-predicate not yet supported. Bail conservatively.
|
|
68
|
+
exprs.push({ kind: 'unknown' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// F4 — C++20 leading `requires` clause. Tree-sitter-cpp exposes it as a
|
|
73
|
+
// `requires_clause` child of `template_declaration` (sibling of the
|
|
74
|
+
// template_parameter_list).
|
|
75
|
+
const requiresClause = childOfType(templateDecl, 'requires_clause');
|
|
76
|
+
if (requiresClause !== null) {
|
|
77
|
+
const parsed = parseRequiresClause(requiresClause);
|
|
78
|
+
if (parsed !== undefined)
|
|
79
|
+
exprs.push(parsed);
|
|
80
|
+
}
|
|
81
|
+
if (templateParams.length === 0 && exprs.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const paramArgIndex = buildParamArgIndex(templateParams, funcDeclarator);
|
|
84
|
+
const expr = exprs.length === 0
|
|
85
|
+
? { kind: 'unknown' }
|
|
86
|
+
: exprs.length === 1
|
|
87
|
+
? exprs[0]
|
|
88
|
+
: { kind: 'and', children: exprs };
|
|
89
|
+
return { templateParams, paramArgIndex, expr };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Inspect a non-type template parameter's declared type to see whether
|
|
93
|
+
* it's `enable_if_t<P, T>` (F1) or `std::enable_if_t<P, T>` (F2). When
|
|
94
|
+
* matched, extract the predicate `P` and return it as a `ConstraintExpr`.
|
|
95
|
+
*
|
|
96
|
+
* Returns undefined when the parameter's type is not enable_if (so the
|
|
97
|
+
* caller can decide whether to bail or ignore).
|
|
98
|
+
*/
|
|
99
|
+
function extractEnableIfPredicate(typeNode) {
|
|
100
|
+
if (typeNode === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
// Unwrap a type_descriptor wrapper (when present).
|
|
103
|
+
let t = typeNode;
|
|
104
|
+
if (t.type === 'type_descriptor') {
|
|
105
|
+
t = t.childForFieldName('type') ?? firstDescendantOfType(t, 'template_type');
|
|
106
|
+
}
|
|
107
|
+
// F2 shape: tree-sitter-cpp models `std::enable_if_t<...>` as
|
|
108
|
+
// `qualified_identifier` whose `name` field is the `template_type`.
|
|
109
|
+
// F1 shape (unqualified `enable_if_t<...>`) is `template_type` directly.
|
|
110
|
+
if (t !== null && t.type === 'qualified_identifier') {
|
|
111
|
+
const inner = t.childForFieldName('name') ?? firstDescendantOfType(t, 'template_type');
|
|
112
|
+
if (inner !== null && inner.type === 'template_type') {
|
|
113
|
+
t = inner;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (t === null || t.type !== 'template_type')
|
|
117
|
+
return undefined;
|
|
118
|
+
const nameNode = t.childForFieldName('name');
|
|
119
|
+
if (nameNode === null)
|
|
120
|
+
return undefined;
|
|
121
|
+
const tail = stripQualifiedPrefix(nameNode.text);
|
|
122
|
+
if (tail !== 'enable_if_t' && tail !== 'enable_if')
|
|
123
|
+
return undefined;
|
|
124
|
+
// Predicate is the first template argument of enable_if_t.
|
|
125
|
+
const argList = t.childForFieldName('arguments') ?? childOfType(t, 'template_argument_list');
|
|
126
|
+
if (argList === null)
|
|
127
|
+
return { kind: 'unknown' };
|
|
128
|
+
for (let i = 0; i < argList.namedChildCount; i++) {
|
|
129
|
+
const arg = argList.namedChild(i);
|
|
130
|
+
if (arg === null)
|
|
131
|
+
continue;
|
|
132
|
+
if (arg.type !== 'type_descriptor')
|
|
133
|
+
continue;
|
|
134
|
+
const inner = arg.childForFieldName('type') ?? arg.namedChild(0);
|
|
135
|
+
if (inner === null)
|
|
136
|
+
continue;
|
|
137
|
+
return parseAtomicOrBoolean(inner);
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'unknown' };
|
|
140
|
+
}
|
|
141
|
+
/** Parse a requires-clause body. The body is a binary or unary expression
|
|
142
|
+
* over atomic predicates (variable templates like `is_integral_v<T>`). */
|
|
143
|
+
function parseRequiresClause(requiresClause) {
|
|
144
|
+
// tree-sitter-cpp exposes the expression as a named child or via a
|
|
145
|
+
// `constraint` field. Probe both.
|
|
146
|
+
let expr = requiresClause.childForFieldName('constraint');
|
|
147
|
+
if (expr === null) {
|
|
148
|
+
for (let i = 0; i < requiresClause.namedChildCount; i++) {
|
|
149
|
+
const c = requiresClause.namedChild(i);
|
|
150
|
+
if (c === null)
|
|
151
|
+
continue;
|
|
152
|
+
// Skip the `requires` keyword token.
|
|
153
|
+
if (c.type === 'requires')
|
|
154
|
+
continue;
|
|
155
|
+
expr = c;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (expr === null)
|
|
160
|
+
return undefined;
|
|
161
|
+
return parseAtomicOrBoolean(expr);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Recursively parse a constraint sub-expression. Recognizes:
|
|
165
|
+
* - `template_type` / `template_function` named `<predicate>_v` → atomic
|
|
166
|
+
* - binary_expression with `&&` / `||` → conjunction / disjunction
|
|
167
|
+
* - unary_expression with `!` → negation
|
|
168
|
+
* - parenthesized_expression → unwrap
|
|
169
|
+
* - anything else → `{kind:'unknown'}` (monotonicity-safe)
|
|
170
|
+
*
|
|
171
|
+
* `requires_expression` blocks intentionally fall through to 'unknown'
|
|
172
|
+
* — they need substitution semantics we don't model in V1.
|
|
173
|
+
*/
|
|
174
|
+
function parseAtomicOrBoolean(node) {
|
|
175
|
+
// Unwrap parentheses.
|
|
176
|
+
if (node.type === 'parenthesized_expression') {
|
|
177
|
+
const inner = node.namedChild(0);
|
|
178
|
+
return inner === null ? { kind: 'unknown' } : parseAtomicOrBoolean(inner);
|
|
179
|
+
}
|
|
180
|
+
// Boolean composition.
|
|
181
|
+
if (node.type === 'binary_expression') {
|
|
182
|
+
const left = node.childForFieldName('left');
|
|
183
|
+
const right = node.childForFieldName('right');
|
|
184
|
+
const opNode = node.childForFieldName('operator');
|
|
185
|
+
if (left !== null && right !== null && opNode !== null) {
|
|
186
|
+
const op = opNode.text;
|
|
187
|
+
const l = parseAtomicOrBoolean(left);
|
|
188
|
+
const r = parseAtomicOrBoolean(right);
|
|
189
|
+
if (op === '&&')
|
|
190
|
+
return { kind: 'and', children: [l, r] };
|
|
191
|
+
if (op === '||')
|
|
192
|
+
return { kind: 'or', children: [l, r] };
|
|
193
|
+
}
|
|
194
|
+
return { kind: 'unknown' };
|
|
195
|
+
}
|
|
196
|
+
if (node.type === 'unary_expression') {
|
|
197
|
+
const opNode = node.childForFieldName('operator') ?? node.namedChild(0);
|
|
198
|
+
const arg = node.childForFieldName('argument') ?? node.namedChild(1) ?? node.namedChild(0);
|
|
199
|
+
if (opNode !== null && opNode.text === '!' && arg !== null && arg !== opNode) {
|
|
200
|
+
return { kind: 'not', child: parseAtomicOrBoolean(arg) };
|
|
201
|
+
}
|
|
202
|
+
return { kind: 'unknown' };
|
|
203
|
+
}
|
|
204
|
+
// Atomic predicate — `template_type` is the typical shape for variable
|
|
205
|
+
// templates like `is_integral_v<T>`. Some grammar variants surface it as
|
|
206
|
+
// `template_function` or via a `qualified_identifier` wrapper.
|
|
207
|
+
if (node.type === 'template_type' || node.type === 'template_function') {
|
|
208
|
+
return parseAtomicTemplate(node);
|
|
209
|
+
}
|
|
210
|
+
if (node.type === 'qualified_identifier') {
|
|
211
|
+
// `std::is_integral_v<T>` shape (without template_type wrapping).
|
|
212
|
+
const inner = node.childForFieldName('name');
|
|
213
|
+
if (inner !== null && (inner.type === 'template_type' || inner.type === 'template_function')) {
|
|
214
|
+
return parseAtomicTemplate(inner);
|
|
215
|
+
}
|
|
216
|
+
return { kind: 'unknown' };
|
|
217
|
+
}
|
|
218
|
+
// `requires { typename T::U; }` blocks and decltype: out of V1 scope.
|
|
219
|
+
return { kind: 'unknown' };
|
|
220
|
+
}
|
|
221
|
+
function parseAtomicTemplate(t) {
|
|
222
|
+
const nameNode = t.childForFieldName('name');
|
|
223
|
+
if (nameNode === null)
|
|
224
|
+
return { kind: 'unknown' };
|
|
225
|
+
const name = stripQualifiedPrefix(nameNode.text);
|
|
226
|
+
const argList = t.childForFieldName('arguments') ?? childOfType(t, 'template_argument_list');
|
|
227
|
+
const args = [];
|
|
228
|
+
if (argList !== null) {
|
|
229
|
+
for (let i = 0; i < argList.namedChildCount; i++) {
|
|
230
|
+
const arg = argList.namedChild(i);
|
|
231
|
+
if (arg === null)
|
|
232
|
+
continue;
|
|
233
|
+
if (arg.type !== 'type_descriptor')
|
|
234
|
+
continue;
|
|
235
|
+
const inner = arg.childForFieldName('type') ?? arg.namedChild(0);
|
|
236
|
+
if (inner === null)
|
|
237
|
+
continue;
|
|
238
|
+
// For Tier-A predicates the args are bare template-parameter names
|
|
239
|
+
// (`T`, `U`). Anything more elaborate is bailed via 'unknown' at the
|
|
240
|
+
// top level if needed; here we just record the textual identifier.
|
|
241
|
+
const id = inner.type === 'type_identifier' ? inner : firstDescendantOfType(inner, 'type_identifier');
|
|
242
|
+
args.push(id !== null ? id.text : inner.text);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { kind: 'atomic', name, args };
|
|
246
|
+
}
|
|
247
|
+
/** Build a `paramName → call-site argument index` map by scanning the
|
|
248
|
+
* function's parameter list for parameters typed by each template param. */
|
|
249
|
+
function buildParamArgIndex(templateParams, funcDeclarator) {
|
|
250
|
+
const out = {};
|
|
251
|
+
if (funcDeclarator === null || templateParams.length === 0)
|
|
252
|
+
return out;
|
|
253
|
+
const paramList = funcDeclarator.childForFieldName('parameters');
|
|
254
|
+
if (paramList === null)
|
|
255
|
+
return out;
|
|
256
|
+
let argIdx = 0;
|
|
257
|
+
for (let i = 0; i < paramList.childCount; i++) {
|
|
258
|
+
const p = paramList.child(i);
|
|
259
|
+
if (p === null)
|
|
260
|
+
continue;
|
|
261
|
+
if (p.type !== 'parameter_declaration' &&
|
|
262
|
+
p.type !== 'optional_parameter_declaration' &&
|
|
263
|
+
p.type !== 'variadic_parameter_declaration') {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const typeNode = p.childForFieldName('type');
|
|
267
|
+
if (typeNode !== null) {
|
|
268
|
+
const tname = bareTypeIdentifier(typeNode);
|
|
269
|
+
if (tname !== null && templateParams.includes(tname) && !(tname in out)) {
|
|
270
|
+
out[tname] = argIdx;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
argIdx++;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function bareTypeIdentifier(typeNode) {
|
|
278
|
+
if (typeNode.type === 'type_identifier')
|
|
279
|
+
return typeNode.text;
|
|
280
|
+
// Allow `T const`, `T&`, `T*` shapes — the inner type_identifier still wins.
|
|
281
|
+
const id = firstDescendantOfType(typeNode, 'type_identifier');
|
|
282
|
+
return id !== null ? id.text : null;
|
|
283
|
+
}
|
|
284
|
+
function stripQualifiedPrefix(text) {
|
|
285
|
+
const idx = text.lastIndexOf('::');
|
|
286
|
+
return idx >= 0 ? text.slice(idx + 2) : text;
|
|
287
|
+
}
|
|
288
|
+
function childOfType(node, type) {
|
|
289
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
290
|
+
const c = node.child(i);
|
|
291
|
+
if (c !== null && c.type === type)
|
|
292
|
+
return c;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function firstDescendantOfType(node, type) {
|
|
297
|
+
if (node.type === type)
|
|
298
|
+
return node;
|
|
299
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
300
|
+
const c = node.child(i);
|
|
301
|
+
if (c === null)
|
|
302
|
+
continue;
|
|
303
|
+
const hit = firstDescendantOfType(c, type);
|
|
304
|
+
if (hit !== null)
|
|
305
|
+
return hit;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kleene 3-valued evaluator + curated 4-predicate registry +
|
|
3
|
+
* `cppConstraintCompatibility` hook export for SFINAE / `requires`-clause
|
|
4
|
+
* filtering (issue #1579).
|
|
5
|
+
*
|
|
6
|
+
* Semantics:
|
|
7
|
+
* - `'incompatible'` → predicate provably fails for these argumentTypes
|
|
8
|
+
* (ISO `[temp.constr.atomic]` "not satisfied")
|
|
9
|
+
* - `'compatible'` → predicate provably holds
|
|
10
|
+
* - `'unknown'` → cannot decide (missing arg-type info, predicate
|
|
11
|
+
* not in registry, AST shape bailed during extraction). The shared
|
|
12
|
+
* filter keeps the candidate on `'unknown'` — monotonicity guarantee.
|
|
13
|
+
*
|
|
14
|
+
* Kleene rules (extension of ISO's 2-valued short-circuit conjunction in
|
|
15
|
+
* `<https://en.cppreference.com/w/cpp/language/constraints>`):
|
|
16
|
+
* AND: incompatible if any child incompatible; compatible iff all
|
|
17
|
+
* children compatible; otherwise unknown.
|
|
18
|
+
* OR: compatible if any child compatible; incompatible iff all
|
|
19
|
+
* children incompatible; otherwise unknown.
|
|
20
|
+
* NOT: flip compatible↔incompatible; pass through unknown.
|
|
21
|
+
*/
|
|
22
|
+
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from '../../../../_shared/index.js';
|
|
23
|
+
import type { ConstraintExpr, CppConstraintPayload } from './constraint-extractor.js';
|
|
24
|
+
/** Public surface — registered as `ScopeResolver.constraintCompatibility`. */
|
|
25
|
+
export declare function cppConstraintCompatibility(_callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext): ArityVerdict;
|
|
26
|
+
/** Exposed for unit tests — lets `cpp-constraint.test.ts` assert
|
|
27
|
+
* `expect(getRegistrySize()).toBe(4)` without exporting the Map itself. */
|
|
28
|
+
export declare function getRegistrySize(): number;
|
|
29
|
+
/** Exposed for unit tests covering the Kleene 3-valued truth table
|
|
30
|
+
* directly, without an AST round-trip. */
|
|
31
|
+
export declare function evaluateForTest(expr: ConstraintExpr, payload: CppConstraintPayload, ctx: ConstraintContext): ArityVerdict;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kleene 3-valued evaluator + curated 4-predicate registry +
|
|
3
|
+
* `cppConstraintCompatibility` hook export for SFINAE / `requires`-clause
|
|
4
|
+
* filtering (issue #1579).
|
|
5
|
+
*
|
|
6
|
+
* Semantics:
|
|
7
|
+
* - `'incompatible'` → predicate provably fails for these argumentTypes
|
|
8
|
+
* (ISO `[temp.constr.atomic]` "not satisfied")
|
|
9
|
+
* - `'compatible'` → predicate provably holds
|
|
10
|
+
* - `'unknown'` → cannot decide (missing arg-type info, predicate
|
|
11
|
+
* not in registry, AST shape bailed during extraction). The shared
|
|
12
|
+
* filter keeps the candidate on `'unknown'` — monotonicity guarantee.
|
|
13
|
+
*
|
|
14
|
+
* Kleene rules (extension of ISO's 2-valued short-circuit conjunction in
|
|
15
|
+
* `<https://en.cppreference.com/w/cpp/language/constraints>`):
|
|
16
|
+
* AND: incompatible if any child incompatible; compatible iff all
|
|
17
|
+
* children compatible; otherwise unknown.
|
|
18
|
+
* OR: compatible if any child compatible; incompatible iff all
|
|
19
|
+
* children incompatible; otherwise unknown.
|
|
20
|
+
* NOT: flip compatible↔incompatible; pass through unknown.
|
|
21
|
+
*/
|
|
22
|
+
import { classifyType } from './type-classifier.js';
|
|
23
|
+
/**
|
|
24
|
+
* Curated Tier-A predicate registry — the four canonical
|
|
25
|
+
* `<type_traits>` variable templates whose truth tables are closed-form
|
|
26
|
+
* over our coarse `TypeClass` enum.
|
|
27
|
+
*
|
|
28
|
+
* Deferred predicates that need a cv/ref/pointer sidecar on
|
|
29
|
+
* `normalizeCppParamType` (today the normalizer strips those markers
|
|
30
|
+
* before storage) live in #1579 as one-line follow-up adds.
|
|
31
|
+
*/
|
|
32
|
+
// ISO `<type_traits>` treats `bool`, `char`, and the signed/unsigned char
|
|
33
|
+
// variants as integral types (§21.3.4 Table 48), so `is_integral_v<bool>`
|
|
34
|
+
// and `is_integral_v<char>` must both yield `true`. We keep the `TypeClass`
|
|
35
|
+
// enum precise (separate `'bool'` / `'char'` buckets) so that
|
|
36
|
+
// `is_same_v<bool, int>` still resolves to `'incompatible'`; the integral-
|
|
37
|
+
// family widening lives here in the predicate evaluators instead.
|
|
38
|
+
function isIntegralClass(c) {
|
|
39
|
+
return c === 'integral' || c === 'bool' || c === 'char';
|
|
40
|
+
}
|
|
41
|
+
const REGISTRY = new Map([
|
|
42
|
+
['is_integral_v', (cls) => verdictFromBool(isIntegralClass(cls[0]), cls)],
|
|
43
|
+
['is_floating_point_v', (cls) => verdictFromBool(cls[0] === 'floating', cls)],
|
|
44
|
+
[
|
|
45
|
+
'is_arithmetic_v',
|
|
46
|
+
(cls) => verdictFromBool(isIntegralClass(cls[0]) || cls[0] === 'floating', cls),
|
|
47
|
+
],
|
|
48
|
+
// NOTE: cv-qualifiers are stripped by `normalizeCppParamType` before the
|
|
49
|
+
// type token reaches `classifyType`, so `is_same_v<const T, T>` returns
|
|
50
|
+
// `'compatible'` instead of the ISO-correct `false`. Tracked under the
|
|
51
|
+
// cv-sidecar refactor in #1579's "Out of scope" list; until that lands
|
|
52
|
+
// this approximation matches the common `is_same_v<T, ConcreteType>`
|
|
53
|
+
// dispatch idiom and silently degrades on cv-distinct compares.
|
|
54
|
+
[
|
|
55
|
+
'is_same_v',
|
|
56
|
+
(cls) => {
|
|
57
|
+
if (cls.length < 2 || cls[0] === 'unknown' || cls[1] === 'unknown')
|
|
58
|
+
return 'unknown';
|
|
59
|
+
return cls[0] === cls[1] ? 'compatible' : 'incompatible';
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
]);
|
|
63
|
+
function verdictFromBool(predicate, cls) {
|
|
64
|
+
if (cls[0] === 'unknown')
|
|
65
|
+
return 'unknown';
|
|
66
|
+
return predicate ? 'compatible' : 'incompatible';
|
|
67
|
+
}
|
|
68
|
+
/** Public surface — registered as `ScopeResolver.constraintCompatibility`. */
|
|
69
|
+
export function cppConstraintCompatibility(_callsite, def, ctx) {
|
|
70
|
+
const payload = def.templateConstraints;
|
|
71
|
+
if (payload === undefined)
|
|
72
|
+
return 'unknown';
|
|
73
|
+
return evaluate(payload.expr, payload, ctx);
|
|
74
|
+
}
|
|
75
|
+
function evaluate(expr, payload, ctx) {
|
|
76
|
+
switch (expr.kind) {
|
|
77
|
+
case 'unknown':
|
|
78
|
+
return 'unknown';
|
|
79
|
+
case 'atomic': {
|
|
80
|
+
const evaluator = REGISTRY.get(expr.name);
|
|
81
|
+
if (evaluator === undefined)
|
|
82
|
+
return 'unknown';
|
|
83
|
+
const classes = expr.args.map((paramName) => {
|
|
84
|
+
const argIdx = payload.paramArgIndex[paramName];
|
|
85
|
+
if (argIdx === undefined)
|
|
86
|
+
return 'unknown';
|
|
87
|
+
const token = ctx.argumentTypes?.[argIdx];
|
|
88
|
+
if (token === undefined || token === '')
|
|
89
|
+
return 'unknown';
|
|
90
|
+
return classifyType(token);
|
|
91
|
+
});
|
|
92
|
+
return evaluator(classes);
|
|
93
|
+
}
|
|
94
|
+
case 'and': {
|
|
95
|
+
let result = 'compatible';
|
|
96
|
+
for (const child of expr.children) {
|
|
97
|
+
const v = evaluate(child, payload, ctx);
|
|
98
|
+
if (v === 'incompatible')
|
|
99
|
+
return 'incompatible';
|
|
100
|
+
if (v === 'unknown')
|
|
101
|
+
result = 'unknown';
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
case 'or': {
|
|
106
|
+
let result = 'incompatible';
|
|
107
|
+
for (const child of expr.children) {
|
|
108
|
+
const v = evaluate(child, payload, ctx);
|
|
109
|
+
if (v === 'compatible')
|
|
110
|
+
return 'compatible';
|
|
111
|
+
if (v === 'unknown')
|
|
112
|
+
result = 'unknown';
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
case 'not': {
|
|
117
|
+
const v = evaluate(expr.child, payload, ctx);
|
|
118
|
+
if (v === 'compatible')
|
|
119
|
+
return 'incompatible';
|
|
120
|
+
if (v === 'incompatible')
|
|
121
|
+
return 'compatible';
|
|
122
|
+
return 'unknown';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Exposed for unit tests — lets `cpp-constraint.test.ts` assert
|
|
127
|
+
* `expect(getRegistrySize()).toBe(4)` without exporting the Map itself. */
|
|
128
|
+
export function getRegistrySize() {
|
|
129
|
+
return REGISTRY.size;
|
|
130
|
+
}
|
|
131
|
+
/** Exposed for unit tests covering the Kleene 3-valued truth table
|
|
132
|
+
* directly, without an AST round-trip. */
|
|
133
|
+
export function evaluateForTest(expr, payload, ctx) {
|
|
134
|
+
return evaluate(expr, payload, ctx);
|
|
135
|
+
}
|
|
@@ -13,6 +13,7 @@ import { populateCppDependentBases, clearCppDependentBases, isCppDependentBaseMe
|
|
|
13
13
|
import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates } from './adl.js';
|
|
14
14
|
import { clearCppInlineNamespaces, populateCppInlineNamespaceScopes, resolveCppQualifiedNamespaceMember, } from './inline-namespaces.js';
|
|
15
15
|
import { populateCppRangeBindings } from './range-bindings.js';
|
|
16
|
+
import { cppConstraintCompatibility } from './constraint-filter.js';
|
|
16
17
|
/**
|
|
17
18
|
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
|
18
19
|
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
|
@@ -58,6 +59,11 @@ export const cppScopeResolver = {
|
|
|
58
59
|
// Adapter: cppArityCompatibility predates ScopeResolver and uses
|
|
59
60
|
// (def, callsite). ScopeResolver contract is (callsite, def).
|
|
60
61
|
arityCompatibility: (callsite, def) => cppArityCompatibility(def, callsite),
|
|
62
|
+
// SFINAE / `requires`-clause aware overload filter (issue #1579).
|
|
63
|
+
// Drops candidates whose template constraints (`enable_if_t<P, T>`,
|
|
64
|
+
// C++20 `requires P`) provably fail at the call site. Three-valued —
|
|
65
|
+
// `'unknown'` keeps the candidate, preserving "degrade not lie".
|
|
66
|
+
constraintCompatibility: cppConstraintCompatibility,
|
|
61
67
|
buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
|
62
68
|
populateOwners: (parsed) => {
|
|
63
69
|
populateClassOwnedMembers(parsed);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse-grained type classifier for C++ constraint evaluation
|
|
3
|
+
* (`<https://en.cppreference.com/w/cpp/types/is_integral>`,
|
|
4
|
+
* `<https://en.cppreference.com/w/cpp/types/is_floating_point>`).
|
|
5
|
+
*
|
|
6
|
+
* Maps a normalized type token (as produced by `normalizeCppParamType` /
|
|
7
|
+
* the call-site inference in `captures.ts`) to one of the categories
|
|
8
|
+
* the `<type_traits>` predicate registry uses for SFINAE filtering.
|
|
9
|
+
*
|
|
10
|
+
* Intentionally coarse: cv / pointer / reference qualifiers are stripped
|
|
11
|
+
* upstream by `normalizeCppParamType`. Tier-A predicates
|
|
12
|
+
* (`is_integral_v`, `is_floating_point_v`, `is_arithmetic_v`, `is_same_v`)
|
|
13
|
+
* are insensitive to those modifiers per ISO `<type_traits>` semantics
|
|
14
|
+
* ("including any cv-qualified variants").
|
|
15
|
+
*/
|
|
16
|
+
export type TypeClass = 'integral' | 'floating' | 'bool' | 'char' | 'string' | 'null' | 'class' | 'unknown';
|
|
17
|
+
/**
|
|
18
|
+
* Classify a normalized C++ type token. The mapping mirrors the literal-
|
|
19
|
+
* inference table in `captures.ts:inferCppLiteralType` plus the std::
|
|
20
|
+
* normalization in `arity-metadata.ts:normalizeCppParamType`.
|
|
21
|
+
*
|
|
22
|
+
* Caller note: token must already be normalized (no `const`, no `&` / `*`,
|
|
23
|
+
* no `std::` prefix). Tokens passed via `ConstraintContext.argumentTypes`
|
|
24
|
+
* coming from `inferCppCallArgTypes` satisfy this.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyType(token: string): TypeClass;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse-grained type classifier for C++ constraint evaluation
|
|
3
|
+
* (`<https://en.cppreference.com/w/cpp/types/is_integral>`,
|
|
4
|
+
* `<https://en.cppreference.com/w/cpp/types/is_floating_point>`).
|
|
5
|
+
*
|
|
6
|
+
* Maps a normalized type token (as produced by `normalizeCppParamType` /
|
|
7
|
+
* the call-site inference in `captures.ts`) to one of the categories
|
|
8
|
+
* the `<type_traits>` predicate registry uses for SFINAE filtering.
|
|
9
|
+
*
|
|
10
|
+
* Intentionally coarse: cv / pointer / reference qualifiers are stripped
|
|
11
|
+
* upstream by `normalizeCppParamType`. Tier-A predicates
|
|
12
|
+
* (`is_integral_v`, `is_floating_point_v`, `is_arithmetic_v`, `is_same_v`)
|
|
13
|
+
* are insensitive to those modifiers per ISO `<type_traits>` semantics
|
|
14
|
+
* ("including any cv-qualified variants").
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Classify a normalized C++ type token. The mapping mirrors the literal-
|
|
18
|
+
* inference table in `captures.ts:inferCppLiteralType` plus the std::
|
|
19
|
+
* normalization in `arity-metadata.ts:normalizeCppParamType`.
|
|
20
|
+
*
|
|
21
|
+
* Caller note: token must already be normalized (no `const`, no `&` / `*`,
|
|
22
|
+
* no `std::` prefix). Tokens passed via `ConstraintContext.argumentTypes`
|
|
23
|
+
* coming from `inferCppCallArgTypes` satisfy this.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyType(token) {
|
|
26
|
+
if (token.length === 0)
|
|
27
|
+
return 'unknown';
|
|
28
|
+
switch (token) {
|
|
29
|
+
case 'int':
|
|
30
|
+
return 'integral';
|
|
31
|
+
case 'double':
|
|
32
|
+
case 'float':
|
|
33
|
+
return 'floating';
|
|
34
|
+
case 'bool':
|
|
35
|
+
return 'bool';
|
|
36
|
+
case 'char':
|
|
37
|
+
return 'char';
|
|
38
|
+
case 'string':
|
|
39
|
+
return 'string';
|
|
40
|
+
case 'null':
|
|
41
|
+
return 'null';
|
|
42
|
+
default:
|
|
43
|
+
// After normalization, anything that isn't a recognized primitive
|
|
44
|
+
// is assumed to be a class-like type. The Tier-A predicate registry
|
|
45
|
+
// doesn't introspect class types — `is_integral_v` etc. simply
|
|
46
|
+
// returns `false` for `'class'`, matching ISO behavior.
|
|
47
|
+
return 'class';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -11,7 +11,7 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCapt
|
|
|
11
11
|
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
12
12
|
import { buildTypeEnv } from './type-env.js';
|
|
13
13
|
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
|
|
14
|
-
import { extractTemplateArguments, templateArgumentsIdTag } from './utils/template-arguments.js';
|
|
14
|
+
import { extractTemplateArguments, templateArgumentsIdTag, templateConstraintsIdTag, } from './utils/template-arguments.js';
|
|
15
15
|
import { logger } from '../logger.js';
|
|
16
16
|
import { getTreeSitterBufferSize, getTreeSitterContentByteLength, TREE_SITTER_MAX_BUFFER, } from './constants.js';
|
|
17
17
|
// ============================================================================
|
|
@@ -499,7 +499,35 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
499
499
|
classTemplateArguments.length > 0
|
|
500
500
|
? templateArgumentsIdTag(classTemplateArguments)
|
|
501
501
|
: '';
|
|
502
|
-
|
|
502
|
+
// SFINAE / `requires`-clause aware ID disambiguation (issue #1579).
|
|
503
|
+
// Function-template overloads with identical parameterTypes but
|
|
504
|
+
// mutually-exclusive constraints (e.g. `enable_if_t<is_integral_v<T>>`
|
|
505
|
+
// vs `enable_if_t<is_floating_point_v<T>>`) need distinct graph
|
|
506
|
+
// nodes so the constraint-filter step in `narrowOverloadCandidates`
|
|
507
|
+
// has two candidates to narrow between. Without this tag they
|
|
508
|
+
// collapse to a single Function node and the SFINAE call resolves
|
|
509
|
+
// to only one edge regardless of which overload's constraint holds.
|
|
510
|
+
// The provider hook is the right invocation point — parsing-processor
|
|
511
|
+
// sees raw tree-sitter matches without the `@`-prefixed synthetic
|
|
512
|
+
// captures `scope-extractor` consumes, so we delegate extraction to
|
|
513
|
+
// the language adapter (C++ implements this; other languages opt out).
|
|
514
|
+
let parsedTemplateConstraints = undefined;
|
|
515
|
+
let constraintsTag = '';
|
|
516
|
+
if ((nodeLabel === 'Function' || nodeLabel === 'Method') &&
|
|
517
|
+
provider.extractTemplateConstraints !== undefined &&
|
|
518
|
+
definitionNode !== null) {
|
|
519
|
+
try {
|
|
520
|
+
parsedTemplateConstraints = provider.extractTemplateConstraints(definitionNode);
|
|
521
|
+
if (parsedTemplateConstraints !== undefined) {
|
|
522
|
+
constraintsTag = templateConstraintsIdTag(parsedTemplateConstraints);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
parsedTemplateConstraints = undefined;
|
|
527
|
+
constraintsTag = '';
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`);
|
|
503
531
|
const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
|
|
504
532
|
const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
|
|
505
533
|
(classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
|
|
@@ -528,6 +556,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
528
556
|
...(classTemplateArguments !== undefined && classTemplateArguments.length > 0
|
|
529
557
|
? { templateArguments: classTemplateArguments }
|
|
530
558
|
: {}),
|
|
559
|
+
...(parsedTemplateConstraints !== undefined
|
|
560
|
+
? { templateConstraints: parsedTemplateConstraints }
|
|
561
|
+
: {}),
|
|
531
562
|
...(frameworkHint
|
|
532
563
|
? {
|
|
533
564
|
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
|
|
@@ -374,6 +374,7 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
374
374
|
const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
|
|
375
375
|
const declaredType = match['@declaration.field-type']?.text;
|
|
376
376
|
const returnType = match['@declaration.return-type']?.text;
|
|
377
|
+
const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
|
|
377
378
|
return {
|
|
378
379
|
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
|
|
379
380
|
filePath,
|
|
@@ -385,8 +386,23 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
385
386
|
...(declaredType !== undefined ? { declaredType } : {}),
|
|
386
387
|
...(returnType !== undefined ? { returnType } : {}),
|
|
387
388
|
...(templateArguments !== undefined ? { templateArguments } : {}),
|
|
389
|
+
...(templateConstraints !== undefined ? { templateConstraints } : {}),
|
|
388
390
|
};
|
|
389
391
|
}
|
|
392
|
+
/** Parse an opaque JSON payload synthesized by per-language captures
|
|
393
|
+
* (e.g. C++ `@declaration.template-constraints`). Producer owns the
|
|
394
|
+
* shape; shared code threads it through as `unknown` per the
|
|
395
|
+
* `SymbolDefinition.templateConstraints` contract. */
|
|
396
|
+
function parseJsonCapture(cap) {
|
|
397
|
+
if (cap === undefined)
|
|
398
|
+
return undefined;
|
|
399
|
+
try {
|
|
400
|
+
return JSON.parse(cap.text);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
390
406
|
function parseIntCapture(cap) {
|
|
391
407
|
if (cap === undefined)
|
|
392
408
|
return undefined;
|
|
@@ -747,6 +763,7 @@ const KNOWN_SUB_TAGS = new Set([
|
|
|
747
763
|
'@declaration.parameter-count',
|
|
748
764
|
'@declaration.required-parameter-count',
|
|
749
765
|
'@declaration.parameter-types',
|
|
766
|
+
'@declaration.template-constraints',
|
|
750
767
|
]);
|
|
751
768
|
/**
|
|
752
769
|
* Return the anchor capture for a match — the one whose name begins with
|