From e234dac84918175346404dffb28927f828490a08 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Fri, 29 May 2026 21:38:19 +0100 Subject: [PATCH] feat(cpp): add template partial ordering (#1885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cpp): add template partial ordering * fix(cpp): harden template partial ordering --------- Co-authored-by: Gergő Magyar --- .../group/extractors/http-patterns/python.ts | 8 +- .../method-extractors/configs/c-cpp.ts | 16 +++ gitnexus/src/core/ingestion/method-types.ts | 3 +- .../src/core/ingestion/parsing-processor.ts | 10 +- .../scope-resolution/graph-bridge/ids.ts | 18 ++- .../graph-bridge/node-lookup.ts | 35 ++++-- .../passes/overload-narrowing.ts | 117 ++++++++++++++++++ .../src/core/ingestion/utils/method-props.ts | 50 +++++++- .../core/ingestion/workers/parse-worker.ts | 10 +- .../main.cpp | 13 ++ .../cpp-template-partial-order-tied/main.cpp | 13 ++ .../test/integration/resolvers/cpp.test.ts | 28 +++++ .../test/integration/resolvers/helpers.ts | 4 + .../cpp/cpp-overload-ranking.test.ts | 83 +++++++++++++ 14 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp diff --git a/gitnexus/src/core/group/extractors/http-patterns/python.ts b/gitnexus/src/core/group/extractors/http-patterns/python.ts index 5dc314d35..d467cdb34 100644 --- a/gitnexus/src/core/group/extractors/http-patterns/python.ts +++ b/gitnexus/src/core/group/extractors/http-patterns/python.ts @@ -622,8 +622,9 @@ interface PythonRepoContext { /** Strip `.py` and return the bare basename (e.g. `api/users.py` → `users`). */ function fileShortKey(rel: string): string { - const slash = rel.lastIndexOf('/'); - const file = slash >= 0 ? rel.slice(slash + 1) : rel; + const normalized = rel.replace(/\\/g, '/'); + const slash = normalized.lastIndexOf('/'); + const file = slash >= 0 ? normalized.slice(slash + 1) : normalized; return file.endsWith('.py') ? file.slice(0, -3) : file; } @@ -633,7 +634,8 @@ function fileShortKey(rel: string): string { * case callers should fall back to the short key. */ function fileLongKey(rel: string): string { - const noExt = rel.endsWith('.py') ? rel.slice(0, -3) : rel; + const normalized = rel.replace(/\\/g, '/'); + const noExt = normalized.endsWith('.py') ? normalized.slice(0, -3) : normalized; const lastSlash = noExt.lastIndexOf('/'); if (lastSlash < 0) return ''; const beforeLast = noExt.slice(0, lastSlash); diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/c-cpp.ts b/gitnexus/src/core/ingestion/method-extractors/configs/c-cpp.ts index e369b3de9..dccc3f52f 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/c-cpp.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/c-cpp.ts @@ -8,6 +8,7 @@ import type { MethodVisibility, } from '../../method-types.js'; import { hasKeyword } from '../../field-extractors/configs/helpers.js'; +import { classifyCppParameterType } from '../../languages/cpp/arity-metadata.js'; import { extractSimpleTypeName } from '../../type-extractors/shared.js'; import type { SyntaxNode } from '../../utils/ast-helpers.js'; @@ -149,6 +150,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] { ? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null) : null, rawType: typeNode?.text?.trim() ?? null, + typeClass: classifyCppParameterType( + typeNode?.text?.trim() ?? 'unknown', + declNode?.text, + param.text, + ), isOptional: false, isVariadic: false, }); @@ -164,6 +170,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] { ? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null) : null, rawType: typeNode?.text?.trim() ?? null, + typeClass: classifyCppParameterType( + typeNode?.text?.trim() ?? 'unknown', + declNode?.text, + param.text, + ), isOptional: true, isVariadic: false, }); @@ -180,6 +191,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] { ? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null) : null, rawType: typeNode?.text?.trim() ?? null, + typeClass: classifyCppParameterType( + typeNode?.text?.trim() ?? 'unknown', + declNode?.text, + param.text, + ), isOptional: false, isVariadic: true, }); diff --git a/gitnexus/src/core/ingestion/method-types.ts b/gitnexus/src/core/ingestion/method-types.ts index 105b4f251..301941535 100644 --- a/gitnexus/src/core/ingestion/method-types.ts +++ b/gitnexus/src/core/ingestion/method-types.ts @@ -1,6 +1,6 @@ // gitnexus/src/core/ingestion/method-types.ts -import type { SupportedLanguages } from 'gitnexus-shared'; +import type { ParameterTypeClass, SupportedLanguages } from 'gitnexus-shared'; import type { FieldVisibility } from './field-types.js'; import type { SyntaxNode } from './utils/ast-helpers.js'; @@ -14,6 +14,7 @@ export interface ParameterInfo { * Used by typeTagForId for overload disambiguation where generic args matter. * Falls back to `type` when not set. */ rawType?: string | null; + typeClass?: ParameterTypeClass; isOptional: boolean; isVariadic: boolean; } diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 6b69f75c3..4e0d5a2e5 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -30,6 +30,7 @@ import { typeTagForId, constTagForId, buildCollisionGroups, + parameterShapeIdTag, } from './utils/method-props.js'; import { extractTemplateArguments, @@ -665,6 +666,13 @@ const processParsingSequential = async ( cached.groups, ); } + const parameterShapeTag = + nodeLabel === 'Function' || nodeLabel === 'Method' + ? parameterShapeIdTag( + methodProps.parameterTypes as string[] | undefined, + methodProps.parameterTypeClasses as ParameterTypeClass[] | undefined, + ) + : ''; const classTemplateArguments = extractedClassSymbol?.templateArguments ?? provider.classExtractor?.extractTemplateArgumentsFromCapture?.({ @@ -717,7 +725,7 @@ const processParsingSequential = async ( } const nodeId = generateId( nodeLabel, - `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`, + `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}${parameterShapeTag}`, ); const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode; const qualifiedTypeName = diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts index 8a1bf5a0a..94eec67c0 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts @@ -17,11 +17,12 @@ * migrate. */ -import type { NodeLabel, ScopeId, SymbolDefinition } from 'gitnexus-shared'; +import type { NodeLabel, ParameterTypeClass, ScopeId, SymbolDefinition } from 'gitnexus-shared'; import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; import { generateId } from '../../../../lib/utils.js'; import { qualifiedKey, simpleKey, type GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import { templateConstraintsIdTag } from '../../utils/template-arguments.js'; +import { parameterShapeIdTag } from '../../utils/method-props.js'; /** * Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the * source ("caller"). A Variable / Property can be the TARGET of an @@ -76,6 +77,7 @@ export function resolveDefGraphId( qualifiedName?: string; type?: NodeLabel; parameterTypes?: readonly string[]; + parameterTypeClasses?: readonly ParameterTypeClass[]; templateArguments?: readonly string[]; templateConstraints?: unknown; }, @@ -102,11 +104,23 @@ export function resolveDefGraphId( const cHit = nodeLookup.get(cKey); if (cHit !== undefined) return cHit; } + if ( + (def.type === 'Function' || def.type === 'Method') && + def.parameterTypes !== undefined && + def.parameterTypeClasses !== undefined + ) { + const shapeTag = parameterShapeIdTag(def.parameterTypes, def.parameterTypeClasses); + if (shapeTag !== '') { + const shapeKey = qualifiedKey(filePath, def.type, `${qn}${shapeTag}`); + const shapeHit = nodeLookup.get(shapeKey); + if (shapeHit !== undefined) return shapeHit; + } + } // Overload disambiguation: when the def carries parameter types, // try the parameter-typed key first so same-name same-arity // overloads route to their distinct graph nodes. if ( - def.type === 'Method' && + (def.type === 'Function' || def.type === 'Method') && def.parameterTypes !== undefined && def.parameterTypes.length > 0 ) { diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index 8c29f8f2c..229c83f45 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -18,9 +18,10 @@ * format that downstream consumers (queries, edges, MCP) expect. */ -import type { NodeLabel } from 'gitnexus-shared'; +import type { NodeLabel, ParameterTypeClass } from 'gitnexus-shared'; import type { KnowledgeGraph } from '../../../graph/types.js'; import { templateConstraintsIdTag } from '../../utils/template-arguments.js'; +import { parameterShapeIdTag } from '../../utils/method-props.js'; export type GraphNodeLookup = ReadonlyMap; @@ -42,6 +43,10 @@ function parseQualifiedFromId(id: string, label: NodeLabel, filePath: string): s return hash === -1 ? suffix : suffix.slice(0, hash); } +function stripCallableDisambiguatorTags(qualifiedName: string): string { + return qualifiedName.replace(/~shape:.*$/, '').replace(/~c:[a-z0-9]+$/, ''); +} + /** * Build a qualified-key string in a separate keyspace from simple-key * strings. Prefix `` can't appear in a valid filePath on any OS, so @@ -84,7 +89,8 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { const qualified = props.qualifiedName ?? parseQualifiedFromId(node.id, node.label, props.filePath); if (qualified !== undefined && qualified.length > 0) { - const qKey = qualifiedKey(props.filePath, node.label, qualified); + const keyQualified = stripCallableDisambiguatorTags(qualified); + const qKey = qualifiedKey(props.filePath, node.label, keyQualified); if (!lookup.has(qKey)) lookup.set(qKey, node.id); // Overload-disambiguating key: include parameter types so two // same-arity overloads (e.g. `Lookup(int)` vs `Lookup(string)`) @@ -93,10 +99,25 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { // a parameter-types-suffixed key so resolveDefGraphId can find // the right overload by matching its def's parameterTypes. const pTypes = (props as { parameterTypes?: readonly string[] }).parameterTypes; - if (pTypes !== undefined && pTypes.length > 0 && node.label === 'Method') { - const pKey = qualifiedKey(props.filePath, node.label, `${qualified}~${pTypes.join(',')}`); + if ( + pTypes !== undefined && + pTypes.length > 0 && + (node.label === 'Function' || node.label === 'Method') + ) { + const pKey = qualifiedKey( + props.filePath, + node.label, + `${keyQualified}~${pTypes.join(',')}`, + ); // Each overload is unique — set unconditionally. - lookup.set(pKey, node.id); + if (!lookup.has(pKey)) lookup.set(pKey, node.id); + } + const pClasses = (props as { parameterTypeClasses?: readonly ParameterTypeClass[] }) + .parameterTypeClasses; + const shapeTag = parameterShapeIdTag(pTypes, pClasses); + if (shapeTag !== '' && (node.label === 'Function' || node.label === 'Method')) { + const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`); + if (!lookup.has(shapeKey)) lookup.set(shapeKey, node.id); } // SFINAE / `requires`-clause disambiguation (issue #1579) — register // a constraint-fingerprinted key so resolveDefGraphId can locate the @@ -109,7 +130,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { const cKey = qualifiedKey( props.filePath, node.label, - `${qualified}${templateConstraintsIdTag(tConstraints)}`, + `${keyQualified}${templateConstraintsIdTag(tConstraints)}`, ); lookup.set(cKey, node.id); } @@ -125,7 +146,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { const tKey = qualifiedKey( props.filePath, node.label, - `${qualified}~${props.templateArguments.join(',')}`, + `${keyQualified}~${props.templateArguments.join(',')}`, ); if (!lookup.has(tKey)) lookup.set(tKey, node.id); } 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 f28cd3185..b69252ce2 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/overload-narrowing.ts @@ -35,6 +35,11 @@ * candidates whose template constraints provably fail at the * call site. Three-valued; `'unknown'` keeps the candidate * (monotonicity). + * 4d. Conservative C++ template partial-order approximation. When + * template-placeholder overloads remain tied, prefer a candidate + * whose parameter shape is more specialized for the observed + * argument shape (`T*` over `T`, `const T&` over `T`). Unknown or + * incomparable shapes are left ambiguous. * 5. Empty input returns empty output. */ @@ -205,6 +210,15 @@ export function narrowOverloadCandidates( }); } + if (result.length > 1 && argTypes !== undefined && argTypes.length > 0) { + const partiallyOrdered = rankByTemplatePartialOrdering( + result, + argTypes, + hookCtx?.argumentTypeClasses, + ); + if (partiallyOrdered !== undefined) result = partiallyOrdered; + } + return result; } @@ -330,6 +344,109 @@ function pairwiseCompare(a: readonly number[], b: readonly number[]): -1 | 0 | 1 return 0; } +/** + * Closed-table approximation of C++ function-template partial ordering. + * + * Full `[temp.func.order]` requires template argument deduction. GitNexus + * keeps this graph-safe by recognizing only syntactic placeholder shapes + * that the C++ parameter sidecar already preserves: + * - `T*` is more specialized than `T` for pointer arguments. + * + * Anything with unknown argument shape, non-template parameter spelling, or + * incomparable specialized shapes stays ambiguous so callers suppress. The + * placeholder detector is intentionally narrow: lowercase template parameters + * are left ambiguous rather than guessed. + */ +function rankByTemplatePartialOrdering( + candidates: readonly SymbolDefinition[], + argTypes: readonly string[], + argTypeClasses?: readonly ParameterTypeClass[], +): readonly SymbolDefinition[] | undefined { + if (argTypeClasses === undefined) return undefined; + + const viable: Array<{ def: SymbolDefinition; ranks: number[] }> = []; + for (const def of candidates) { + const params = def.parameterTypes; + const paramClasses = def.parameterTypeClasses; + if (params === undefined || paramClasses === undefined) continue; + + const ranks: number[] = []; + let sawTemplateSlot = false; + let ok = true; + for (let i = 0; i < argTypes.length; i++) { + const paramType = parameterTypeAt(params, i); + const paramClass = parameterTypeClassAt(paramClasses, i); + const argClass = argTypeClasses[i]; + if (paramType === undefined || paramClass === undefined || argClass === undefined) { + ok = false; + break; + } + + const rank = templatePartialOrderSlotRank(paramType, paramClass, argClass); + if (rank === undefined) { + ok = false; + break; + } + sawTemplateSlot ||= isTemplatePlaceholder(paramType); + ranks.push(rank); + } + if (ok && sawTemplateSlot) viable.push({ def, ranks }); + } + if (viable.length === 0) return undefined; + if (viable.length !== candidates.length) return []; + if (viable.length <= 1) return viable.map((v) => v.def); + + const dominated = new Set(); + for (let i = 0; i < viable.length; i++) { + if (dominated.has(i)) continue; + for (let j = i + 1; j < viable.length; j++) { + if (dominated.has(j)) continue; + const cmp = compareSpecializationRanks(viable[i].ranks, viable[j].ranks); + if (cmp < 0) dominated.add(j); + else if (cmp > 0) dominated.add(i); + } + } + return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def); +} + +function templatePartialOrderSlotRank( + paramType: string, + paramClass: ParameterTypeClass, + argClass: ParameterTypeClass, +): number | undefined { + if (!isTemplatePlaceholder(paramType)) return undefined; + if (argClass.indirection === 'unknown' || paramClass.indirection === 'unknown') { + return undefined; + } + if (isPointerShape(paramClass)) { + return isPointerShape(argClass) ? 3 : undefined; + } + if (paramClass.indirection === 'value') return 1; + return undefined; +} + +function isTemplatePlaceholder(typeName: string): boolean { + return /^[A-Z]\w*$/.test(typeName); +} + +/** + * Higher specialization rank is better. Returns -1 when `a` dominates `b`, + * +1 when `b` dominates `a`, and 0 for ties / incomparable vectors. + */ +function compareSpecializationRanks(a: readonly number[], b: readonly number[]): -1 | 0 | 1 { + let aBetter = false; + let bBetter = false; + const len = Math.min(a.length, b.length); + for (let i = 0; i < len; i++) { + if (a[i] > b[i]) aBetter = true; + else if (b[i] > a[i]) bBetter = true; + if (aBetter && bBetter) return 0; + } + if (aBetter && !bBetter) return -1; + if (bBetter && !aBetter) return 1; + return 0; +} + /** * Detect when >1 candidate share identical `parameterTypes` after the * per-language normalizer has collapsed distinct underlying types. This diff --git a/gitnexus/src/core/ingestion/utils/method-props.ts b/gitnexus/src/core/ingestion/utils/method-props.ts index 2a08df8b6..b97b5de4b 100644 --- a/gitnexus/src/core/ingestion/utils/method-props.ts +++ b/gitnexus/src/core/ingestion/utils/method-props.ts @@ -1,5 +1,5 @@ import type { MethodInfo } from '../method-types.js'; -import { SupportedLanguages } from 'gitnexus-shared'; +import { SupportedLanguages, type ParameterTypeClass } from 'gitnexus-shared'; /** Languages where class overload signatures are declaration-only contracts * that should collapse to the implementation body's node ID. */ @@ -139,13 +139,58 @@ export function constTagForId( return ''; } +/** + * Disambiguate function-template overloads whose normalized parameter types + * intentionally collapse to the same placeholder token (`T`, `U`, ...), but + * whose C++ sidecar shape is semantically different (`T` vs `T*` / `T&`). + * + * Kept intentionally narrow: concrete types already use the existing raw-type + * overload tag, and non-template languages should not acquire sidecar-shaped + * IDs. + */ +export function parameterShapeIdTag( + parameterTypes?: readonly string[], + parameterTypeClasses?: readonly ParameterTypeClass[], +): string { + if ( + parameterTypes === undefined || + parameterTypeClasses === undefined || + parameterTypes.length === 0 + ) { + return ''; + } + let hasTemplatePlaceholder = false; + let hasDisambiguatingShape = false; + const parts: string[] = []; + for (let i = 0; i < parameterTypes.length; i++) { + const type = parameterTypes[i]; + const typeClass = parameterTypeClasses[i]; + if (typeClass === undefined) return ''; + if (/^[A-Z]\w*$/.test(type)) hasTemplatePlaceholder = true; + if ( + typeClass.indirection !== 'value' || + typeClass.pointerDepth > 0 || + (typeClass.cv !== 'none' && typeClass.cv !== 'unknown') + ) { + hasDisambiguatingShape = true; + } + parts.push( + `${type}:${typeClass.cv}:${typeClass.indirection}:${typeClass.pointerDepth.toString()}`, + ); + } + if (!hasTemplatePlaceholder || !hasDisambiguatingShape) return ''; + return `~shape:${parts.join('|')}`; +} + /** Convert MethodInfo from methodExtractor into flat properties for a graph node. */ export function buildMethodProps(info: MethodInfo): Record { const types: string[] = []; + const typeClasses: ParameterTypeClass[] = []; let optionalCount = 0; let hasVariadic = false; for (const p of info.parameters) { if (p.type !== null) types.push(p.type); + if (p.typeClass !== undefined) typeClasses.push(p.typeClass); if (p.isOptional) optionalCount++; if (p.isVariadic) hasVariadic = true; } @@ -155,6 +200,9 @@ export function buildMethodProps(info: MethodInfo): Record { ? { requiredParameterCount: info.parameters.length - optionalCount } : {}), ...(types.length > 0 ? { parameterTypes: types } : {}), + ...(typeClasses.length === info.parameters.length && typeClasses.length > 0 + ? { parameterTypeClasses: typeClasses } + : {}), returnType: info.returnType ?? undefined, visibility: info.visibility, isStatic: info.isStatic, diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index a20d86d92..0a4bc47e9 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -85,6 +85,7 @@ import { typeTagForId, constTagForId, buildCollisionGroups, + parameterShapeIdTag, } from '../utils/method-props.js'; import { extractTemplateArguments, templateArgumentsIdTag } from '../utils/template-arguments.js'; import type { LanguageProvider } from '../language-provider.js'; @@ -1814,6 +1815,13 @@ const processFileGroup = ( ); arityTag += constTagForId(defMethodMap, nodeName, arityForId, defMethodInfo, groups); } + const parameterShapeTag = + nodeLabel === 'Function' || nodeLabel === 'Method' + ? parameterShapeIdTag( + methodProps.parameterTypes as string[] | undefined, + methodProps.parameterTypeClasses as ParameterTypeClass[] | undefined, + ) + : ''; const classTemplateArguments = extractedClassSymbol?.templateArguments ?? provider.classExtractor?.extractTemplateArgumentsFromCapture?.({ @@ -1837,7 +1845,7 @@ const processFileGroup = ( : ''; const nodeId = generateId( nodeLabel, - `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}`, + `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${parameterShapeTag}`, ); const classNodeForSymbol = definitionNode || nameNode; const qualifiedTypeName = diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp new file mode 100644 index 000000000..56f6f51fd --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp @@ -0,0 +1,13 @@ +template +void pick(T value) { +} + +template +void pick(T* value) { +} + +void run() { + int n = 0; + int* p = &n; + pick(p); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp new file mode 100644 index 000000000..2daba2444 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp @@ -0,0 +1,13 @@ +template +void pick(T* lhs, T rhs) { +} + +template +void pick(T lhs, T* rhs) { +} + +void run() { + int n = 0; + int* p = &n; + pick(p, p); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index a4a9a9fa8..57fadebb1 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1473,6 +1473,34 @@ describe('C++ template overload disambiguation (vector vs vector)', }); }); +describe('C++ template partial ordering (#1635)', () => { + it('pick(T*) wins over pick(T) for pointer arguments', async () => { + const result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-template-partial-order-pointer'), + () => {}, + ); + + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'pick', + ); + expect(calls.length).toBe(1); + const target = result.graph.getNode(calls[0].rel.targetId); + expect(target?.properties.startLine).toBe(5); + }); + + it('pick(T*, T) vs pick(T, T*) emits zero CALLS edges when partial ordering is incomparable', async () => { + const result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-template-partial-order-tied'), + () => {}, + ); + + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'pick', + ); + expect(calls.length).toBe(0); + }); +}); + // ── Phase P: C++ template overload cross-file + chain resolution ────────── describe('C++ template overload cross-file and chain resolution', () => { diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index f91349d91..d4722d2e7 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -384,6 +384,10 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly::g() -> this->f() emits zero CALLS when Inner is two levels deep (ns.a.b) — one-level cap enforced', + // Template partial ordering (#1635) relies on C++ parameter type-class + // sidecars and scope-resolver overload narrowing. The legacy DAG does not + // rank function-template shapes, so it leaves the call unresolved. + 'pick(T*) wins over pick(T) for pointer arguments', ]), }; 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 93cf86dc2..7203e0633 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 @@ -21,6 +21,13 @@ const pointer = (base: string): ParameterTypeClass => ({ pointerDepth: 1, }); +const constRef = (base: string): ParameterTypeClass => ({ + base, + cv: 'const', + indirection: 'lvalue-ref', + pointerDepth: 0, +}); + const ellipsis = (): ParameterTypeClass => ({ base: '...', cv: 'unknown', @@ -143,3 +150,79 @@ describe('narrowOverloadCandidates with C++ pointer-rank sidecars (#1637)', () = expect(result.map((d) => d.nodeId)).toEqual(['log:ellipsis']); }); }); + +describe('narrowOverloadCandidates with C++ template partial ordering (#1635)', () => { + it('selects T* over T for pointer arguments', () => { + const byValue = mkDef('pick:T', ['T'], [value('T')]); + const byPointer = mkDef('pick:T*', ['T'], [pointer('T')]); + + const result = narrowOverloadCandidates([byValue, byPointer], 1, ['int'], { + argumentTypeClasses: [pointer('int')], + }); + + expect(result.map((d) => d.nodeId)).toEqual(['pick:T*']); + }); + + it('keeps const T& versus T ambiguous for value arguments', () => { + const byValue = mkDef('pick:T', ['T'], [value('T')]); + const byReference = mkDef('pick:const-T-ref', ['T'], [constRef('T')]); + + const result = narrowOverloadCandidates([byValue, byReference], 1, ['int'], { + argumentTypeClasses: [value('int')], + }); + + expect(result.map((d) => d.nodeId)).toEqual([]); + }); + + it('suppresses when any surviving candidate cannot participate in ordering', () => { + const concreteSlot = mkDef('pick:T-int', ['T', 'int'], [value('T'), value('int')]); + const pointerSlot = mkDef('pick:T-T*', ['T', 'T'], [value('T'), pointer('T')]); + + const result = narrowOverloadCandidates([concreteSlot, pointerSlot], 2, ['int', 'int'], { + argumentTypeClasses: [pointer('int'), pointer('int')], + }); + + expect(result.map((d) => d.nodeId)).toEqual([]); + }); + + it('suppresses when a surviving candidate lacks parameter sidecars', () => { + const withoutSidecar: SymbolDefinition = { + ...mkDef('pick:no-sidecar', ['T'], [value('T')]), + parameterTypeClasses: undefined, + }; + const byPointer = mkDef('pick:T*', ['T'], [pointer('T')]); + + const result = narrowOverloadCandidates([withoutSidecar, byPointer], 1, ['int'], { + argumentTypeClasses: [pointer('int')], + }); + + expect(result.map((d) => d.nodeId)).toEqual([]); + }); + + it('leaves lowercase template placeholders ambiguous rather than guessing', () => { + const byValue = mkDef('pick:t', ['t'], [value('t')]); + const byPointer = mkDef('pick:t*', ['t'], [pointer('t')]); + + const result = narrowOverloadCandidates([byValue, byPointer], 1, ['int'], { + argumentTypeClasses: [pointer('int')], + }); + + expect(result.map((d) => d.nodeId)).toEqual(['pick:t', 'pick:t*']); + }); + + it('keeps crossed template shapes ambiguous', () => { + const pointerThenValue = mkDef('pick:T*-T', ['T', 'T'], [pointer('T'), value('T')]); + const valueThenPointer = mkDef('pick:T-T*', ['T', 'T'], [value('T'), pointer('T')]); + + const result = narrowOverloadCandidates( + [pointerThenValue, valueThenPointer], + 2, + ['int', 'int'], + { + argumentTypeClasses: [pointer('int'), pointer('int')], + }, + ); + + expect(result.map((d) => d.nodeId)).toEqual(['pick:T*-T', 'pick:T-T*']); + }); +});