feat(cpp): Expand type_traits constraint registry (#1648)

This commit is contained in:
azizur100389 2026-05-18 21:10:18 +01:00 committed by GitHub
parent 2632bcccc0
commit 5f0c0eba0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 659 additions and 56 deletions

View file

@ -21,6 +21,7 @@
* (defined in `./types.ts`).
*/
import type { ParameterTypeClass } from './symbol-definition.js';
import type { Range, ScopeId } from './types.js';
/**
@ -79,4 +80,11 @@ export interface ReferenceSite {
* (C#: `42` `'int'`, `"alice"` `'string'`).
*/
readonly argumentTypes?: readonly string[];
/**
* Optional per-argument type-shape sidecar for languages that need
* cv/ref/pointer distinctions during constraint filtering. This is
* intentionally separate from `argumentTypes`, which stays normalized
* for existing overload narrowing and conversion-rank logic.
*/
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
}

View file

@ -13,7 +13,7 @@
*/
import type { NodeLabel } from '../../graph/types.js';
import type { SymbolDefinition } from '../symbol-definition.js';
import type { ParameterTypeClass, SymbolDefinition } from '../symbol-definition.js';
import type { Callsite, DefId } from '../types.js';
import type { DefIndex } from '../def-index.js';
import type { QualifiedNameIndex } from '../qualified-name-index.js';
@ -65,6 +65,13 @@ export interface ConstraintContext {
* `narrowOverloadCandidates`' `argTypes` parameter.
*/
readonly argumentTypes?: readonly string[];
/**
* Optional shape-preserving sidecar aligned with `argumentTypes`.
* Unknown or unsupported slots should be omitted by producers or
* marked with `indirection: 'unknown'`; consumers must preserve the
* monotonic fallback and return 'unknown' instead of guessing.
*/
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
}
// ─── Owner-scoped contributor (concrete shape for `RegistryContributor`) ────

View file

