diff --git a/gitnexus-shared/src/scope-resolution/symbol-definition.ts b/gitnexus-shared/src/scope-resolution/symbol-definition.ts index 3bcdb43eb..64fbce93a 100644 --- a/gitnexus-shared/src/scope-resolution/symbol-definition.ts +++ b/gitnexus-shared/src/scope-resolution/symbol-definition.ts @@ -20,6 +20,8 @@ export interface ParameterTypeClass { indirection: 'value' | 'lvalue-ref' | 'rvalue-ref' | 'pointer' | 'unknown'; /** Number of pointer markers when indirection is `pointer`; otherwise 0. */ pointerDepth: number; + /** Normalized top-level template arguments, when a language preserves them. */ + templateArguments?: string[]; } export interface SymbolDefinition { diff --git a/gitnexus/src/core/ingestion/languages/cpp/arity-metadata.ts b/gitnexus/src/core/ingestion/languages/cpp/arity-metadata.ts index 6bd08710c..b3f32930f 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/arity-metadata.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/arity-metadata.ts @@ -201,6 +201,7 @@ export function classifyCppParameterType( cv, indirection, pointerDepth, + ...templateArgumentsFor(`${source} ${rawType} ${declaratorText ?? ''}`), }; } @@ -213,6 +214,39 @@ function unknownTypeClass(base: string): ParameterTypeClass { }; } +function templateArgumentsFor(rawType: string): Pick { + const args = parseTopLevelTemplateArguments(rawType); + return args === undefined ? {} : { templateArguments: args }; +} + +function parseTopLevelTemplateArguments(rawType: string): string[] | undefined { + const start = rawType.indexOf('<'); + if (start < 0) return undefined; + + const args: string[] = []; + let depth = 0; + let argStart = start + 1; + for (let i = start + 1; i < rawType.length; i++) { + const ch = rawType[i]; + if (ch === '<') { + depth++; + } else if (ch === '>') { + if (depth === 0) { + const finalArg = rawType.slice(argStart, i).trim(); + if (finalArg.length > 0) args.push(normalizeCppParamType(finalArg)); + return args.length > 0 ? args : undefined; + } + depth--; + } else if (ch === ',' && depth === 0) { + const arg = rawType.slice(argStart, i).trim(); + if (arg.length > 0) args.push(normalizeCppParamType(arg)); + argStart = i + 1; + } + } + + return undefined; +} + function findFuncDeclarator(node: SyntaxNode): SyntaxNode | null { let decl = node.childForFieldName('declarator'); if (decl === null) { diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 6618d6c04..db76f22c8 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -21,8 +21,7 @@ import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './a import { markCppInlineNamespaceRange } from './inline-namespaces.js'; import { extractCppTemplateConstraints } from './constraint-extractor.js'; import { captureCppMemberLookupFacts } from './member-lookup.js'; - -const CPP_BRACED_INIT_TYPE_PREFIX = 'braced-init:'; +import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js'; export function emitCppScopeCaptures( sourceText: string, @@ -1065,15 +1064,15 @@ function inferCppBracedInitType(node: SyntaxNode): string { if (child.type === ',' || child.type === '{' || child.type === '}') continue; const elementType = inferCppLiteralType(child); if (elementType === '' || elementType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) { - return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown`; + return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length + 1}`; } elementTypes.push(elementType); } - if (elementTypes.length === 0) return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown`; + if (elementTypes.length === 0) return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:0`; const first = elementTypes[0]; return elementTypes.every((type) => type === first) - ? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}` - : `${CPP_BRACED_INIT_TYPE_PREFIX}unknown`; + ? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}` + : `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`; } /** diff --git a/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts b/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts index 547920783..e400ae836 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts @@ -21,6 +21,7 @@ */ import type { ParameterTypeClass } from 'gitnexus-shared'; +import { normalizeCppParamType } from './arity-metadata.js'; import { hasCppUserDefinedConversion } from './user-defined-conversions.js'; /** Set of normalized arithmetic types that support implicit conversion. */ @@ -32,7 +33,9 @@ const INTEGRAL_PROMOTION = new Map([ ['bool', 'int'], ]); -const BRACED_INIT_TYPE_PREFIX = 'braced-init:'; +export const CPP_BRACED_INIT_TYPE_PREFIX = 'braced-init:'; +export const CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES = [CPP_BRACED_INIT_TYPE_PREFIX] as const; + const BRACED_INIT_CONTAINER_TYPES = new Set([ 'array', 'deque', @@ -48,6 +51,11 @@ const BRACED_INIT_CONTAINER_TYPES = new Set([ 'vector', ]); +interface BracedInitArgType { + elementType: string; + elementCount?: number; +} + /** * Return the conversion rank from `argType` to `paramType`. * @@ -62,10 +70,19 @@ export function cppConversionRank( argTypeClass?: ParameterTypeClass, paramTypeClass?: ParameterTypeClass, ): number { - const bracedInitElementType = parseBracedInitElementType(argType); - if (bracedInitElementType !== undefined) { - if (bracedInitElementType === 'unknown') return Infinity; - return bracedInitConversionRank(paramType); + const bracedInitType = parseBracedInitArgType(argType); + if (bracedInitType !== undefined) { + if (bracedInitType.elementType === 'unknown') return Infinity; + if (bracedInitType.elementCount === 1) { + const scalarRank = cppConversionRank( + bracedInitType.elementType, + paramType, + undefined, + paramTypeClass, + ); + if (isFinite(scalarRank)) return scalarRank; + } + return bracedInitConversionRank(paramType, bracedInitType, paramTypeClass); } if (argType === paramType) { return exactShapeCompatible(argTypeClass, paramTypeClass) ? 0 : Infinity; @@ -81,30 +98,77 @@ export function cppConversionRank( return Infinity; } -function parseBracedInitElementType(argType: string): string | undefined { - const elementType = argType.slice(BRACED_INIT_TYPE_PREFIX.length); - return argType.startsWith(BRACED_INIT_TYPE_PREFIX) && elementType !== '' - ? elementType - : undefined; +function parseBracedInitArgType(argType: string): BracedInitArgType | undefined { + if (!argType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) return undefined; + const payload = argType.slice(CPP_BRACED_INIT_TYPE_PREFIX.length); + if (payload === '') return undefined; + const separator = payload.lastIndexOf(':'); + if (separator > 0) { + const countText = payload.slice(separator + 1); + if (/^\d+$/.test(countText)) { + return { + elementType: payload.slice(0, separator), + elementCount: Number(countText), + }; + } + } + return { elementType: payload }; } -function bracedInitConversionRank(paramType: string): number { +function bracedInitConversionRank( + paramType: string, + argType: BracedInitArgType, + paramTypeClass?: ParameterTypeClass, +): number { const targetBase = bracedInitTargetBase(paramType); - if (targetBase === 'initializer_list' || targetBase === 'std::initializer_list') return 0; - if (BRACED_INIT_CONTAINER_TYPES.has(targetBase)) return 1; + if (targetBase === 'initializer_list' || targetBase === 'std::initializer_list') { + return bracedInitValueTypeMatches(paramType, argType, paramTypeClass) ? 0 : Infinity; + } + if (BRACED_INIT_CONTAINER_TYPES.has(targetBase)) { + return bracedInitValueTypeMatches(paramType, argType, paramTypeClass) ? 4 : Infinity; + } return Infinity; } -function bracedInitTargetBase(paramType: string): string { - let type = paramType.trim(); - type = type.replace(/\b(const|volatile|restrict|mutable|constexpr)\b/g, '').trim(); - type = type.replace(/[&*]+\s*$/, '').trim(); - while (type.includes('<')) { - const stripped = type.replace(/<[^<>]*>/g, ''); - if (stripped === type) break; - type = stripped; +function bracedInitValueTypeMatches( + paramType: string, + argType: BracedInitArgType, + paramTypeClass?: ParameterTypeClass, +): boolean { + const valueType = bracedInitTargetValueType(paramType, paramTypeClass); + if (valueType === undefined) return false; + return isFinite(cppConversionRank(argType.elementType, valueType)); +} + +function bracedInitTargetValueType( + paramType: string, + paramTypeClass?: ParameterTypeClass, +): string | undefined { + return firstTemplateArgument(paramType) ?? paramTypeClass?.templateArguments?.[0]; +} + +function firstTemplateArgument(rawType: string): string | undefined { + const start = rawType.indexOf('<'); + if (start < 0) return undefined; + + let depth = 0; + for (let i = start + 1; i < rawType.length; i++) { + const ch = rawType[i]; + if (ch === '<') { + depth++; + } else if (ch === '>') { + if (depth === 0) return bracedInitTargetBase(rawType.slice(start + 1, i)); + depth--; + } else if (ch === ',' && depth === 0) { + return bracedInitTargetBase(rawType.slice(start + 1, i)); + } } - return type.trim(); + + return undefined; +} + +function bracedInitTargetBase(paramType: string): string { + return normalizeCppParamType(paramType); } function isPointer(typeClass: ParameterTypeClass | undefined): boolean { diff --git a/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts b/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts index c7280bf46..134738dfd 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts @@ -33,7 +33,7 @@ import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js'; -import { cppConversionRank } from './conversion-rank.js'; +import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js'; interface RangeKey { readonly startLine: number; @@ -167,7 +167,12 @@ export function resolveCppQualifiedNamespaceMember( allHits, callsite?.arity, callsite?.argumentTypes, - callsite !== undefined ? { conversionRankFn: cppConversionRank } : undefined, + callsite !== undefined + ? { + conversionRankFn: cppConversionRank, + conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, + } + : undefined, ); if (narrowed.length === 1) return narrowed[0]; if (narrowed.length === 0) return undefined; diff --git a/gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts b/gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts index a681c4952..49b6ca884 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts @@ -13,7 +13,7 @@ import { import { isClassLike } from '../../scope-resolution/scope/walkers.js'; import type { SyntaxNode } from '../../utils/ast-helpers.js'; import { cppConstraintCompatibility } from './constraint-filter.js'; -import { cppConversionRank } from './conversion-rank.js'; +import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js'; interface CapturedBaseEdge { readonly childName: string; @@ -309,6 +309,7 @@ function chooseOverload( const narrowed = narrowOverloadCandidates(candidates, callsite.arity, callsite.argumentTypes, { argumentTypeClasses: callsite.argumentTypeClasses, conversionRankFn: cppConversionRank, + conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, constraintCompatibility: cppConstraintCompatibility, }); if (narrowed.length === 1) return { kind: 'resolved', definition: narrowed[0]! }; diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 3ef89bc07..5ff5a1f8f 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -11,7 +11,7 @@ import { import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js'; import { cppProvider } from '../c-cpp.js'; import { cppArityCompatibility } from './arity.js'; -import { cppConversionRank } from './conversion-rank.js'; +import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js'; import { cppMergeBindings } from './merge-bindings.js'; import { resolveCppImportTarget } from './import-target.js'; import { scanCppHeaderFiles } from './header-scan.js'; @@ -257,6 +257,7 @@ export const cppScopeResolver: ScopeResolver = { // Disambiguates `f(int)` vs `f(double)` called with `f(2.5)` by scoring // each candidate's conversion cost; exact match wins over standard conversion. conversionRankFn: cppConversionRank, + conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, // Range-for element type inference: for (auto& user : users) → bind user to User populateRangeBindings: populateCppRangeBindings, // C++ method return-type bindings need to be visible from module scope diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 1977767d4..64d4869b5 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -654,12 +654,19 @@ function parseJsonParameterTypeClassesCapture( if (typeof o.pointerDepth !== 'number' || !Number.isFinite(o.pointerDepth)) { return undefined; } - out.push({ + const shape: ParameterTypeClass = { base: o.base, cv: o.cv, indirection: o.indirection, pointerDepth: o.pointerDepth, - }); + }; + if (Array.isArray(o.templateArguments)) { + if (!o.templateArguments.every((x): x is string => typeof x === 'string')) { + return undefined; + } + shape.templateArguments = [...o.templateArguments]; + } + out.push(shape); } return out; } catch { diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index fc17ac414..ec2b3ec7d 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -706,6 +706,16 @@ export interface ScopeResolver { */ readonly conversionRankFn?: ConversionRankFn; + /** + * Optional per-language argument-type prefixes for conversion-only + * argument sentinels. When ranking cannot find any viable candidate + * for a multi-overload set containing one of these sentinels, shared + * narrowing suppresses the ambiguous set instead of falling back to + * arity-only candidates. Languages without such sentinels leave this + * undefined. + */ + readonly conversionOnlyArgTypePrefixes?: readonly string[]; + /** * Optional predicate to identify definitions with file-local linkage * (e.g. C `static` functions). When provided, `pickUniqueGlobalCallable` 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 1a8d1d0f4..e680ea4e7 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 @@ -80,6 +80,7 @@ export function emitFreeCallFallback( parsedFiles: readonly ParsedFile[], ) => readonly SymbolDefinition[] | undefined; readonly conversionRankFn?: ConversionRankFn; + readonly conversionOnlyArgTypePrefixes?: readonly string[]; /** Optional per-language constraint hook threaded into * `narrowOverloadCandidates`. Drops candidates whose template * constraints (e.g. C++ `enable_if_t`, C++20 `requires`) provably @@ -164,6 +165,7 @@ export function emitFreeCallFallback( if (fnDef === undefined) { fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, { conversionRankFn: options.conversionRankFn, + conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes, constraintCompatibility: options.constraintCompatibility, }); fnDefFromImplicitThis = fnDef !== undefined; @@ -191,6 +193,7 @@ export function emitFreeCallFallback( { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: options.conversionRankFn, + conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes, constraintCompatibility: options.constraintCompatibility, }, ); @@ -277,6 +280,7 @@ export function emitFreeCallFallback( const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: options.conversionRankFn, + conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes, constraintCompatibility: options.constraintCompatibility, }); if (narrowed.length === 1) { @@ -324,6 +328,7 @@ export function emitFreeCallFallback( const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: options.conversionRankFn, + conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes, constraintCompatibility: options.constraintCompatibility, }); if (narrowed.length === 1) { @@ -380,6 +385,7 @@ export function emitFreeCallFallback( site.argumentTypeClasses, options.conversionRankFn, scopeDefsCache, + options.conversionOnlyArgTypePrefixes, ); } if (fnDef === undefined) continue; @@ -576,6 +582,7 @@ export function pickUniqueGlobalCallable( callArgTypeClasses?: readonly ParameterTypeClass[], conversionRankFn?: ConversionRankFn, scopeDefsCache?: Map, + conversionOnlyArgTypePrefixes?: readonly string[], ): SymbolDefinition | undefined { // The scope-index candidate list is a pure function of (name, callerFilePath): // the same-name bucket is fixed for the pass, the file-local filter depends @@ -637,6 +644,7 @@ export function pickUniqueGlobalCallable( const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, { argumentTypeClasses: callArgTypeClasses, conversionRankFn, + conversionOnlyArgTypePrefixes, }); if (narrowed.length === 1) return narrowed[0]; } @@ -678,6 +686,7 @@ export function pickUniqueGlobalCallable( const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, { argumentTypeClasses: callArgTypeClasses, conversionRankFn, + conversionOnlyArgTypePrefixes, }); if (narrowed.length === 1) return narrowed[0]; } @@ -808,6 +817,7 @@ export function pickImplicitThisOverload( model: SemanticModel, hookCtx?: { readonly conversionRankFn?: ConversionRankFn; + readonly conversionOnlyArgTypePrefixes?: readonly string[]; readonly constraintCompatibility?: ScopeResolver['constraintCompatibility']; }, ): SymbolDefinition | undefined { @@ -840,6 +850,7 @@ export function pickImplicitThisOverload( const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: hookCtx?.conversionRankFn, + conversionOnlyArgTypePrefixes: hookCtx?.conversionOnlyArgTypePrefixes, constraintCompatibility: hookCtx?.constraintCompatibility, }); if (candidates.length !== 1) return undefined; 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 a2766ad90..3645e3549 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts @@ -51,8 +51,6 @@ import type { SymbolDefinition, } from 'gitnexus-shared'; -const CONVERSION_ONLY_ARG_TYPE_PREFIXES = ['braced-init:']; - /** * Per-slot conversion-rank function. Returns a numeric cost for * converting `argType` to `paramType`: @@ -85,6 +83,10 @@ export interface OverloadNarrowingHookCtx { /** Conversion-rank scoring fallback (step 4b). Engages when the * exact-type filter rejects every candidate. */ readonly conversionRankFn?: ConversionRankFn; + /** Per-language argument-type prefixes whose conversion-rank failures + * should suppress genuinely ambiguous multi-overload sets instead of + * falling back to arity-only candidates. */ + readonly conversionOnlyArgTypePrefixes?: readonly string[]; /** Constraint filter (step 4c). Drops candidates whose template * guards (SFINAE `enable_if_t`, C++20 `requires`, future Rust * trait bounds, etc.) provably fail at the call site. Three-valued @@ -178,7 +180,12 @@ export function narrowOverloadCandidates( hookCtx.argumentTypeClasses, ); if (ranked.length > 0) result = ranked; - else if (hasConversionOnlyArgType(argTypes)) result = []; + else if ( + candidates.length > 1 && + hasConversionOnlyArgType(argTypes, hookCtx.conversionOnlyArgTypePrefixes) + ) { + result = []; + } } } @@ -225,10 +232,12 @@ export function narrowOverloadCandidates( return result; } -function hasConversionOnlyArgType(argTypes: readonly string[]): boolean { - return argTypes.some((type) => - CONVERSION_ONLY_ARG_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix)), - ); +function hasConversionOnlyArgType( + argTypes: readonly string[], + prefixes: readonly string[] | undefined, +): boolean { + if (prefixes === undefined || prefixes.length === 0) return false; + return argTypes.some((type) => prefixes.some((prefix) => type.startsWith(prefix))); } function exactTypeSlotMatches( 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 f1e014772..88139e3c1 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 @@ -85,6 +85,7 @@ type ReceiverBoundProviderSubset = Pick< | 'resolveReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' + | 'conversionOnlyArgTypePrefixes' | 'constraintCompatibility' | 'isStaticOnly' >; @@ -519,6 +520,7 @@ export function emitReceiverBoundCalls( { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, constraintCompatibility: provider.constraintCompatibility, }, ); @@ -1275,6 +1277,7 @@ function pickOverload( const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, constraintCompatibility: provider.constraintCompatibility, }); // When narrowing leaves >1 candidate that share identical normalized @@ -1382,6 +1385,7 @@ function pickFirstNonStaticOnly( const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, constraintCompatibility: provider.constraintCompatibility, }); // Same ambiguity handling as `pickOverload`: when normalization @@ -1427,6 +1431,7 @@ function recordReceiverOverloadSuppression( const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, { argumentTypeClasses: site.argumentTypeClasses, conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, constraintCompatibility: provider.constraintCompatibility, }); const reason: ResolutionSuppressionReason = isOverloadAmbiguousAfterNormalization( diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index 7b573e0a6..8c9222325 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -741,6 +741,7 @@ export function runScopeResolution( isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller, resolveAdlCandidates: provider.resolveAdlCandidates, conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, constraintCompatibility: provider.constraintCompatibility, recordResolutionOutcome, }, diff --git a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts index 0284d4ba0..8e80d0109 100644 --- a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts @@ -19,6 +19,7 @@ import { methodToTypeArgPosition, type TypeArgPosition, } from './shared.js'; +import { CPP_BRACED_INIT_TYPE_PREFIX } from '../languages/cpp/conversion-rank.js'; const DECLARATION_NODE_TYPES: ReadonlySet = new Set(['declaration']); @@ -479,6 +480,8 @@ const extractForLoopBinding: ForLoopExtractor = ( /** Infer the type of a literal AST node for C++ overload disambiguation. */ const inferLiteralType: LiteralTypeInferrer = (node) => { switch (node.type) { + case 'initializer_list': + return inferBracedInitLiteralType(node); case 'number_literal': { const t = node.text; // Float suffixes @@ -505,6 +508,23 @@ const inferLiteralType: LiteralTypeInferrer = (node) => { } }; +function inferBracedInitLiteralType(node: SyntaxNode): string | undefined { + const elementTypes: string[] = []; + for (const child of node.children) { + if (child.type === ',' || child.type === '{' || child.type === '}') continue; + const elementType = inferLiteralType(child); + if (elementType === undefined || elementType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) { + return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length + 1}`; + } + elementTypes.push(elementType); + } + if (elementTypes.length === 0) return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:0`; + const first = elementTypes[0]; + return elementTypes.every((type) => type === first) + ? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}` + : `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`; +} + /** C++: detect constructor type from smart pointer factory calls (make_shared()). * Extracts the template type argument as the constructor type for virtual dispatch. */ const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) => { diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp index 913be99b9..a193ce2e1 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp +++ b/gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp @@ -1,4 +1,5 @@ #include +#include #include namespace std { @@ -7,6 +8,8 @@ class initializer_list {}; template class vector {}; + +class string {}; } class InitListService { @@ -17,12 +20,21 @@ public: void consumeVector(std::vector values) {} void consumeVector(int value) {} + void consumeScalarOrVector(std::vector values) {} + void consumeScalarOrVector(int value) {} + + void consumeStringVectorMismatch(std::vector values) {} + void consumeStringVectorMismatch(std::string value) {} + void consumeMixed(std::initializer_list values) {} void consumeMixed(std::initializer_list values) {} void consumeEmpty(std::initializer_list values) {} void consumeEmpty(std::initializer_list values) {} + void consumeSingleMixed(std::initializer_list values) {} + void consumeSingleEmpty(std::initializer_list values) {} + void callHomogeneousInitList() { consume({1, 2, 3}); } @@ -31,6 +43,14 @@ public: consumeVector({1, 2, 3}); } + void callSingleElementScalar() { + consumeScalarOrVector({5}); + } + + void callStringVectorMismatch() { + consumeStringVectorMismatch({"a", "b"}); + } + void callHeterogeneousInitList() { consumeMixed({1, 2.0}); } @@ -38,4 +58,12 @@ public: void callEmptyInitList() { consumeEmpty({}); } + + void callSingleHeterogeneousInitList() { + consumeSingleMixed({1, 2.0}); + } + + void callSingleEmptyInitList() { + consumeSingleEmpty({}); + } }; diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 80f2c9b2e..a091e1cb9 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1267,6 +1267,16 @@ describe('C++ braced-init-list overload disambiguation (#1899 A8 conservative)', ]); }); + it('prefers a scalar overload for single-element braced-init lists', () => { + expect(singleTargetParameterTypes('callSingleElementScalar', 'consumeScalarOrVector')).toEqual([ + 'int', + ]); + }); + + it('rejects container overloads whose value type cannot accept the braced elements', () => { + expect(callsFrom('callStringVectorMismatch', 'consumeStringVectorMismatch')).toHaveLength(0); + }); + it('suppresses heterogeneous braced-init lists instead of guessing an element type', () => { expect(callsFrom('callHeterogeneousInitList', 'consumeMixed')).toHaveLength(0); }); @@ -1274,6 +1284,14 @@ describe('C++ braced-init-list overload disambiguation (#1899 A8 conservative)', it('suppresses empty braced-init lists instead of guessing an element type', () => { expect(callsFrom('callEmptyInitList', 'consumeEmpty')).toHaveLength(0); }); + + it('preserves single-overload heterogeneous braced-init recall', () => { + expect(callsFrom('callSingleHeterogeneousInitList', 'consumeSingleMixed')).toHaveLength(1); + }); + + it('preserves single-overload empty braced-init recall', () => { + expect(callsFrom('callSingleEmptyInitList', 'consumeSingleEmpty')).toHaveLength(1); + }); }); describe('C++ same-arity overload cross-file and chain resolution', () => { diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-overload-ranking.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-overload-ranking.test.ts index 9efd2a2a3..661bf956e 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-overload-ranking.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-overload-ranking.test.ts @@ -1,6 +1,9 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { ParameterTypeClass, SymbolDefinition } from 'gitnexus-shared'; -import { cppConversionRank } from '../../../../src/core/ingestion/languages/cpp/conversion-rank.js'; +import { + CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, + cppConversionRank, +} from '../../../../src/core/ingestion/languages/cpp/conversion-rank.js'; import { clearCppUserDefinedConversions, registerCppUserDefinedConversion, @@ -104,11 +107,23 @@ describe('cppConversionRank user-defined conversion ranks (#1631)', () => { describe('cppConversionRank braced-init-list ranks (#1899)', () => { it('ranks homogeneous braced-init lists toward initializer_list and containers', () => { - expect(cppConversionRank('braced-init:int', 'std::initializer_list')).toBe(0); - expect(cppConversionRank('braced-init:int', 'std::vector')).toBe(1); + expect(cppConversionRank('braced-init:int:3', 'std::initializer_list')).toBe(0); + expect(cppConversionRank('braced-init:int:3', 'std::vector')).toBe(4); expect(cppConversionRank('braced-init:int', 'int')).toBe(Infinity); }); + it('uses element count and type before ranking container targets', () => { + expect(cppConversionRank('braced-init:int:1', 'int')).toBe(0); + expect(cppConversionRank('braced-init:int:1', 'std::vector')).toBe(4); + expect(cppConversionRank('braced-init:string:2', 'std::vector')).toBe(Infinity); + expect( + cppConversionRank('braced-init:int:3', 'std::vector', value('braced-init:int:3'), { + ...value('std::vector'), + templateArguments: ['int'], + }), + ).toBe(4); + }); + it('suppresses unknown braced-init lists when conversion ranking finds no viable target', () => { const byIntList = mkDef( 'f:int-list', @@ -121,13 +136,35 @@ describe('cppConversionRank braced-init-list ranks (#1899)', () => { [value('std::initializer_list')], ); - const result = narrowOverloadCandidates([byIntList, byDoubleList], 1, ['braced-init:unknown'], { - argumentTypeClasses: [value('braced-init:unknown')], - conversionRankFn: cppConversionRank, - }); + const result = narrowOverloadCandidates( + [byIntList, byDoubleList], + 1, + ['braced-init:unknown:2'], + { + argumentTypeClasses: [value('braced-init:unknown:2')], + conversionRankFn: cppConversionRank, + conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, + }, + ); expect(result).toEqual([]); }); + + it('preserves single-candidate recall for unrankable braced-init lists', () => { + const byIntList = mkDef( + 'f:int-list', + ['std::initializer_list'], + [value('std::initializer_list')], + ); + + const result = narrowOverloadCandidates([byIntList], 1, ['braced-init:unknown:2'], { + argumentTypeClasses: [value('braced-init:unknown:2')], + conversionRankFn: cppConversionRank, + conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, + }); + + expect(result.map((d) => d.nodeId)).toEqual(['f:int-list']); + }); }); describe('narrowOverloadCandidates with C++ pointer-rank sidecars (#1637)', () => {