Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 49 additions & 24 deletions gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
/**
* C++ conversion-rank scoring for overload resolution (#1578).
* C++ conversion-rank scoring for overload resolution (#1578, #1637).
*
* Operates on **normalized** type strings (output of
* `normalizeCppParamType` in `arity-metadata.ts`). After normalization:
* - int/long/short/unsigned → 'int'
* - float/double → 'double'
* - char → 'char', bool → 'bool'
*
* Because the normalizer collapses promotion pairs (int↔long,
* float↔double) to the same string, those promotions are invisible at
* this layer — they appear as exact matches (rank 0).
* Operates on normalized type strings (output of `normalizeCppParamType`
* in `arity-metadata.ts`) plus optional shape sidecars from #1630.
* Normalization intentionally collapses cv/ref/pointer spelling for stable
* graph IDs, so pointer/nullptr rules must consult `ParameterTypeClass`.
*
* Post-normalization ranking:
* - rank 0 — exact (same normalized type)
* - rank 1 — integral promotion (char→int, bool→int)
* - rank 2 — standard arithmetic conversion (int↔double, char→double,
* bool→double)
* - Infinity — mismatch (string↔int, user types, pointers, etc.)
* - rank 0: exact (same normalized type)
* - rank 1: integral promotion (char -> int, bool -> int)
* - rank 2: standard conversion (arithmetic, nullptr -> T*, T* -> bool,
* T* -> void*)
* - rank 3: nullptr -> bool (kept worse than nullptr -> T*)
* - rank 4: ellipsis conversion (worst viable)
* - Infinity: mismatch (string -> int, user types, unsupported shapes)
*
* This function is intentionally C++-specific (issue #1578 pitfall:
* keep conversion-rank tables out of shared overload-narrowing). Other
* languages may define their own `ConversionRankFn` in the future.
* This function is intentionally C++-specific. Other languages may define
* their own `ConversionRankFn` in the future.
*/

import type { ParameterTypeClass } from 'gitnexus-shared';

/** Set of normalized arithmetic types that support implicit conversion. */
const ARITHMETIC = new Set(['int', 'double', 'char', 'bool']);