@ -1,4 +1,4 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import type { Capture, CaptureMatch, ParameterTypeClass } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeToCapture,
@ -9,7 +9,11 @@ import { getCppParser, getCppScopeQuery } from './query.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
import { splitCppInclude, splitCppUsingDecl } from './import-decomposer.js';
import { computeCppDeclarationArity, computeCppCallArity } from './arity-metadata.js';
import {
classifyCppParameterType,
computeCppDeclarationArity,
computeCppCallArity,
} from './arity-metadata.js';
import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js';
import { markCppDependentBase } from './two-phase-lookup.js';
import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js';
@ -217,6 +221,14 @@ export function emitCppScopeCaptures(
JSON.stringify(argTypes),
);
}
const argTypeClasses = inferCppCallArgTypeClasses(cNode);
if (argTypeClasses !== undefined && argTypeClasses.length > 0) {
grouped['@reference.parameter-type-classes'] = syntheticCapture(
'@reference.parameter-type-classes',
cNode,
JSON.stringify(argTypeClasses),
);
}
}
}
@ -683,6 +695,35 @@ function inferCppCallArgTypes(node: SyntaxNode): string[] | undefined {
return types.length > 0 ? types : undefined;
}
function inferCppCallArgTypeClasses(node: SyntaxNode): ParameterTypeClass[] | undefined {
const argList = node.childForFieldName('arguments');
if (argList === null) return undefined;
const classes: ParameterTypeClass[] = [];
for (let i = 0; i < argList.childCount; i++) {
const child = argList.child(i);
if (child === null) continue;
if (child.type === ',' || child.type === '(' || child.type === ')') continue;
const litType = inferCppLiteralType(child);
if (litType !== '') {
classes.push(valueTypeClass(litType));
} else if (child.type === 'identifier') {
classes.push(lookupDeclaredTypeClassForIdentifier(child));
} else {
classes.push(unknownTypeClass('unknown'));
}
}
return classes.length > 0 ? classes : undefined;
}
function valueTypeClass(base: string): ParameterTypeClass {
return { base, cv: 'none', indirection: 'value', pointerDepth: 0 };
}
function unknownTypeClass(base: string): ParameterTypeClass {
return { base, cv: 'unknown', indirection: 'unknown', pointerDepth: 0 };
}
/**
* Infer the canonical type name of a C++ literal AST node.
* Returns empty string for non-literal / unknown nodes.
@ -750,6 +791,9 @@ function lookupDeclaredTypeForIdentifier(identNode: SyntaxNode): string {
}
if (scope === null) return '';
const paramType = lookupFunctionParameterType(scope, varName);
if (paramType !== '') return paramType;
// Scan declarations in the scope for a matching variable name
for (let i = 0; i < scope.childCount; i++) {
const stmt = scope.child(i);
@ -763,18 +807,118 @@ function lookupDeclaredTypeForIdentifier(identNode: SyntaxNode): string {
// Check init_declarator children for the variable name
const declarator = stmt.childForFieldName('declarator');
if (declarator === null) continue;
if (declarator.type === 'init_declarator') {
const nameChild = declarator.childForFieldName('declarator');
if (nameChild !== null && nameChild.text === varName) {
return normalizeCppTypeText(typeNode.text);
}
} else if (declarator.text === varName) {
const nameChild = declaredNameNode(declarator);
if (nameChild !== null && extractDeclaratorLeafName(nameChild) === varName) {
return normalizeCppTypeText(typeNode.text);
}
}
return '';
}
function lookupDeclaredTypeClassForIdentifier(identNode: SyntaxNode): ParameterTypeClass {
const varName = identNode.text;
let scope: SyntaxNode | null = identNode.parent;
while (
scope !== null &&
scope.type !== 'compound_statement' &&
scope.type !== 'translation_unit'
) {
scope = scope.parent;
}
if (scope === null) return unknownTypeClass('unknown');
const paramTypeClass = lookupFunctionParameterTypeClass(scope, varName, identNode);
if (paramTypeClass !== undefined) return paramTypeClass;
for (let i = 0; i < scope.childCount; i++) {
const stmt = scope.child(i);
if (stmt === null || stmt.type !== 'declaration') continue;
const typeNode = stmt.childForFieldName('type');
if (typeNode === null) continue;
if (typeNode.type === 'placeholder_type_specifier') continue;
const declarator = stmt.childForFieldName('declarator');
if (declarator === null) continue;
const nameChild = declaredNameNode(declarator);
if (nameChild === null || extractDeclaratorLeafName(nameChild) !== varName) continue;
const typeClass = classifyCppParameterType(
typeNode.text,
nameChild.text,
stmt.text.replace(/;\s*$/, ''),
);
if (isKnownEnumName(identNode, typeClass.base)) {
return { ...typeClass, base: `enum:${typeClass.base}` };
}
return typeClass;
}
return unknownTypeClass('unknown');
}
function lookupFunctionParameterType(scope: SyntaxNode, varName: string): string {
const param = findEnclosingFunctionParameter(scope, varName);
if (param === null) return '';
const typeNode = param.childForFieldName('type');
if (typeNode === null) return '';
return normalizeCppTypeText(typeNode.text);
}
function lookupFunctionParameterTypeClass(
scope: SyntaxNode,
varName: string,
identNode: SyntaxNode,
): ParameterTypeClass | undefined {
const param = findEnclosingFunctionParameter(scope, varName);
if (param === null) return undefined;
const typeNode = param.childForFieldName('type');
if (typeNode === null) return undefined;
const declarator = param.childForFieldName('declarator');
if (declarator === null) return undefined;
const typeClass = classifyCppParameterType(typeNode.text, declarator.text, param.text);
if (isKnownEnumName(identNode, typeClass.base)) {
return { ...typeClass, base: `enum:${typeClass.base}` };
}
return typeClass;
}
function findEnclosingFunctionParameter(scope: SyntaxNode, varName: string): SyntaxNode | null {
let node: SyntaxNode | null = scope.parent;
while (node !== null) {
if (node.type === 'function_definition' || node.type === 'function_declarator') {
const fnDecl =
node.type === 'function_declarator'
? node
: findFirstDescendantOfType(node, 'function_declarator');
const params = fnDecl?.childForFieldName('parameters') ?? null;
if (params !== null) {
for (let i = 0; i < params.namedChildCount; i++) {
const param = params.namedChild(i);
if (param === null || param.type !== 'parameter_declaration') continue;
const declarator = param.childForFieldName('declarator');
if (declarator !== null && extractDeclaratorLeafName(declarator) === varName) {
return param;
}
}
}
return null;
}
node = node.parent;
}
return null;
}
function declaredNameNode(declarator: SyntaxNode): SyntaxNode | null {
if (declarator.type !== 'init_declarator') return declarator;
for (let i = 0; i < declarator.namedChildCount; i++) {
const child = declarator.namedChild(i);
if (child === null) continue;
if (child.type === 'identifier') return child;
if (child.type.endsWith('_declarator')) return child;
}
return declarator.childForFieldName('declarator');
}
/** Normalize a type-specifier text for argument type matching.
* Strips qualifiers (const, volatile), namespace prefixes (std::),
* and pointer/reference markers. */
@ -786,6 +930,25 @@ function normalizeCppTypeText(text: string): string {
return t;
}
function isKnownEnumName(node: SyntaxNode, typeName: string): boolean {
if (typeName === '' || typeName === 'unknown') return false;
let root: SyntaxNode = node;
while (root.parent !== null) root = root.parent;
const stack: SyntaxNode[] = [root];
while (stack.length > 0) {
const cur = stack.pop()!;
if (cur.type === 'enum_specifier') {
const name = cur.childForFieldName('name');
if (name?.text === typeName) return true;
}
for (let i = 0; i < cur.childCount; i++) {
const child = cur.child(i);
if (child !== null) stack.push(child);
}
}
return false;
}
/**
* Detect whether a `namespace_definition` AST node is inline.
* Tree-sitter-cpp exposes the `inline` keyword as an anonymous child
@ -1247,7 +1410,9 @@ function extractDeclaratorLeafName(node: SyntaxNode): string | null {
const next =
cur.childForFieldName('declarator') ??
// parenthesized_declarator: single named child
(cur.type === 'parenthesized_declarator' ? cur.namedChild(0) : null);
(cur.type === 'parenthesized_declarator' || cur.type.endsWith('_declarator')
? cur.namedChild(0)
: null);
if (next === null) return null;
cur = next;
}

View file

@ -20,20 +20,27 @@
* NOT: flip compatibleincompatible; pass through unknown.
*/
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from 'gitnexus-shared';
import type {
ArityVerdict,
Callsite,
ConstraintContext,
ParameterTypeClass,
SymbolDefinition,
} from 'gitnexus-shared';
import { classifyType, type TypeClass } from './type-classifier.js';
import type { ConstraintExpr, CppConstraintPayload } from './constraint-extractor.js';
type AtomicEvaluator = (argClasses: readonly TypeClass[]) => ArityVerdict;
interface ConstraintArgClass {
readonly typeClass: TypeClass;
readonly shape?: ParameterTypeClass;
}
type AtomicEvaluator = (args: readonly ConstraintArgClass[]) => ArityVerdict;
/**
* Curated Tier-A predicate registry the four canonical
* `<type_traits>` variable templates whose truth tables are closed-form
* over our coarse `TypeClass` enum.
*
* Deferred predicates that need a cv/ref/pointer sidecar on
* `normalizeCppParamType` (today the normalizer strips those markers
* before storage) live in #1579 as one-line follow-up adds.
* Curated Tier-A predicate registry. Predicates that depend on pointer,
* reference, or cv shape consult `ConstraintContext.argumentTypeClasses`.
* Missing or unsupported shape returns 'unknown' to preserve monotonicity.
*/
// ISO `<type_traits>` treats `bool`, `char`, and the signed/unsigned char
// variants as integral types (§21.3.4 Table 48), so `is_integral_v<bool>`
@ -46,30 +53,135 @@ function isIntegralClass(c: TypeClass | undefined): boolean {
}
const REGISTRY = new Map<string, AtomicEvaluator>([
['is_integral_v', (cls) => verdictFromBool(isIntegralClass(cls[0]), cls)],
['is_floating_point_v', (cls) => verdictFromBool(cls[0] === 'floating', cls)],
[
'is_void_v',
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'void'),
],
[
'is_integral_v',
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && isIntegralClass(arg.typeClass)),
],
[
'is_floating_point_v',
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'floating'),
],
[
'is_arithmetic_v',
(cls) => verdictFromBool(isIntegralClass(cls[0]) || cls[0] === 'floating', cls),
(args) =>
unaryVerdict(
args,
(arg) =>
isPlainValue(arg) && (isIntegralClass(arg.typeClass) || arg.typeClass === 'floating'),
),
],
[
'is_enum_v',
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'enum'),
],
[
'is_class_v',
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'class'),
],
[
'is_pointer_v',
(args) =>
unaryShapeVerdict(args, (shape) => shape.indirection === 'pointer' && shape.pointerDepth > 0),
],
[
'is_reference_v',
(args) =>
unaryShapeVerdict(
args,
(shape) => shape.indirection === 'lvalue-ref' || shape.indirection === 'rvalue-ref',
),
],
[
'is_const_v',
(args) =>
unaryShapeVerdict(args, (shape) => shape.cv === 'const' || shape.cv === 'const volatile', {
requireTopLevelCv: true,
}),
],
[
'is_volatile_v',
(args) =>
unaryShapeVerdict(args, (shape) => shape.cv === 'volatile' || shape.cv === 'const volatile', {
requireTopLevelCv: true,
}),
],
// NOTE: cv-qualifiers are stripped by `normalizeCppParamType` before the
// type token reaches `classifyType`, so `is_same_v<const T, T>` returns
// `'compatible'` instead of the ISO-correct `false`. Tracked under the
// cv-sidecar refactor in #1579's "Out of scope" list; until that lands
// this approximation matches the common `is_same_v<T, ConcreteType>`
// dispatch idiom and silently degrades on cv-distinct compares.
[
'is_same_v',
(cls) => {
if (cls.length < 2 || cls[0] === 'unknown' || cls[1] === 'unknown') return 'unknown';
return cls[0] === cls[1] ? 'compatible' : 'incompatible';
(args) => {
if (args.length < 2 || args[0].typeClass === 'unknown' || args[1].typeClass === 'unknown') {
return 'unknown';
}
return args[0].typeClass === args[1].typeClass ? 'compatible' : 'incompatible';
},
],
]);
function verdictFromBool(predicate: boolean, cls: readonly TypeClass[]): ArityVerdict {
if (cls[0] === 'unknown') return 'unknown';
return predicate ? 'compatible' : 'incompatible';
function unaryVerdict(
args: readonly ConstraintArgClass[],
predicate: (arg: ConstraintArgClass) => boolean,
): ArityVerdict {
const arg = args[0];
if (arg === undefined || arg.typeClass === 'unknown') return 'unknown';
return predicate(arg) ? 'compatible' : 'incompatible';
}
function unaryShapeVerdict(
args: readonly ConstraintArgClass[],
predicate: (shape: ParameterTypeClass) => boolean,
options: { readonly requireTopLevelCv?: boolean } = {},
): ArityVerdict {
const arg = args[0];
if (arg === undefined || arg.typeClass === 'unknown') return 'unknown';
const shape = arg.shape;
if (shape === undefined || shape.indirection === 'unknown' || shape.cv === 'unknown') {
return 'unknown';
}
if (options.requireTopLevelCv === true && shape.indirection === 'pointer') {
return 'unknown';
}
return predicate(shape) ? 'compatible' : 'incompatible';
}
function isPlainValue(arg: ConstraintArgClass): boolean {
const shape = arg.shape;
if (shape === undefined) return true;
return shape.indirection === 'value';
}
function classifyConstraintArg(
token: string | undefined,
shape?: ParameterTypeClass,
): ConstraintArgClass {
if (shape !== undefined && shape.base.startsWith('enum:')) {
return { typeClass: 'enum', shape };
}
const typeClass = token === undefined || token === '' ? 'unknown' : classifyType(token);
return { typeClass, ...(shape !== undefined ? { shape } : {}) };
}
function tokenForArg(ctx: ConstraintContext, argIdx: number): string | undefined {
const shape = ctx.argumentTypeClasses?.[argIdx];
if (shape?.base.startsWith('enum:')) return shape.base;
return ctx.argumentTypes?.[argIdx];
}
function shapeForTemplateParam(
ctx: ConstraintContext,
paramName: string,
argIdx: number,
def?: SymbolDefinition,
): ParameterTypeClass | undefined {
const argShape = ctx.argumentTypeClasses?.[argIdx];
if (argShape === undefined) return undefined;
const paramShape = def?.parameterTypeClasses?.[argIdx];
if (paramShape === undefined) return argShape;
if (paramShape.base === paramName && paramShape.indirection === 'value') return argShape;
return undefined;
}
/** Public surface — registered as `ScopeResolver.constraintCompatibility`. */
@ -80,13 +192,14 @@ export function cppConstraintCompatibility(
): ArityVerdict {
const payload = def.templateConstraints as CppConstraintPayload | undefined;
if (payload === undefined) return 'unknown';
return evaluate(payload.expr, payload, ctx);
return evaluate(payload.expr, payload, ctx, def);
}
function evaluate(
expr: ConstraintExpr,
payload: CppConstraintPayload,
ctx: ConstraintContext,
def?: SymbolDefinition,
): ArityVerdict {
switch (expr.kind) {
case 'unknown':
@ -96,17 +209,18 @@ function evaluate(
if (evaluator === undefined) return 'unknown';
const classes = expr.args.map((paramName) => {
const argIdx = payload.paramArgIndex[paramName];
if (argIdx === undefined) return 'unknown' as TypeClass;
const token = ctx.argumentTypes?.[argIdx];
if (token === undefined || token === '') return 'unknown' as TypeClass;
return classifyType(token);
if (argIdx === undefined) return { typeClass: 'unknown' as TypeClass };
return classifyConstraintArg(
tokenForArg(ctx, argIdx),
shapeForTemplateParam(ctx, paramName, argIdx, def),
);
});
return evaluator(classes);
}
case 'and': {
let result: ArityVerdict = 'compatible';
for (const child of expr.children) {
const v = evaluate(child, payload, ctx);
const v = evaluate(child, payload, ctx, def);
if (v === 'incompatible') return 'incompatible';
if (v === 'unknown') result = 'unknown';
}
@ -115,14 +229,14 @@ function evaluate(
case 'or': {
let result: ArityVerdict = 'incompatible';
for (const child of expr.children) {
const v = evaluate(child, payload, ctx);
const v = evaluate(child, payload, ctx, def);
if (v === 'compatible') return 'compatible';
if (v === 'unknown') result = 'unknown';
}
return result;
}
case 'not': {
const v = evaluate(expr.child, payload, ctx);
const v = evaluate(expr.child, payload, ctx, def);
if (v === 'compatible') return 'incompatible';
if (v === 'incompatible') return 'compatible';
return 'unknown';

View file

@ -7,11 +7,10 @@
* the call-site inference in `captures.ts`) to one of the categories
* the `<type_traits>` predicate registry uses for SFINAE filtering.
*
* Intentionally coarse: cv / pointer / reference qualifiers are stripped
* upstream by `normalizeCppParamType`. Tier-A predicates
* (`is_integral_v`, `is_floating_point_v`, `is_arithmetic_v`, `is_same_v`)
* are insensitive to those modifiers per ISO `<type_traits>` semantics
* ("including any cv-qualified variants").
* `argumentTypes` remain normalized for overload narrowing, while
* constraint predicates that need cv/ref/pointer shape read the parallel
* `argumentTypeClasses` sidecar. Unknown shapes must stay unknown rather
* than being guessed as incompatible.
*/
export type TypeClass =
@ -21,7 +20,11 @@ export type TypeClass =
| 'char'
| 'string'
| 'null'
| 'void'
| 'enum'
| 'class'
| 'pointer'
| 'reference'
| 'unknown';
/**
@ -29,13 +32,17 @@ export type TypeClass =
* inference table in `captures.ts:inferCppLiteralType` plus the std::
* normalization in `arity-metadata.ts:normalizeCppParamType`.
*
* Caller note: token must already be normalized (no `const`, no `&` / `*`,
* no `std::` prefix). Tokens passed via `ConstraintContext.argumentTypes`
* coming from `inferCppCallArgTypes` satisfy this.
* Caller note: token should be normalized for overload matching. Enum
* tokens produced by the C++ adapter use the internal `enum:<Name>`
* prefix so `is_enum_v` does not have to guess that every user token is
* class-like.
*/
export function classifyType(token: string): TypeClass {
if (token.length === 0) return 'unknown';
if (token.startsWith('enum:')) return 'enum';
switch (token) {
case 'void':
return 'void';
case 'int':
return 'integral';
case 'double':

View file

@ -913,6 +913,9 @@ function pass5CollectReferences(
const explicitReceiver = extractExplicitReceiver(match);
const arity = extractArity(match);
const argumentTypes = extractArgumentTypes(match);
const argumentTypeClasses = parseJsonParameterTypeClassesCapture(
match['@reference.parameter-type-classes'],
);
const site: ReferenceSite = {
name: nameCap.text,
@ -923,6 +926,7 @@ function pass5CollectReferences(
...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
...(arity !== undefined ? { arity } : {}),
...(argumentTypes !== undefined ? { argumentTypes } : {}),
...(argumentTypeClasses !== undefined ? { argumentTypeClasses } : {}),
};
referenceSites.push(site);
}
@ -1040,9 +1044,11 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
'@reference.receiver',
'@reference.arity',
'@reference.parameter-types',
'@reference.parameter-type-classes',
'@declaration.parameter-count',
'@declaration.required-parameter-count',
'@declaration.parameter-types',
'@declaration.parameter-type-classes',
'@declaration.template-constraints',
]);

View file

@ -132,6 +132,7 @@ export function emitFreeCallFallback(
site.arity,
site.argumentTypes,
{
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: options.conversionRankFn,
constraintCompatibility: options.constraintCompatibility,
},
@ -196,6 +197,7 @@ export function emitFreeCallFallback(
fnDef = ordinary[0];
} else {
const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: options.conversionRankFn,
constraintCompatibility: options.constraintCompatibility,
});
@ -231,6 +233,7 @@ export function emitFreeCallFallback(
push(adl);
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: options.conversionRankFn,
constraintCompatibility: options.constraintCompatibility,
});
@ -490,6 +493,7 @@ export function pickImplicitThisOverload(
readonly name: string;
readonly arity?: number;
readonly argumentTypes?: readonly string[];
readonly argumentTypeClasses?: readonly import('gitnexus-shared').ParameterTypeClass[];
},
scopes: ScopeResolutionIndexes,
workspaceIndex: WorkspaceResolutionIndex,
@ -526,6 +530,7 @@ export function pickImplicitThisOverload(
// disambiguating signal) leaves the call unresolved rather than
// routing to an arbitrary first overload by registration order.
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: hookCtx?.conversionRankFn,
constraintCompatibility: hookCtx?.constraintCompatibility,
});

View file

@ -62,6 +62,8 @@ export type ConversionRankFn = (argType: string, paramType: string) => number;
* undefined preserves the legacy arity + exact-type behavior.
*/
export interface OverloadNarrowingHookCtx {
/** Shape-preserving per-argument sidecar aligned with `argTypes`. */
readonly argumentTypeClasses?: ConstraintContext['argumentTypeClasses'];
/** Conversion-rank scoring fallback (step 4b). Engages when the
* exact-type filter rejects every candidate. */
readonly conversionRankFn?: ConversionRankFn;
@ -163,7 +165,15 @@ export function narrowOverloadCandidates(
// than emitting a wrong edge.
if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
const callsite: Callsite = { arity: argCount };
const ctx: ConstraintContext = argTypes !== undefined ? { argumentTypes: argTypes } : {};
const ctx: ConstraintContext =
argTypes !== undefined
? {
argumentTypes: argTypes,
...(hookCtx.argumentTypeClasses !== undefined
? { argumentTypeClasses: hookCtx.argumentTypeClasses }
: {}),
}
: {};
result = result.filter((def) => {
if (def.templateConstraints === undefined) return true;
return hookCtx.constraintCompatibility!(callsite, def, ctx) !== 'incompatible';

View file

@ -346,6 +346,7 @@ export function emitReceiverBoundCalls(
site.arity,
site.argumentTypes,
{
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: provider.conversionRankFn,
constraintCompatibility: provider.constraintCompatibility,
},
@ -732,6 +733,7 @@ function pickOverload(
if (overloads.length === 1) return overloads[0];
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
argumentTypeClasses: site.argumentTypeClasses,
conversionRankFn: provider.conversionRankFn,
constraintCompatibility: provider.constraintCompatibility,
});

View file

@ -0,0 +1,16 @@
#include <type_traits>
struct S {};
template <class T, std::enable_if_t<std::is_class_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void pick(T value) {}
void run() {
S s;
int n = 0;
pick(s);
pick(n);
}

View file

@ -0,0 +1,14 @@
#include <type_traits>
template <class T, std::enable_if_t<std::is_const_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_volatile_v<T>, int> = 0>
void pick(T value) {}
void run() {
const int c = 0;
volatile int v = 0;
pick(c);
pick(v);
}

View file

@ -0,0 +1,16 @@
#include <type_traits>
enum Color { Red };
template <class T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void pick(T value) {}
void run() {
Color color = Red;
int n = 0;
pick(color);
pick(n);
}

View file

@ -0,0 +1,14 @@
#include <type_traits>
struct S {};
template <class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_class_v<T>, int> = 0>
void pick(T value) {}
void run(S* p, S s) {
pick(p);
pick(s);
}

View file

@ -0,0 +1,14 @@
#include <type_traits>
template <class T, std::enable_if_t<std::is_reference_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void pick(T value) {}
void run() {
int n = 0;
int& r = n;
pick(r);
pick(n);
}

View file

@ -0,0 +1,12 @@
#include <type_traits>
template <class T, std::enable_if_t<std::is_void_v<T>, int> = 0>
void pick(T value) {}
template <class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
void pick(T value) {}
void run() {
void* p;
pick(p);
}

View file

@ -3156,6 +3156,59 @@ describe('C++ SFINAE filter — C++20 requires-clause shape', () => {
});
});
describe('C++ SFINAE filter — Tier-A type_traits predicates', () => {
async function runFixture(name: string): Promise<PipelineResult> {
return runPipelineFromRepo(path.join(FIXTURES, name), () => {});
}
function callsFromRunToPick(result: PipelineResult) {
return getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'pick',
);
}
it('is_pointer_v and is_class_v disambiguate pointer vs class arguments', async () => {
const result = await runFixture('cpp-sfinae-is-pointer');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(2);
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
}, 60000);
it('is_reference_v keeps reference-shaped arguments distinct from values', async () => {
const result = await runFixture('cpp-sfinae-is-reference');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(2);
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
}, 60000);
it('is_class_v rejects primitive arguments while keeping class arguments', async () => {
const result = await runFixture('cpp-sfinae-is-class');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(2);
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
}, 60000);
it('is_enum_v distinguishes known enum declarations from primitives', async () => {
const result = await runFixture('cpp-sfinae-is-enum');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(2);
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
}, 60000);
it('is_const_v and is_volatile_v disambiguate cv-qualified locals', async () => {
const result = await runFixture('cpp-sfinae-is-const-volatile');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(2);
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
}, 60000);
it('is_void_v does not misclassify void pointers as void values', async () => {
const result = await runFixture('cpp-sfinae-is-void');
const calls = callsFromRunToPick(result);
expect(calls.length).toBe(1);
}, 60000);
});
describe('C++ SFINAE filter — unknown predicate keeps both candidates (monotonicity contract)', () => {
let result: PipelineResult;

View file

@ -200,6 +200,12 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'enable_if_t<is_integral_v<T>> overload binds only on integral call sites',
'enable_if_t<is_floating_point_v<T>> overload binds only on floating call sites',
'requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)',
'is_pointer_v and is_class_v disambiguate pointer vs class arguments',
'is_reference_v keeps reference-shaped arguments distinct from values',
'is_class_v rejects primitive arguments while keeping class arguments',
'is_enum_v distinguishes known enum declarations from primitives',
'is_const_v and is_volatile_v disambiguate cv-qualified locals',
'is_void_v does not misclassify void pointers as void values',
// The legacy DAG path has no inline-namespace same-name ambiguity
// detection. When two inline children declare the same name, the
// legacy path picks an arbitrary match. The scope-resolver returns

View file

@ -19,7 +19,7 @@ import {
evaluateForTest,
getRegistrySize,
} from '../../../../src/core/ingestion/languages/cpp/constraint-filter.js';
import type { ArityVerdict, SymbolDefinition } from 'gitnexus-shared';
import type { ArityVerdict, ParameterTypeClass, SymbolDefinition } from 'gitnexus-shared';
function templateConstraintsFor(src: string): CppConstraintPayload | undefined {
const matches = emitCppScopeCaptures(src, 'test.cpp');
@ -201,11 +201,26 @@ describe('evaluate — Kleene 3-valued truth table', () => {
// ─── Section 3: Predicate registry ─────────────────────────────────────────
describe('Tier-A predicate registry', () => {
it('registry size is exactly 4 (surface-guard against accidental adds)', () => {
expect(getRegistrySize()).toBe(4);
it('registry size is exactly 11 (surface-guard against accidental adds)', () => {
expect(getRegistrySize()).toBe(11);
});
function verdict(name: string, args: string[], argumentTypes: readonly string[]): ArityVerdict {
const shape = (
base: string,
indirection: ParameterTypeClass['indirection'] = 'value',
cv: ParameterTypeClass['cv'] = 'none',
pointerDepth = indirection === 'pointer' ? 1 : 0,
): ParameterTypeClass => ({ base, cv, indirection, pointerDepth });
function verdict(
name: string,
args: string[],
argumentTypes: readonly string[],
opts: {
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
readonly parameterTypeClasses?: readonly ParameterTypeClass[];
} = {},
): ArityVerdict {
const payload: CppConstraintPayload = {
templateParams: args,
paramArgIndex: Object.fromEntries(args.map((a, i) => [a, i])),
@ -216,8 +231,16 @@ describe('Tier-A predicate registry', () => {
filePath: 'x.cpp',
type: 'Function',
templateConstraints: payload,
...(opts.parameterTypeClasses !== undefined
? { parameterTypeClasses: opts.parameterTypeClasses }
: {}),
};
return cppConstraintCompatibility({ arity: argumentTypes.length }, def, { argumentTypes });
return cppConstraintCompatibility({ arity: argumentTypes.length }, def, {
argumentTypes,
...(opts.argumentTypeClasses !== undefined
? { argumentTypeClasses: opts.argumentTypeClasses }
: {}),
});
}
it('is_integral_v matches int, rejects double, unknown for blank', () => {
@ -257,6 +280,117 @@ describe('Tier-A predicate registry', () => {
expect(verdict('is_same_v', ['A', 'B'], ['char', 'int'])).toBe('incompatible');
});
it('is_void_v matches void, rejects int, unknown for blank', () => {
expect(verdict('is_void_v', ['T'], ['void'])).toBe('compatible');
expect(verdict('is_void_v', ['T'], ['int'])).toBe('incompatible');
expect(verdict('is_void_v', ['T'], [''])).toBe('unknown');
});
it('is_enum_v matches known enum tokens, rejects class, unknown for blank', () => {
expect(
verdict('is_enum_v', ['T'], ['Color'], {
argumentTypeClasses: [shape('enum:Color')],
parameterTypeClasses: [shape('T')],
}),
).toBe('compatible');
expect(verdict('is_enum_v', ['T'], ['Widget'])).toBe('incompatible');
expect(verdict('is_enum_v', ['T'], [''])).toBe('unknown');
});
it('is_class_v matches class-like tokens, rejects primitives, unknown for blank', () => {
expect(verdict('is_class_v', ['T'], ['Widget'])).toBe('compatible');
expect(verdict('is_class_v', ['T'], ['int'])).toBe('incompatible');
expect(verdict('is_class_v', ['T'], [''])).toBe('unknown');
});
it('is_pointer_v uses the argument type-class sidecar conservatively', () => {
expect(
verdict('is_pointer_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'pointer')],
parameterTypeClasses: [shape('T')],
}),
).toBe('compatible');
expect(
verdict('is_pointer_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int')],
parameterTypeClasses: [shape('T')],
}),
).toBe('incompatible');
expect(verdict('is_pointer_v', ['T'], ['int'])).toBe('unknown');
expect(
verdict('is_pointer_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'unknown', 'none')],
parameterTypeClasses: [shape('T')],
}),
).toBe('unknown');
});
it('is_reference_v uses the argument type-class sidecar conservatively', () => {
expect(
verdict('is_reference_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'lvalue-ref')],
parameterTypeClasses: [shape('T')],
}),
).toBe('compatible');
expect(
verdict('is_reference_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int')],
parameterTypeClasses: [shape('T')],
}),
).toBe('incompatible');
expect(verdict('is_reference_v', ['T'], ['int'])).toBe('unknown');
expect(
verdict('is_reference_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'unknown', 'none')],
parameterTypeClasses: [shape('T')],
}),
).toBe('unknown');
});
it('is_const_v and is_volatile_v read top-level cv from the sidecar conservatively', () => {
expect(
verdict('is_const_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'value', 'const')],
parameterTypeClasses: [shape('T')],
}),
).toBe('compatible');
expect(
verdict('is_const_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int')],
parameterTypeClasses: [shape('T')],
}),
).toBe('incompatible');
expect(verdict('is_const_v', ['T'], ['int'])).toBe('unknown');
expect(
verdict('is_volatile_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'value', 'volatile')],
parameterTypeClasses: [shape('T')],
}),
).toBe('compatible');
expect(verdict('is_volatile_v', ['T'], ['int'])).toBe('unknown');
expect(
verdict('is_const_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'pointer', 'const')],
parameterTypeClasses: [shape('T')],
}),
).toBe('unknown');
expect(
verdict('is_const_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'value', 'unknown')],
parameterTypeClasses: [shape('T')],
}),
).toBe('unknown');
});
it('shape-sensitive predicates stay unknown when T is not the whole parameter type', () => {
expect(
verdict('is_pointer_v', ['T'], ['int'], {
argumentTypeClasses: [shape('int', 'pointer')],
parameterTypeClasses: [shape('T', 'pointer')],
}),
).toBe('unknown');
});
it('unregistered predicate yields unknown (monotonicity)', () => {
expect(verdict('__not_in_registry__', ['T'], ['int'])).toBe('unknown');
});