Skip to content
1 change: 1 addition & 0 deletions gitnexus-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export type {
RegistryProviders,
OwnerScopedContributor,
ArityVerdict,
ConstraintContext,
} from './scope-resolution/registries/context.js';

// Scope tree spine + position lookup (RFC §2.2 + §3.1; Ring 2 SHARED #912)
Expand Down
33 changes: 33 additions & 0 deletions gitnexus-shared/src/scope-resolution/registries/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,43 @@ export interface RegistryProviders {
* when absent, every candidate receives `'unknown'` (neutral signal).
*/
arityCompatibility?(callsite: Callsite, def: SymbolDefinition): ArityVerdict;

/**
* Language-specific constraint compatibility between a callsite and a
* candidate `def`. Mirrors `arityCompatibility` and shares its three-valued
* verdict shape; the third value `'unknown'` MUST keep the candidate
* (monotonicity: adding a predicate can only narrow correctly, never
* produce a wrong edge). Consulted by `narrowOverloadCandidates` after
* arity + type filters when a candidate carries `templateConstraints`.
*
* Optional; when absent the constraint filter is a pass-through. Languages
* with no constrained-overload semantics leave this undefined.
*/
constraintCompatibility?(
callsite: Callsite,
def: SymbolDefinition,
ctx: ConstraintContext,
): ArityVerdict;
}

export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';

/**
* Context threaded into `constraintCompatibility`. Kept minimal in the
* Tier-A scope (only `argumentTypes`, riding here until a separate
* `Callsite`-widening refactor moves them onto the call site directly).
* Future Tier-B graph-aware predicates (`is_base_of_v`, etc.) will widen
* this interface with `lookupTypeByName` and similar helpers.
*/
export interface ConstraintContext {
/**
* Per-slot argument types at the call site, normalized per the language
* adapter. Empty string means unknown. Same convention as
* `narrowOverloadCandidates`' `argTypes` parameter.
*/
readonly argumentTypes?: readonly string[];
}

// ─── Owner-scoped contributor (concrete shape for `RegistryContributor`) ────

