diff --git a/gitnexus-shared/src/scope-resolution/reference-site.ts b/gitnexus-shared/src/scope-resolution/reference-site.ts index 67ed23e6b..58fabd427 100644 --- a/gitnexus-shared/src/scope-resolution/reference-site.ts +++ b/gitnexus-shared/src/scope-resolution/reference-site.ts @@ -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[]; } diff --git a/gitnexus-shared/src/scope-resolution/registries/context.ts b/gitnexus-shared/src/scope-resolution/registries/context.ts index e802ec081..657856f3e 100644 --- a/gitnexus-shared/src/scope-resolution/registries/context.ts +++ b/gitnexus-shared/src/scope-resolution/registries/context.ts @@ -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`) ──── diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 8b1e120f2..4e4cb4dcf 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -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; } diff --git a/gitnexus/src/core/ingestion/languages/cpp/constraint-filter.ts b/gitnexus/src/core/ingestion/languages/cpp/constraint-filter.ts index a5f760daa..3364f6cdb 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/constraint-filter.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/constraint-filter.ts @@ -20,20 +20,27 @@ * NOT: flip compatible↔incompatible; 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 - * `` 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 `` treats `bool`, `char`, and the signed/unsigned char // variants as integral types (§21.3.4 Table 48), so `is_integral_v` @@ -46,30 +53,135 @@ function isIntegralClass(c: TypeClass | undefined): boolean { } const REGISTRY = new Map([ - ['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` 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` - // 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'; diff --git a/gitnexus/src/core/ingestion/languages/cpp/type-classifier.ts b/gitnexus/src/core/ingestion/languages/cpp/type-classifier.ts index d26435abf..88a120783 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/type-classifier.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/type-classifier.ts @@ -7,11 +7,10 @@ * the call-site inference in `captures.ts`) to one of the categories * the `` 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 `` 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:` + * 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': diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 661f0336b..746b9d694 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -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 = new Set([ '@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', ]); diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 3647fbffa..f3010abf4 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -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, }); diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts index 564fe2200..b9b452eb7 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts @@ -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'; diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index 939f2d88e..430d83c2d 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -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, }); diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-class/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-class/main.cpp new file mode 100644 index 000000000..27e8d7d5e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-class/main.cpp @@ -0,0 +1,16 @@ +#include + +struct S {}; + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run() { + S s; + int n = 0; + pick(s); + pick(n); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-const-volatile/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-const-volatile/main.cpp new file mode 100644 index 000000000..aa0a51228 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-const-volatile/main.cpp @@ -0,0 +1,14 @@ +#include + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run() { + const int c = 0; + volatile int v = 0; + pick(c); + pick(v); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-enum/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-enum/main.cpp new file mode 100644 index 000000000..b942efb2b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-enum/main.cpp @@ -0,0 +1,16 @@ +#include + +enum Color { Red }; + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run() { + Color color = Red; + int n = 0; + pick(color); + pick(n); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-pointer/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-pointer/main.cpp new file mode 100644 index 000000000..8e2afdd31 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-pointer/main.cpp @@ -0,0 +1,14 @@ +#include + +struct S {}; + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run(S* p, S s) { + pick(p); + pick(s); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-reference/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-reference/main.cpp new file mode 100644 index 000000000..534b46288 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-reference/main.cpp @@ -0,0 +1,14 @@ +#include + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run() { + int n = 0; + int& r = n; + pick(r); + pick(n); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-void/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-void/main.cpp new file mode 100644 index 000000000..ef3172260 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-void/main.cpp @@ -0,0 +1,12 @@ +#include + +template , int> = 0> +void pick(T value) {} + +template , int> = 0> +void pick(T value) {} + +void run() { + void* p; + pick(p); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 722916d43..1537c3884 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -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 { + 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; diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index b4eec6f33..cc2697849 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -200,6 +200,12 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly> overload binds only on integral call sites', 'enable_if_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 diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts index 2edc7b74b..032b8b0eb 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts @@ -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'); });