/** Integral promotion targets: charint and boolint are rank 1. */
/** Integral promotion targets: char -> int and bool -> int are rank 1. */
const INTEGRAL_PROMOTION = new Map([
['char', 'int'],
['bool', 'int'],
Expand All @@ -35,13 +33,40 @@ const INTEGRAL_PROMOTION = new Map([
/**
* Return the conversion rank from `argType` to `paramType`.
*
* @returns 0 for exact match, 1 for integral promotion (char/bool→int),
* 2 for standard arithmetic conversion, Infinity for mismatch.
* @returns 0 for exact match, 1 for integral promotion, 2 for standard
* conversion, 3 for nullptr -> bool, 4 for ellipsis, Infinity
* for mismatch.
*/
export function cppConversionRank(argType: string, paramType: string): number {
if (argType === paramType) return 0;
// Integral promotions: char→int, bool→int (ISO C++ [conv.prom])
export function cppConversionRank(
argType: string,
paramType: string,
argTypeClass?: ParameterTypeClass,
paramTypeClass?: ParameterTypeClass,
): number {
if (argType === paramType) {
return exactShapeCompatible(argTypeClass, paramTypeClass) ? 0 : Infinity;
}
if (paramType === '...') return 4;
if (INTEGRAL_PROMOTION.get(argType) === paramType) return 1;
if (ARITHMETIC.has(argType) && ARITHMETIC.has(paramType)) return 2;
if (argType === 'null' && isPointer(paramTypeClass)) return 2;
if (argType === 'null' && paramType === 'bool') return 3;
if (isPointer(argTypeClass) && paramType === 'bool') return 2;
if (isPointer(argTypeClass) && isPointer(paramTypeClass) && paramType === 'void') return 2;
return Infinity;
}

function isPointer(typeClass: ParameterTypeClass | undefined): boolean {
return typeClass?.indirection === 'pointer' && typeClass.pointerDepth > 0;
}

function exactShapeCompatible(
argTypeClass: ParameterTypeClass | undefined,
paramTypeClass: ParameterTypeClass | undefined,
): boolean {
if (argTypeClass === undefined || paramTypeClass === undefined) return true;
if (argTypeClass.indirection === 'unknown' || paramTypeClass.indirection === 'unknown') {
return true;
}
return isPointer(argTypeClass) === isPointer(paramTypeClass);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
* generalization plan.
*/

import type { ParsedFile, Reference, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import type {
ParameterTypeClass,
ParsedFile,
Reference,
ScopeId,
SymbolDefinition,
} from 'gitnexus-shared';
import type { KnowledgeGraph } from '../../../graph/types.js';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import type { SemanticModel } from '../../model/semantic-model.js';
Expand Down Expand Up @@ -277,6 +283,7 @@ export function emitFreeCallFallback(
})
: undefined,
site.argumentTypes,
site.argumentTypeClasses,
options.conversionRankFn,
);
}
Expand Down Expand Up @@ -342,6 +349,7 @@ function pickUniqueGlobalCallable(
callArity?: number,
isCallerVisible?: (candidate: SymbolDefinition) => boolean,
callArgTypes?: readonly string[],
callArgTypeClasses?: readonly ParameterTypeClass[],
conversionRankFn?: ConversionRankFn,
): SymbolDefinition | undefined {
const scopeDefs: SymbolDefinition[] = [];
Expand Down Expand Up @@ -380,6 +388,7 @@ function pickUniqueGlobalCallable(
// disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
if (scopeDefs.length > 1) {
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
argumentTypeClasses: callArgTypeClasses,
conversionRankFn,
});
if (narrowed.length === 1) return narrowed[0];
Expand Down Expand Up @@ -420,6 +429,7 @@ function pickUniqueGlobalCallable(
// Same argument-type + conversion-rank narrowing for the model pool.
if (defs.length > 1) {
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
argumentTypeClasses: callArgTypeClasses,
conversionRankFn,
});
if (narrowed.length === 1) return narrowed[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@
* 5. Empty input returns empty output.
*/

import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from 'gitnexus-shared';
import type {
ArityVerdict,
Callsite,
ConstraintContext,
ParameterTypeClass,
SymbolDefinition,
} from 'gitnexus-shared';

/**
* Per-slot conversion-rank function. Returns a numeric cost for
Expand All @@ -51,7 +57,12 @@ import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from
* Each language provides its own implementation. The function operates
* on normalized type strings (output of the language's type normalizer).
*/
export type ConversionRankFn = (argType: string, paramType: string) => number;
export type ConversionRankFn = (
argType: string,
paramType: string,
argTypeClass?: ParameterTypeClass,
paramTypeClass?: ParameterTypeClass,
) => number;

/**
* Optional hook bundle for narrowing extension points. Threaded in
Expand Down Expand Up @@ -130,7 +141,16 @@ export function narrowOverloadCandidates(
if (params === undefined) return false;
for (let i = 0; i < argTypes.length && i < params.length; i++) {
if (argTypes[i] === '') continue;
if (argTypes[i] !== params[i]) return false;
if (
!exactTypeSlotMatches(
argTypes[i],
params[i],
hookCtx?.argumentTypeClasses?.[i],
d.parameterTypeClasses?.[i],
)
) {
return false;
}
}
return true;
});
Expand All @@ -144,7 +164,12 @@ export function narrowOverloadCandidates(
// are returned; multiple survivors are genuinely ambiguous. When
// ranking also yields empty, fall through to the arity-filtered
// `candidates` set — matches pre-#1606 behavior.
const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn);
const ranked = rankByConversion(
candidates,
argTypes,
hookCtx.conversionRankFn,
hookCtx.argumentTypeClasses,
);
if (ranked.length > 0) result = ranked;
}
}
Expand Down Expand Up @@ -183,6 +208,27 @@ export function narrowOverloadCandidates(
return result;
}

function exactTypeSlotMatches(
argType: string,
paramType: string,
argTypeClass?: ParameterTypeClass,
paramTypeClass?: ParameterTypeClass,
): boolean {
if (argType !== paramType) return false;
// C++ normalizes away pointer markers (`int*` -> `int`). When both sides
// provide shape sidecars, do not let that collapse make `int` exactly match
// `int*`. Unknown sidecar evidence preserves the previous string-only path.
if (argTypeClass === undefined || paramTypeClass === undefined) return true;
if (argTypeClass.indirection === 'unknown' || paramTypeClass.indirection === 'unknown') {
return true;
}
return isPointerShape(argTypeClass) === isPointerShape(paramTypeClass);
}

function isPointerShape(typeClass: ParameterTypeClass): boolean {
return typeClass.indirection === 'pointer' && typeClass.pointerDepth > 0;
}

/**
* Pairwise dominance comparison (ISO C++ [over.ics.rank]).
*
Expand All @@ -199,6 +245,7 @@ function rankByConversion(
candidates: readonly SymbolDefinition[],
argTypes: readonly string[],
rankFn: ConversionRankFn,
argTypeClasses?: readonly ParameterTypeClass[],
): readonly SymbolDefinition[] {
// Step 1: compute per-slot ranks and exclude non-viable candidates.
const viable: Array<{ def: SymbolDefinition; ranks: number[] }> = [];
Expand All @@ -207,12 +254,22 @@ function rankByConversion(
if (params === undefined) continue;
const ranks: number[] = [];
let ok = true;
for (let i = 0; i < argTypes.length && i < params.length; i++) {
for (let i = 0; i < argTypes.length; i++) {
const paramType = parameterTypeAt(params, i);
if (paramType === undefined) {
ok = false;
break;
}
if (argTypes[i] === '') {
ranks.push(0); // unknown arg → any-match (rank 0)
continue;
}
const r = rankFn(argTypes[i], params[i]);
const r = rankFn(
argTypes[i],
paramType,
argTypeClasses?.[i],
parameterTypeClassAt(d.parameterTypeClasses, i),
);
if (!isFinite(r)) {
ok = false;
break;
Expand All @@ -239,6 +296,20 @@ function rankByConversion(
return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
}

function parameterTypeAt(params: readonly string[], argIndex: number): string | undefined {
if (argIndex < params.length) return params[argIndex];
return params[params.length - 1] === '...' ? '...' : undefined;
}

function parameterTypeClassAt(
params: readonly ParameterTypeClass[] | undefined,
argIndex: number,
): ParameterTypeClass | undefined {
if (params === undefined) return undefined;
if (argIndex < params.length) return params[argIndex];
return params[params.length - 1]?.base === '...' ? params[params.length - 1] : undefined;
}

/**
* Compare two per-slot rank vectors.
* Returns -1 if `a` dominates `b` (not worse everywhere, better somewhere),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include "lib.h"

void Service::f(int* p) {}
void Service::f(bool flag) {}

void Service::g(int a, int b) {}
void Service::g(int a, ...) {}

void Service::h(int a, double b) {}
void Service::h(int a, ...) {}

void Service::k(int a, ...) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

class Service {
public:
void f(int* p);
void f(bool flag);

void g(int a, int b);
void g(int a, ...);

void h(int a, double b);
void h(int a, ...);

void k(int a, ...);

void runNullptr() {
f(nullptr);
}

void runPointer() {
int* p = nullptr;
f(p);
}

void runBoolConversion() {
f(42);
}

void run() {
int* p = nullptr;
f(nullptr);
f(p);
f(42);
g(1, 2);
h(1, 'a');
k(1, 2, 3);
}
};
Loading
Loading