fix(cpp): rank homogeneous braced-init overloads (#2214)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled

This commit is contained in:
azizur100389 2026-06-16 18:29:28 +01:00 committed by GitHub
parent b895a20415
commit 72876ab69a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 456 additions and 9 deletions

View file

@ -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 {

View file

@ -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.",

View file

@ -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<ParameterTypeClass, 'templateArguments'> {
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) {

View file

@ -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:

View file

@ -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;
}

View file

@ -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;

View file

@ -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]! };

View file

@ -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

View file

@ -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 {

View file

@ -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`

View file

@ -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<string, readonly SymbolDefinition[]>,
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;

View file

@ -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,

View file

@ -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(

View file

@ -741,6 +741,7 @@ export function runScopeResolution(
isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
resolveAdlCandidates: provider.resolveAdlCandidates,
conversionRankFn: provider.conversionRankFn,
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
constraintCompatibility: provider.constraintCompatibility,
recordResolutionOutcome,
},

View file

@ -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<string> = 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<Dog>()).
* Extracts the template type argument as the constructor type for virtual dispatch. */
const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) => {

View file

@ -0,0 +1,69 @@
#include <initializer_list>
#include <string>
#include <vector>
namespace std {
template <typename T>
class initializer_list {};
template <typename T>
class vector {};
class string {};
}
class InitListService {
public:
void consume(std::initializer_list<int> values) {}
void consume(int value) {}
void consumeVector(std::vector<int> values) {}
void consumeVector(int value) {}
void consumeScalarOrVector(std::vector<int> values) {}
void consumeScalarOrVector(int value) {}
void consumeStringVectorMismatch(std::vector<int> values) {}
void consumeStringVectorMismatch(std::string value) {}
void consumeMixed(std::initializer_list<int> values) {}
void consumeMixed(std::initializer_list<double> values) {}
void consumeEmpty(std::initializer_list<int> values) {}
void consumeEmpty(std::initializer_list<double> values) {}
void consumeSingleMixed(std::initializer_list<int> values) {}
void consumeSingleEmpty(std::initializer_list<int> 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({});
}
};

View file

@ -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<int>',
]);
});
it('resolves homogeneous literal braces to container overloads', () => {
expect(singleTargetParameterTypes('callHomogeneousVector', 'consumeVector')).toEqual([
'std::vector<int>',
]);
});
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;

View file

@ -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<int>')).toBe(0);
expect(cppConversionRank('braced-init:int:3', 'std::vector<int>')).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<int>')).toBe(4);
expect(cppConversionRank('braced-init:string:2', 'std::vector<int>')).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<int>'],
[value('std::initializer_list')],
);
const byDoubleList = mkDef(
'f:double-list',
['std::initializer_list<double>'],
[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<int>'],
[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')]);