From 72876ab69a7aaa1676ed062f5e2fc0ac642017a9 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Tue, 16 Jun 2026 18:29:28 +0100 Subject: [PATCH] fix(cpp): rank homogeneous braced-init overloads (#2214) --- .../src/scope-resolution/symbol-definition.ts | 2 + gitnexus/bench/scope-capture/baselines.json | 5 +- .../ingestion/languages/cpp/arity-metadata.ts | 34 ++++++ .../core/ingestion/languages/cpp/captures.ts | 22 ++++ .../languages/cpp/conversion-rank.ts | 111 ++++++++++++++++++ .../languages/cpp/inline-namespaces.ts | 9 +- .../ingestion/languages/cpp/member-lookup.ts | 3 +- .../ingestion/languages/cpp/scope-resolver.ts | 3 +- .../src/core/ingestion/scope-extractor.ts | 11 +- .../contract/scope-resolver.ts | 10 ++ .../passes/free-call-fallback.ts | 11 ++ .../passes/overload-narrowing.ts | 18 +++ .../passes/receiver-bound-calls.ts | 5 + .../scope-resolution/pipeline/run.ts | 1 + .../core/ingestion/type-extractors/c-cpp.ts | 20 ++++ .../cpp-braced-init-list-overload/service.cpp | 69 +++++++++++ .../test/integration/resolvers/cpp.test.ts | 64 ++++++++++ .../cpp/cpp-overload-ranking.test.ts | 67 ++++++++++- 18 files changed, 456 insertions(+), 9 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp 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/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index f6f68252a..110f91075 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -18,11 +18,12 @@ "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)." }, "cpp": { - "fingerprint": "9b5b4393d158d76dcf1ef9807e0326462c45a5266310f0ae7894d017f3858219", + "fingerprint": "5ef259c2cf9c4804bc83c41d25a3141d1a2cc57ce46485cce09013cf6f5e725e", "scaling_budget": 1.5, + "_note_1899_followup": "#1899 follow-up: braced-init metadata now carries element count, intentionally changing C++ capture output; CI benchmark scaling remains linear (1.129 < 1.5).", "_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.", "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).", - "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5)." + "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5)." }, "csharp": { "_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.", 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 ef39076f1..db76f22c8 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -21,6 +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'; +import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js'; export function emitCppScopeCaptures( sourceText: string, @@ -1022,6 +1023,8 @@ function unknownTypeClass(base: string): ParameterTypeClass { */ function inferCppLiteralType(node: SyntaxNode): string { switch (node.type) { + case 'initializer_list': + return inferCppBracedInitType(node); case 'number_literal': { const text = node.text; // Floating-point literals contain '.', 'e', 'E', or end with 'f'/'F' @@ -1053,6 +1056,25 @@ function inferCppLiteralType(node: SyntaxNode): string { } } +function inferCppBracedInitType(node: SyntaxNode): string { + const elementTypes: string[] = []; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child === null) continue; + 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:${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}`; +} + /** * Look up the declared type of a variable by scanning sibling declarations * in the enclosing compound_statement (function body). Handles: diff --git a/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts b/gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts index 330deea09..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,6 +33,29 @@ const INTEGRAL_PROMOTION = new Map([ ['bool', 'int'], ]); +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', + 'list', + 'set', + 'std::array', + 'std::deque', + 'std::list', + 'std::set', + 'std::unordered_set', + 'std::vector', + 'unordered_set', + 'vector', +]); + +interface BracedInitArgType { + elementType: string; + elementCount?: number; +} + /** * Return the conversion rank from `argType` to `paramType`. * @@ -46,6 +70,20 @@ export function cppConversionRank( argTypeClass?: ParameterTypeClass, paramTypeClass?: ParameterTypeClass, ): number { + 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; } @@ -60,6 +98,79 @@ export function cppConversionRank( return Infinity; } +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, + argType: BracedInitArgType, + paramTypeClass?: ParameterTypeClass, +): number { + const targetBase = bracedInitTargetBase(paramType); + 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 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 undefined; +} + +function bracedInitTargetBase(paramType: string): string { + return normalizeCppParamType(paramType); +} + function isPointer(typeClass: ParameterTypeClass | undefined): boolean { return typeClass?.indirection === 'pointer' && typeClass.pointerDepth > 0; } 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 b69252ce2..3645e3549 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts @@ -83,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 @@ -176,6 +180,12 @@ export function narrowOverloadCandidates( hookCtx.argumentTypeClasses, ); if (ranked.length > 0) result = ranked; + else if ( + candidates.length > 1 && + hasConversionOnlyArgType(argTypes, hookCtx.conversionOnlyArgTypePrefixes) + ) { + result = []; + } } } @@ -222,6 +232,14 @@ export function narrowOverloadCandidates( return result; } +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( argType: string, paramType: string, 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 new file mode 100644 index 000000000..a193ce2e1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp @@ -0,0 +1,69 @@ +#include +#include +#include + +namespace std { +template +class initializer_list {}; + +template +class vector {}; + +class string {}; +} + +class InitListService { +public: + void consume(std::initializer_list values) {} + void consume(int value) {} + + 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}); + } + + void callHomogeneousVector() { + consumeVector({1, 2, 3}); + } + + void callSingleElementScalar() { + consumeScalarOrVector({5}); + } + + void callStringVectorMismatch() { + consumeStringVectorMismatch({"a", "b"}); + } + + void callHeterogeneousInitList() { + consumeMixed({1, 2.0}); + } + + 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 925602a5c..a091e1cb9 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1230,6 +1230,70 @@ describe('C++ overload disambiguation by parameter types', () => { // ── Phase P: Same-arity overloads — cross-file + chain resolution ───────── +describe('C++ braced-init-list overload disambiguation (#1899 A8 conservative)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-braced-init-list-overload'), + () => {}, + ); + }, 60000); + + const callsFrom = (source: string, target: string) => + getRelationships(result, 'CALLS').filter( + (edge) => edge.source === source && edge.target === target, + ); + + const singleTargetParameterTypes = (source: string, target: string) => { + const calls = callsFrom(source, target); + expect(calls).toHaveLength(1); + const [call] = calls; + expect(call).toBeDefined(); + return call === undefined + ? undefined + : result.graph.getNode(call.rel.targetId)?.properties.parameterTypes; + }; + + it('resolves homogeneous literal braces to initializer_list overloads', () => { + expect(singleTargetParameterTypes('callHomogeneousInitList', 'consume')).toEqual([ + 'std::initializer_list', + ]); + }); + + it('resolves homogeneous literal braces to container overloads', () => { + expect(singleTargetParameterTypes('callHomogeneousVector', 'consumeVector')).toEqual([ + 'std::vector', + ]); + }); + + 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); + }); + + 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', () => { let result: PipelineResult; 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 7203e0633..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, @@ -102,6 +105,68 @@ 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: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', + ['std::initializer_list'], + [value('std::initializer_list')], + ); + const byDoubleList = mkDef( + 'f:double-list', + ['std::initializer_list'], + [value('std::initializer_list')], + ); + + 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)', () => { it('selects pointer overload for nullptr over bool overload', () => { const byPointer = mkDef('f:intptr', ['int'], [pointer('int')]);