/**
Expand Down
7 changes: 7 additions & 0 deletions gitnexus-shared/src/scope-resolution/symbol-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export interface SymbolDefinition {
declaredType?: string;
/** Generic/template specialization arguments for class-like symbols (e.g. ['User'], ['T*']). */
templateArguments?: string[];
/** Per-language constraint payload for template / generic overloads
* (e.g. C++ `enable_if_t<P, T>` predicate trees, C++20 `requires` clauses).
* Opaque to shared code — the producing language adapter owns the shape
* and is the only consumer. Read via the optional
* `ScopeResolver.constraintCompatibility` hook during overload narrowing.
* Absent for symbols that have no constraints (the common case). */
templateConstraints?: unknown;
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
ownerId?: string;
}
17 changes: 15 additions & 2 deletions gitnexus/.claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
{
"permissions": {
"allow": ["mcp__plugin_claude-mem_mcp-search__get_observations"]
}
"allow": [
"mcp__plugin_claude-mem_mcp-search__get_observations",
"Skill(gitnexus-exploring)",
"Bash(npx gitnexus *)",
"mcp__obsidian-memory__search_nodes",
"mcp__obsidian-memory__add_observations",
"WebSearch",
"WebFetch(domain:cppreference.net)",
"Bash(xargs grep -l \"templateArguments\\\\|parameterTypes\")",
"Bash(gh issue *)",
"Bash(gh pr *)"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["gitnexus"]
}
31 changes: 31 additions & 0 deletions gitnexus/src/core/ingestion/language-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ interface LanguageProviderConfig {
ancestorNode: SyntaxNode,
) => { funcName: string; label: NodeLabel } | null;

// ── Template constraint extraction (SFINAE / `requires`) ────────────
/**
* Extract a per-language template-constraint payload for a templated
* function / method definition. Used by `parsing-processor` to
* disambiguate same-name same-arity overloads whose distinguishing
* signal is their template constraints rather than their parameter
* types — the canonical C++ SFINAE case (issue #1579):
*
* template<class T, std::enable_if_t<is_integral_v<T>, int> = 0>
* void process(T); // overload A
*
* template<class T, std::enable_if_t<is_floating_point_v<T>, int> = 0>
* void process(T); // overload B
*
* Both overloads' `parameterTypes` collapse to `['T']`, so without a
* constraint fingerprint in the graph node ID they merge into one
* Function node and the resolver only ever sees one candidate to
* narrow. The hook's return value is stamped onto the node's ID via
* `templateConstraintsIdTag()` AND stored on the node's
* `templateConstraints` property so `resolveDefGraphId` can look up
* the right overload by re-hashing the def's constraints at resolve
* time.
*
* Returns the opaque payload (any JSON-serializable shape — the
* producing adapter owns it; shared code MUST NOT inspect) or
* `undefined` when no constraints exist / the node isn't a templated
* function. Languages without SFINAE / concept semantics leave this
* undefined and the disambiguation is a pass-through.
*/
readonly extractTemplateConstraints?: (definitionNode: SyntaxNode) => unknown;

// ── Labels ────────────────────────────────────────────────────────
/** Override the default node label for definition.function captures.
* Return null to skip (C/C++ duplicate), a different label to reclassify
Expand Down
45 changes: 45 additions & 0 deletions gitnexus/src/core/ingestion/languages/c-cpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
cppImportOwningScope,
cppReceiverBinding,
} from './cpp/index.js';
import { extractCppTemplateConstraints } from './cpp/constraint-extractor.js';

const C_BUILT_INS: ReadonlySet<string> = new Set([
'printf',
Expand Down Expand Up @@ -463,6 +464,7 @@ export const cppProvider = defineLanguage({
heritageExtractor: createHeritageExtractor(SupportedLanguages.CPlusPlus),
labelOverride: cppLabelOverride,
builtInNames: C_BUILT_INS,
extractTemplateConstraints: extractCppTemplateConstraintsForProvider,

// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
emitScopeCaptures: emitCppScopeCaptures,
Expand All @@ -474,3 +476,46 @@ export const cppProvider = defineLanguage({
arityCompatibility: cppArityCompatibility,
// mergeBindings + resolveImportTarget live on ScopeResolver (see cpp/scope-resolver.ts).
});

/**
* LanguageProvider hook: walk from a function definition node up to its
* enclosing `template_declaration` and extract the SFINAE / `requires`-
* clause constraint payload. Used by `parsing-processor` to fingerprint
* the graph node ID so two SFINAE overloads with identical
* `parameterTypes` get distinct nodes (issue #1579).
*
* Returns `undefined` for non-templated functions and for templated
* functions whose constraints the extractor can't model — both cases
* result in no constraint suffix on the node ID.
*/
function extractCppTemplateConstraintsForProvider(definitionNode: SyntaxNode): unknown {
// Walk up to the enclosing template_declaration. Bound the walk so we
// can't accidentally land on a far-ancestor template_declaration that
// wraps an unrelated function.
let cur: SyntaxNode | null = definitionNode.parent;
let hops = 8;
let templateDecl: SyntaxNode | null = null;
while (cur !== null && hops-- > 0) {
if (cur.type === 'template_declaration') {
templateDecl = cur;
break;
}
if (cur.type === 'translation_unit') break;
cur = cur.parent;
}
if (templateDecl === null) return undefined;

// Find the function_declarator inside the function definition so the
// extractor can map template params to function-argument indices.
let declarator: SyntaxNode | null = definitionNode.childForFieldName('declarator');
let walk = 8;
while (declarator !== null && walk-- > 0) {
if (declarator.type === 'function_declarator') break;
if (declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator') {
declarator = declarator.childForFieldName('declarator');
continue;
}
break;
}
return extractCppTemplateConstraints(templateDecl, declarator);
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function computeCppCallArity(node: SyntaxNode): number {
* argument types (e.g. `inferCppLiteralType` returns `'string'` for
* string literals, not `'std::string'`).
*/
function normalizeCppParamType(raw: string): string {
export function normalizeCppParamType(raw: string): string {
let t = raw.trim();
// Strip const, volatile, etc.
t = t.replace(/\b(const|volatile|restrict|mutable|constexpr)\b/g, '').trim();
Expand Down
5 changes: 4 additions & 1 deletion gitnexus/src/core/ingestion/languages/cpp/arity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type { Callsite, SymbolDefinition } from 'gitnexus-shared';
* - Default parameters (requiredParameterCount < parameterCount)
* - Variadic functions (C-style `...`)
* - Parameter packs (V1: treated as variadic)
* - Templates (V1: generic-ignored, arity check on non-template params)
* - Templates: arity check on non-template params; SFINAE / `requires`
* constraints are filtered separately via `constraintCompatibility`
* (see `constraint-filter.ts` and issue #1579). Type-argument generic
* substitution (`List<T>` ≡ `List<U>`) remains out of V1 scope.
*
* Verdict:
* - 'compatible': callsite.arity fits within [required, total] range
Expand Down
74 changes: 74 additions & 0 deletions gitnexus/src/core/ingestion/languages/cpp/captures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-link
import { markCppDependentBase } from './two-phase-lookup.js';
import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js';
import { markCppInlineNamespaceRange } from './inline-namespaces.js';
import { extractCppTemplateConstraints } from './constraint-extractor.js';

export function emitCppScopeCaptures(
sourceText: string,
Expand Down Expand Up @@ -130,6 +131,24 @@ export function emitCppScopeCaptures(
markFileLocal(filePath, nameText);
}
}

// SFINAE / `requires`-clause aware constraints for overload
// narrowing (issue #1579). Walk from the enclosing
// `template_declaration` — not the inner `function_definition` —
// so inline method templates (`template<...> class C { template<...> void f(); }`)
// pick up the correct outer constraint scope.
const templateDecl = findEnclosingTemplateDeclaration(fnNode);
if (templateDecl !== null) {
const funcDeclarator = findFunctionDeclarator(fnNode);
const constraints = extractCppTemplateConstraints(templateDecl, funcDeclarator);
if (constraints !== undefined) {
grouped['@declaration.template-constraints'] = syntheticCapture(
'@declaration.template-constraints',
fnNode,
JSON.stringify(constraints),
);
}
}
}
}

Expand Down Expand Up @@ -552,6 +571,52 @@ function extractBaseLookupName(baseNode: SyntaxNode): string {
return '';
}

/**
* Walk parent chain from a function_definition / declaration / field_declaration
* to find the enclosing `template_declaration`. Returns null when the function
* isn't templated. The walk only ascends through wrapper nodes the C++
* grammar inserts between `template_declaration` and the function — direct
* parent in the common case, two hops for member templates whose outer
* class is also templated (we return the INNERMOST template_declaration,
* which carries this function's own template parameters).
*/
function findEnclosingTemplateDeclaration(fnNode: SyntaxNode): SyntaxNode | null {
let cur: SyntaxNode | null = fnNode.parent;
// Cap the walk — `template_declaration` is typically the immediate parent
// or one wrapper away. Anything deeper is an inline-method-in-template
// shape and we still want the innermost templates_declaration whose body
// wraps `fnNode`.
let hops = 8;
while (cur !== null && hops-- > 0) {
if (cur.type === 'template_declaration') return cur;
// Don't ascend past structural boundaries that should reset template scope.
if (cur.type === 'translation_unit') return null;
cur = cur.parent;
}
return null;
}

/**
* Locate the `function_declarator` AST node within a function definition
* or declaration. Unwraps pointer/reference declarator wrappers. Returns
* null when no function_declarator is found (e.g. variable declaration
* mis-classified upstream).
*/
function findFunctionDeclarator(fnNode: SyntaxNode): SyntaxNode | null {
const direct = fnNode.childForFieldName('declarator');
let cur: SyntaxNode | null = direct;
let hops = 8;
while (cur !== null && hops-- > 0) {
if (cur.type === 'function_declarator') return cur;
if (cur.type === 'pointer_declarator' || cur.type === 'reference_declarator') {
cur = cur.childForFieldName('declarator');
continue;
}
break;
}
return findFirstDescendantOfType(fnNode, 'function_declarator');
}

/** Find the first direct child matching one of the given types. */
function findChildOfType(node: SyntaxNode, types: readonly string[]): SyntaxNode | null {
for (let i = 0; i < node.childCount; i++) {
Expand Down Expand Up @@ -655,6 +720,15 @@ function inferCppLiteralType(node: SyntaxNode): string {
* - `int n = ...` → 'int'
* - `const int n = ...` → 'int'
* Returns empty string if no declaration found or type is auto/placeholder.
*
* Limitation: only `declaration` siblings inside the enclosing
* `compound_statement` are inspected. Function parameters live in the
* `function_declarator`'s `parameter_list` and are NOT resolved here, so
* `void run(int n) { process(n); }`
* infers `''` for `n` and the constraint filter falls through to
* `'unknown'` → ambiguity suppression → 0 CALLS edges. This is a
* "degrade not lie" gap (no wrong edges, just missing ones); extending
* the scan to `parameter_list` is tracked under #1579 as a follow-up.
*/
function lookupDeclaredTypeForIdentifier(identNode: SyntaxNode): string {
const varName = identNode.text;
Expand Down
Loading
Loading