fix(cpp): rank braced init overloads by element shape

This commit is contained in:
azizur100389 2026-06-16 16:54:26 +01:00
parent f69c21e170
commit f998253320
17 changed files with 300 additions and 48 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

@ -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,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}`;
}
/**

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

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

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

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

@ -1,4 +1,5 @@
#include <initializer_list>
#include <string>
#include <vector>
namespace std {
@ -7,6 +8,8 @@ class initializer_list {};
template <typename T>
class vector {};
class string {};
}
class InitListService {
@ -17,12 +20,21 @@ public:
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});
}
@ -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({});
}
};

View file

@ -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', () => {

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,
@ -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<int>')).toBe(0);
expect(cppConversionRank('braced-init:int', 'std::vector<int>')).toBe(1);
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',
@ -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<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)', () => {