Skip to content

feat(cpp): sfinae filter#1623

Merged
magyargergo merged 8 commits into
abhigyanpatwari:mainfrom
zander-raycraft:feat/cpp-sfinae-filter
May 16, 2026
Merged

feat(cpp): sfinae filter#1623
magyargergo merged 8 commits into
abhigyanpatwari:mainfrom
zander-raycraft:feat/cpp-sfinae-filter

Conversation

@zander-raycraft

@zander-raycraft zander-raycraft commented May 16, 2026

Copy link
Copy Markdown
Collaborator

SFINAE / requires-clause aware overload filter (#1579)

Summary

  • C++ function template overloads guarded by mutually exclusive enable_if_t<> predicates or C++20 requires clauses now resolve to the correct overload at each call site instead of collapsing into a false multi-candidate ambiguity. The canonical case from the ticket (process(42) for an integral template and process(3.14) for a floating template) used to produce zero CALLS edges because both overloads looked viable. It now produces two CALLS edges, one to each overload, matching what a real C++ compiler would resolve.
  • The fix adds an optional constraintCompatibility hook on both RegistryProviders and ScopeResolver that mirrors the existing arityCompatibility surface, along with a new opaque templateConstraints field on SymbolDefinition that carries the per language constraint payload. The hook returns one of three verdicts (compatible, incompatible, or unknown) and the filter only drops a candidate when the verdict is incompatible. This Kleene 3 valued semantics is the monotonicity guarantee: adding a predicate to the registry can only narrow correctly and never produce a wrong edge.
  • The C++ side ships a curated registry of 4 <type_traits> predicates (is_integral_v, is_floating_point_v, is_arithmetic_v, is_same_v) with truth tables hardcoded against a coarse type class enum. This satisfies DoD option 2 from the parent ticket and closes roughly 60 percent of real world SFINAE cases without a full constexpr evaluation engine. The remaining six Tier A predicates (is_void_v, is_enum_v, and similar) are scoped as one line follow up adds once the normalizeCppParamType cv / ref / pointer sidecar refactor lands.
  • A constraint fingerprint is now baked into the graph node ID for templated functions. Without this, two SFINAE overloads whose parameter types both normalize to ['T'] would collapse to the same Function node and the constraint filter would never see two candidates to narrow between. The fingerprint follows the same ~tag suffix pattern that parameterTypes and templateArguments already use elsewhere in the pipeline.
  • Four new integration fixtures cover the golden case (F1 and F2 AST shapes), the C++20 requires clause shape (F4), the monotonicity contract for unrecognized predicates, and the filter ordering invariant that arity always runs before the constraint check.

Changed files

File Change
gitnexus-shared/.../symbol-definition.ts Adds the opaque templateConstraints?: unknown field that carries the per language constraint payload
gitnexus-shared/.../registries/context.ts Adds the optional constraintCompatibility hook to RegistryProviders and the new ConstraintContext interface
gitnexus-shared/src/index.ts Re exports ConstraintContext from the public surface
scope-resolution/contract/scope-resolver.ts Adds the optional constraintCompatibility hook to the ScopeResolver interface
scope-resolution/passes/overload-narrowing.ts Adds the new constraint filter step after the existing arity and parameter type filters, and extends the function signature with an optional hookCtx parameter
scope-resolution/passes/receiver-bound-calls.ts Threads hookCtx into pickOverload and adds constraintCompatibility to the receiver bound provider subset
scope-resolution/passes/free-call-fallback.ts Threads hookCtx into pickImplicitThisOverload and adds a targeted narrow path for the no ADL multi overload case, gated on whether any candidate has constraints so the local shadows import scope walk semantics are preserved for non SFINAE C++
scope-resolution/pipeline/run.ts Wires constraintCompatibility from the provider through the orchestrator options bag into the free call fallback
scope-resolution/graph-bridge/ids.ts Tries the constraint fingerprinted lookup key first in resolveDefGraphId so SFINAE overloads route to their distinct nodes
scope-resolution/graph-bridge/node-lookup.ts Registers a constraint suffixed lookup key for Function and Method nodes that carry templateConstraints
core/ingestion/utils/template-arguments.ts Adds the constraintsHash helper (FNV 1a 32 bit) and the templateConstraintsIdTag ID suffix builder
core/ingestion/parsing-processor.ts Computes the constraint fingerprint via the provider hook, appends it to the generated node ID, and stores the parsed payload on the node properties
core/ingestion/scope-extractor.ts Reads the new @declaration.template-constraints synthetic capture and threads it onto the produced SymbolDefinition
core/ingestion/language-provider.ts Adds the optional extractTemplateConstraints(definitionNode) hook so each language can supply its own constraint payload extractor
core/ingestion/languages/c-cpp.ts Implements extractTemplateConstraints for C++ by walking from the function definition up to its enclosing template_declaration and delegating to the constraint extractor
languages/cpp/type-classifier.ts (new) Maps a normalized type token to a coarse TypeClass enum so the predicate registry can dispatch in closed form
languages/cpp/constraint-extractor.ts (new) Tree sitter walk that recognizes the F1, F2, and F4 AST shapes and produces a structured ConstraintExpr. Anything unrecognized falls back to a kind unknown node, which the filter keeps for monotonicity
languages/cpp/constraint-filter.ts (new) Kleene 3 valued evaluator over ConstraintExpr, the 4 predicate Tier A registry, and the cppConstraintCompatibility hook export. Includes a JSDoc note flagging the is_same_v cv stripping approximation as a known limitation tracked under the sidecar follow up
languages/cpp/captures.ts Emits the @declaration.template-constraints synthetic capture. Walks from the enclosing template_declaration parent rather than the inner function_definition so inline method templates pick up their constraints correctly
languages/cpp/scope-resolver.ts Wires cppConstraintCompatibility onto the C++ scope resolver
languages/cpp/arity-metadata.ts Exports normalizeCppParamType so the constraint filter can reuse the same normalization
languages/cpp/arity.ts Softens the existing "templates are generic ignored" JSDoc note to reflect the new SFINAE filter
test/integration/resolvers/cpp.test.ts Adds four new describe blocks, one per SFINAE fixture, with assertions wired through the existing createResolverParityIt helper
test/integration/resolvers/helpers.ts Adds three strings to LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp for the positive SFINAE assertions that the legacy DAG cannot satisfy
test/unit/scope-resolution/cpp/cpp-constraint.test.ts (new) 17 unit tests across three sections: the extractor with F1, F2, F4, and unknown bail rows, the Kleene truth table for AND, OR, and NOT, and the predicate registry over representative type tokens plus a surface size assertion that pins the registry to four entries
test/unit/scope-resolution/overload-narrowing.test.ts Adds four monotonicity rows that exercise the language agnostic contract: unknown verdicts keep candidates, the filter is a pass through when the hook is absent, and candidates without templateConstraints are never consulted
test/fixtures/lang-resolution/cpp-sfinae-golden/ (new) The golden case from the ticket. Two process<T> overloads guarded by is_integral_v<T> and is_floating_point_v<T>. Asserts two CALLS edges to distinct overloads
test/fixtures/lang-resolution/cpp-sfinae-requires-clause/ (new) Same logical case as the golden one but using the C++20 leading requires shape so the F4 AST path is exercised in integration
test/fixtures/lang-resolution/cpp-sfinae-unknown-predicate/ (new) Uses a predicate name not in the registry and asserts zero edges. Pins the monotonicity contract end to end
test/fixtures/lang-resolution/cpp-sfinae-arity-survives-unknown/ (new) Two overloads of different arity with constraints the registry cannot evaluate. Asserts exactly one edge to the arity matching overload, proving the arity filter runs before the constraint check

Architecture Rational

Function templates guarded by mutually exclusive enable_if_t or requires constraints collapse into a false multi-candidate ambiguity in V1, which suppresses CALLS edges where ISO C++ would resolve cleanly. The canonical example is two process overloads, one guarded by is_integral_v, one by is_floating_point_v, where process(42) should resolve only to the integral overload, but V1 sees both as visible and emits zero edges. The root cause is that V1 does not model template constraints at all: every overload is always viable, and the existing OVERLOAD_AMBIGUOUS sentinel correctly refuses to pick arbitrarily, which is safe but blinds the graph to a very common C++ idiom. The fix follows DoD option 2 from the parent ticket, a curated registry of four <type_traits> predicates plus a Kleene 3-valued evaluator, and the plan estimated it as a narrow change focused on the overload narrowing step. Integration showed it had to be wider than that, because the constraint signal travels through four pipeline stages before it can disambiguate anything: extraction at parse time, graph-node ID generation, node lookup across resolver paths, and the narrowing filter itself. The plan assumed upstream stages would carry the constraint payload through transparently, but both process overloads end up with identical parameterTypes=['T'] after normalization, so the legacy parse phase generated the same graph node ID for both and merged them into one, the constraint filter never got two candidates to compare. Once that surfaced, the fix had to fingerprint the constraint payload into the node ID using the same ~tag suffix pattern that already disambiguates parameter types and template arguments, which meant coordinated edits at every site that participates in that pattern. The rest of the “extra” files are seam plumbing for the same root issue: a new language-provider hook so the parsing processor can compute the constraint fingerprint (it reads raw tree-sitter matches, not the synthetic captures used elsewhere), and a small extra narrow path in the free-call fallback because the SFINAE case takes the free-function route rather than the implicit-this route the original plan threaded through. The end result is 11 new files and 15 modified, but there are no new architectural concepts in any of them, every piece mirrors an existing pattern in the codebase, and the wider surface area is the same surface area the next constrained-overload language (Rust trait bounds, Swift where, TypeScript conditional types) will plug into.

Test plan

  • All five new SFINAE integration tests pass in registry primary mode (golden case asserts twice for the two binding tests, plus one each for requires clause, unknown predicate, and arity survives unknown)
  • All 220 C++ integration tests pass (215 existing plus 5 new) with zero regressions
  • 17 new unit tests in cpp-constraint.test.ts pass across the extractor, Kleene, and registry sections
  • 4 new monotonicity rows in overload-narrowing.test.ts pass and pin the language agnostic contract
  • Legacy parity mode is green: with REGISTRY_PRIMARY_CPP=0 the suite shows 188 passed and 32 expected skips, including the 3 new SFINAE positive assertions added to the failures list. The legacy DAG lacks the constraint filter and the scope resolver only path is expected to win those assertions exclusively
  • Full repo suite passes: 8835 tests across 321 files, zero failures, with only the 32 documented scope resolver only correctness wins skipped
  • npx tsc --noEmit is clean across the monorepo
  • Prettier is clean on all changed files
  • Pre commit hooks pass (eslint, prettier, typecheck)

Closes the C++ SFINAE / concept constrained candidate filtering subtask of #1564.

@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown

@zander-raycraft is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@zander-raycraft zander-raycraft changed the title Feat/cpp sfinae filter feat(cpp): sfinae filter May 16, 2026
@zander-raycraft zander-raycraft marked this pull request as ready for review May 16, 2026 05:21
@github-actions

github-actions Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
9134 9133 0 1 450s

✅ All 9133 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 78.37% 29297/37380 N/A% 🟢 ███████████████░░░░░
Branches 66.81% 18618/27864 N/A% 🟢 █████████████░░░░░░░
Functions 83.27% 2927/3515 N/A% 🟢 ████████████████░░░░
Lines 81.66% 26425/32358 N/A% 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@github-actions

github-actions Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 10m 40s —— View job


Adversarial Production-Readiness Review — PR #1623 (feat/cpp-sfinae-filter#1579)

Task Checklist

  • Read repo context files and relevant architecture
  • Inspect new C++ constraint files (constraint-extractor.ts, constraint-filter.ts, type-classifier.ts)
  • Inspect capture layer (captures.ts, scope-extractor.ts, c-cpp.ts, language-provider.ts)
  • Inspect overload narrowing and pipeline integration (overload-narrowing.ts, free-call-fallback.ts, receiver-bound-calls.ts, run.ts)
  • Inspect graph-bridge ID fingerprinting (ids.ts, node-lookup.ts, template-arguments.ts)
  • Inspect shared contract changes (symbol-definition.ts, context.ts, scope-resolver.ts)
  • Inspect test fixtures and integration tests (cpp.test.ts, helpers.ts, fixtures)
  • Run Unicode/hygiene checks
  • Verify branch diff vs origin/main
  • Assess filtering phase ordering
  • Assess legacy parity skip correctness
  • Compile all findings and write final verdict

Review Bar for This Issue/PR

Adversarial reviewer for #1579 (C++ SFINAE / concept-constrained candidate filtering). PR chooses DoD option 2: curated type-traits approximation. Baseline: GitNexus is a static-analysis approximation of ISO C++ overload resolution, not full parity.


Current PR State


Branch Hygiene Assessment

Classification: merge-from-main commit present but harmless and merge-safe.

bfecf11 Merge remote-tracking branch 'upstream/main' brings in #1595#1600 (ADL V2 work) and #1600 (inline namespace). These are all previously-merged, causally required baseline commits (SFINAE narrowing is layered on top of the ADL merge path from #1599). No unrelated formatter, workflow, or documentation churn. The #1579 delta is cleanly separable as commit da43e1e. No open stacked dependency.


Understanding of the Change

The PR implements constraint-aware overload narrowing in four coordinated stages:

  1. Capture (captures.ts, constraint-extractor.ts): At parse time, for each template_declaration, walk the parameter list for F1/F2 (enable_if_t<P, int> = 0) and the requires_clause child for F4. JSON-serialise the structured ConstraintExpr payload as @declaration.template-constraints.

  2. Graph node ID fingerprinting (parsing-processor.ts, template-arguments.ts): FNV-1a 32-bit hash of the JSON payload is appended as ~c:<hash> to the graph node ID via provider.extractTemplateConstraints. This distinguishes two process<T> overloads whose parameterTypes=['T'] would otherwise generate the same node ID.

  3. Scope-resolution def (scope-extractor.ts, node-lookup.ts, ids.ts): SymbolDefinition.templateConstraints carries the parsed payload. buildGraphNodeLookup registers a constraint-fingerprinted lookup key for each constrained Function/Method node. resolveDefGraphId tries the fingerprinted key first.

  4. Kleene 3-valued filter (constraint-filter.ts, overload-narrowing.ts): After arity and type filters, narrowOverloadCandidates calls constraintCompatibility for each candidate carrying templateConstraints. Verdict: 'incompatible' drops, 'unknown' keeps, 'compatible' keeps. Curated registry of 4 predicates. Filter is wired into all three candidate paths: implicit-this, ordinary free-call, and merged ordinary+ADL.


Findings

Finding 1 — TypeClass enum misses bool and char as integral

Severity: Minor
Evidence: type-classifier.ts:17–58 defines TypeClass with 'bool' and 'char' as separate buckets. constraint-filter.ts:39 evaluates is_integral_v as cls[0] === 'integral'. Separately, captures.ts:705–708 maps true/false literals to 'bool' and char_literal to 'char'. ISO C++ §21.3.4 (Table 48): is_integral_v<bool> and is_integral_v<char> are both true.
Risk: process(true) against an enable_if_t<is_integral_v<T>> overload returns 'incompatible' instead of 'compatible'. If a complementary is_floating_point_v overload exists, both are marked incompatible → 0 edges instead of the correct integral edge. The direction is "degrade not lie" (false negative, not false positive), so no wrong CALLS edge is emitted.
Recommended fix: Add case 'bool': return 'integral' and treat 'char' as integral in classifyType. One-line follow-up add.
Blocks merge: no — falls within the curated approximation scope and does not emit wrong edges. Should be tracked as a follow-up.

Finding 2 — Unit test "F1" label tests the F2 AST shape

Severity: Minor
Evidence: cpp-constraint.test.ts:37–50 (section 1, "F1 — unqualified enable_if_t") uses source std::enable_if_t<is_integral_v<T>, int> = 0 — the std:: prefix makes this the F2 (qualified) shape. The true F1 form (enable_if_t<P, int> = 0 without std::) has no direct unit-test coverage. Code at constraint-extractor.ts:150–155 handles both via the template_type branch and the qualified_identifier unwrap, but the test suite doesn't verify the template_type-direct path independently.
Risk: Very low — both paths route through the same handler. A future grammar change that breaks the template_type path would go undetected by this test.
Recommended fix: Add one test with a genuinely unqualified enable_if_t<is_integral_v<T>, int> = 0 source string and label it "F1". The "F2" label can stay on the fully-qualified form.
Blocks merge: no. Fix this →

Finding 3 — Function-parameter argument type gap

Severity: Minor / documented limitation
Evidence: captures.ts:669–671 calls lookupDeclaredTypeForIdentifier for identifier arguments. lookupDeclaredTypeForIdentifier (captures.ts:724–759) scans the enclosing compound_statement for declaration nodes. Function parameters are NOT in the compound_statement — they live in the function_declarator's parameter_list. Consequence: void run(int n) { process(n); } infers '' for n's type, yields 'unknown' verdict for both overloads, both survive → ambiguous suppression → 0 edges.
Risk: Correct by "degrade not lie" contract. Not a wrong edge. But the PR doesn't document this limitation explicitly — a user might expect process(n) to resolve when n : int is visible.
Recommended fix: Add a JSDoc note in lookupDeclaredTypeForIdentifier stating it only scans local declaration nodes and not function parameters; follow-up can extend it.
Blocks merge: no.

Finding 4 — Global-fallback logicalCallableKey does not include templateConstraints

Severity: Minor / non-regression
Evidence: free-call-fallback.ts:357–365, logicalCallableKey is built from filePath, qualifiedName, type, parameterCount, parameterTypes. It excludes templateConstraints. Two SFINAE overloads with identical parameterTypes=['T'] share the same key. If both overloads reach pickUniqueGlobalCallable (only possible when neither is in the caller's scope chain), scopeSeen deduplication collapses them to one candidate → one edge without constraint evaluation.
Risk: Reaches the global fallback only when both overloads are unimported (no #include). In standard C++ this is rare. Pre-fix behavior was equally broken in this path (same dedup logic). This is a non-regression. The "degrade not lie" comment at free-call-fallback.ts:199–203 acknowledges the multi-survivor suppression but doesn't address the dedup-to-1 path.
Recommended fix: Add templateConstraintsIdTag(def.templateConstraints) to logicalCallableKey, or add a guard that prevents constrained-template candidates from reaching pickUniqueGlobalCallable. Either way this is a follow-up.
Blocks merge: no — pre-existing limitation, not introduced by this PR.

Finding 5 — is_same_v cv-qualifier approximation (documented but imprecise)

Severity: Known limitation, explicitly documented
Evidence: constraint-filter.ts:48–57 — the JSDoc explicitly flags that is_same_v<const T, T> returns 'compatible' when ISO says false, because normalizeCppParamType strips cv-qualifiers before type tokens reach classifyType.
Risk: Could emit a false CALLS edge where the ISO-correct result is suppression, for is_same_v patterns with cv-distinct comparisons. Since this returns 'compatible' (keeps the candidate) rather than 'incompatible' (drops it), the wrong case is emitting one additional edge, not suppressing a correct one. Tracked under the cv-sidecar refactor.
Blocks merge: no — explicitly documented with a tracked follow-up.


PR-Specific Assessment Sections

ISO C++ SFINAE / Constraints Rule Compliance

The PR correctly models SFINAE semantics for the supported subset. The Kleene 3-valued evaluator (constraint-filter.ts:81–120) follows ISO [temp.constr.normal]:

  • AND: incompatible if any child incompatible (short-circuits correctly)
  • OR: compatible if any child compatible (short-circuits correctly)
  • NOT: flips compatibleincompatible, passes through unknown

The 'unknown' verdict is monotonicity-safe: a predicate not in the registry keeps all candidates rather than falsely satisfying or rejecting. The "substitution failure is not an error" contract is implemented as: unknown predicate → all candidates survive → ambiguity suppression (existing "degrade not lie" behavior). No overreach is claimed.

GitNexus Approximation Boundary

The PR explicitly chooses DoD option 2 (curated type-traits) and states this in both the PR description and code. Supported predicates: is_integral_v, is_floating_point_v, is_arithmetic_v, is_same_v (4 total, registry-size-pinned in unit tests). Deferred predicates (F3, F5 AST shapes, is_void_v, is_enum_v, user-defined traits, requires expressions) all fall through to {kind:'unknown'} which preserves the monotonicity guarantee. The approximation boundary is honest and consistently enforced.

Constraint Capture and Predicate Evaluation

Capture: Three AST shapes handled — F1 (unqualified enable_if_t), F2 (std::enable_if_t), F4 (leading requires_clause). Both captures.ts and c-cpp.ts walk to the same template_declaration node and call extractCppTemplateConstraints → identical JSON → identical FNV-1a hash → identical graph node ID suffix. The two pipelines (scope-extractor and parsing-processor) are correctly synchronized.

Predicate evaluation: The evaluator correctly maps paramArgIndex[T]argumentTypes[idx]classifyType(token) → predicate verdict. For the golden case (process(42) and process(3.14)):

  • 42inferCppLiteralType'int'classifyType('int')'integral'is_integral_v'compatible'
  • 3.14'double''floating'is_floating_point_v'compatible'
    The complementary predicate returns 'incompatible' in each case. Filter works correctly for the motivating fixture.

Gap (Finding 1): bool literal → 'bool' TypeClass → is_integral_v returns 'incompatible' (ISO says true). Conservative degradation only.

Candidate Filtering Phase and Overload Integration

Phase ordering (overload-narrowing.ts:56–127): arity filter → type equality filter → constraint filter. This is strictly before ambiguity suppression (isOverloadAmbiguousAfterNormalization). Disabled candidates never reach the ambiguity suppression step. Verified by the arity-survives-unknown fixture which proves arity filter runs before constraint check.

Coverage of all candidate paths:

  • narrowOverloadCandidates is called in pickImplicitThisOverload (free-call-fallback.ts:435) — constraint hook threaded in ✅
  • narrowOverloadCandidates in ordinary-path no-ADL (free-call-fallback.ts:152) — constraint hook threaded in ✅
  • narrowOverloadCandidates in merged ordinary+ADL path (free-call-fallback.ts:181) — constraint hook threaded in ✅
  • pickOverload in receiver-bound-calls.ts:723 — constraint hook threaded in ✅
  • pickUniqueGlobalCallable — no constraint filtering (Finding 4, non-regression)

Interaction with Conversion Ranking, ADL Merge, and Receiver-Bound Calls

Documentation / Permanent Limitation Option

PR does not choose the documentation-only path — it implements the curated approximation. All deferred forms are explicitly listed in constraint-extractor.ts:12–17 JSDoc and return {kind:'unknown'}. The PR description enumerates what's in scope (Tier A, 4 predicates) and what's deferred (6 more predicates, cv-sidecar refactor). The is_same_v cv approximation is flagged with a JSDoc note in constraint-filter.ts:47–50. Downstream users can rely on the monotonicity guarantee.

helpers.ts Legacy DAG Skip Correctness

Three new entries in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp (helpers.ts:186–188):

'enable_if_t<is_integral_v<T>> overload binds only on integral call sites'
'enable_if_t<is_floating_point_v<T>> overload binds only on floating call sites'
'requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)'

These match exactly the it(...) names in cpp.test.ts:3040, 3051, 3075. Comments reference #1579. Skips apply only when REGISTRY_PRIMARY_CPP=0.

The fourth test ('emits zero CALLS edges when predicate is outside the Tier-A registry') is not in expected failures — correct, because legacy also produces 0 edges for unknown-predicate overloads.

The fifth test ('emits exactly 1 CALLS edge to the arity-matching overload...') is not in expected failures — this is likely acceptable because legacy C++ arity filtering would also resolve the 1-arg overload here (the fixture has one 1-arg and one 2-arg overload, both with same unknown predicate).

No stale skips from prior ADL/overload PRs are visible in the new entries. No duplicate or missing entries.

Tests and CI Coverage

Integration fixtures (4 new, each a distinct contract):

  • cpp-sfinae-golden: F2 enable_if_t golden case, process(42)+process(3.14) → 2 distinct edges ✅
  • cpp-sfinae-requires-clause: F4 requires clause, same logical case ✅
  • cpp-sfinae-unknown-predicate: monotonicity — unknown predicate → 0 edges ✅
  • cpp-sfinae-arity-survives-unknown: filter ordering — arity drops bad candidate before constraint check → 1 edge ✅

Target identity: targetIds.size === 2 in the golden test verifies two distinct graph nodes (via constraint-fingerprinted IDs). targetStartLines[0] < targetStartLines[1] verifies distinct source positions. Strong identity check.

Gap: No fixture for process(n) where n is a typed variable argument (function parameter or local variable). The curated approximation's variable-type resolution is untested end-to-end in integration. Minor.

Unit tests (17 in cpp-constraint.test.ts): extractor shapes, Kleene truth table, registry surface. Registry-size assertion (expect(getRegistrySize()).toBe(4)) pins the supported predicate count. 4 monotonicity rows in overload-narrowing.test.ts verify the language-agnostic contract.

CI: All pipelines green. 8991 tests pass; no regressions across 221 existing C++ tests.

Hidden Unicode / Hygiene

No bidi controls (\u202A–\u202E, \u2066–\u2069) found in any changed file. Non-ASCII characters in the new files are visible em-dashes () and arrows (, ) in JSDoc comments — consistent with codebase style. Clean.


Final Verdict

production-ready with minor follow-ups

The PR correctly implements DoD option 2 (curated type-traits approximation) with honest scope declaration and conservative "degrade not lie" semantics throughout. The golden fixture (process(42) / process(3.14)) is fixed end-to-end: the constraint fingerprint distinguishes the two overloads in the graph, the Kleene filter correctly marks the incompatible candidate, and the integration tests verify distinct graph-node targets via targetIds.size === 2. The filtering runs strictly before ambiguity suppression across all three candidate paths (implicit-this, ordinary free-call, merged ordinary+ADL). Legacy parity skips match exact test names and apply only in legacy mode. CI is green with no regressions.

The four minor findings (bool/char TypeClass gap, F1 unit-test label mislabeled as F2, function-parameter type resolution undocumented limitation, global-fallback logicalCallableKey missing templateConstraints) are all within the curated approximation scope, all degrade safely (0 edges rather than wrong edges), and none introduce new incorrect CALLS edges. The first three are one-line or one-test follow-ups that can land without a second PR review cycle.

@magyargergo

Copy link
Copy Markdown
Collaborator

@zander-raycraft Can you please address claude's findings? 🙏

@github-actions

github-actions Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 4m 59s —— View job


Adversarial Production-Readiness Review — PR #1623 (feat/cpp-sfinae-filter#1579)

Task Checklist

  • Read repo context files
  • Inspect branch hygiene (.claude, AGENTS.md, CLAUDE.md, settings.local.json)
  • Inspect shared contracts (symbol-definition.ts, context.ts, scope-resolver.ts)
  • Inspect constraint extraction pipeline (language-provider.ts, c-cpp.ts, captures.ts, scope-extractor.ts, parsing-processor.ts)
  • Inspect new C++ constraint files (constraint-extractor.ts, constraint-filter.ts, type-classifier.ts)
  • Inspect overload narrowing & pipeline (overload-narrowing.ts, free-call-fallback.ts, receiver-bound-calls.ts, run.ts)
  • Inspect graph-bridge ID fingerprinting (ids.ts, node-lookup.ts, template-arguments.ts)
  • Inspect related files (conversion-rank.ts, receiver-bound-calls.ts)
  • Inspect all fixtures and integration/unit tests
  • Run hidden Unicode checks
  • Verify CI status
  • Compile all findings and write final verdict

Review Bar for This PR

Adversarial reviewer for #1579 (C++ SFINAE / concept-constrained candidate filtering). PR chooses DoD option 2: curated type-traits approximation. Baseline: GitNexus is a static-analysis approximation of ISO C++ overload resolution, not full ISO parity.


Current PR State


Branch Hygiene Assessment

Classification: merge-from-main commit present but harmless and merge-safe.

gitnexus/.claude/settings.local.json adds development-time tool permission overrides used during the PR (e.g., WebFetch(domain:cppreference.net), Bash(gh issue *)) — these are dev-context entries with no production runtime effect. AGENTS.md/CLAUDE.md changes are documented index-stat updates and deduplication of the gitnexus:start block. All are causally adjacent to the #1579 work and none alter resolver behavior. The final effective diff is a cohesive, reviewable SFINAE implementation.


Understanding of the Change

The PR wires SFINAE / requires-clause awareness through four pipeline stages:

  1. Extraction (constraint-extractor.ts, captures.ts): At parse time, each template_declaration is walked for F1/F2 (enable_if_t<P, int> = 0) and F4 (requires_clause) shapes. The result is a structured ConstraintExpr JSON payload stored as the @declaration.template-constraints synthetic capture.

  2. Graph node ID fingerprinting (parsing-processor.ts, template-arguments.ts): FNV-1a 32-bit hash of the JSON payload is appended as ~c:<hash> so two process<T> overloads with identical parameterTypes=['T'] generate distinct node IDs. Without this they would collapse to the same Function node.

  3. Scope-resolution lookup (scope-extractor.ts, node-lookup.ts, ids.ts): SymbolDefinition.templateConstraints carries the parsed payload. buildGraphNodeLookup registers a constraint-fingerprinted key for constrained Function/Method nodes. resolveDefGraphId tries the fingerprinted key first.

  4. Kleene 3-valued filter (constraint-filter.ts, overload-narrowing.ts): After arity → exact-type → conversion-rank filters (step 4c), constraintCompatibility drops candidates whose constraints provably fail. Only 'incompatible' drops; 'unknown' keeps. Filter is threaded into all three candidate paths.


ISO C++ Conformance Assessment

Category Details
Conforms Mutually exclusive is_integral_v<T> / is_floating_point_v<T> via F2 enable_if_t and F4 requires clause. bool and char correctly treated as integral per ISO §21.3.4 Table 48 (fixed in ca26c3f). is_arithmetic_v correctly spans integral and floating. Kleene AND/OR/NOT logic matches ISO [temp.constr.normal].
Approximate but acceptable is_same_v strips cv/ref/pointer via normalizeCppParamTypeis_same_v<const T, T> returns 'compatible' rather than ISO-correct false. Explicitly documented with JSDoc note in constraint-filter.ts:55–60, conservative direction (over-permits, never emits wrong edges).
Unsupported but conservative F3 (void-default typename = enable_if_t<P>), F5 (trailing requires), requires_expression blocks, decltype, user-defined predicates, alias templates, named concepts, constrained template parameters. All return {kind:'unknown'} → filter keeps candidate.
Unsupported and risky None found. The implementation never treats an unrecognized constraint as satisfied.

Quantitative scope: ~60% of real-world SFINAE cases per the PR description. The remaining 40% degrade conservatively to ambiguity suppression.


Findings

Finding 1 — long double literals classified as 'floating' but only via 'double' normalization

Severity: Minor, approximation
Evidence: inferCppLiteralType (captures.ts:686) returns 'double' for any number_literal containing . — including 3.14L. The token therefore reaches classifyType as 'double', which maps to 'floating'. A bare type annotation long double (via a variable declaration) would reach classifyType as the string 'long double', which hits the default case → 'class'is_floating_point_v returns 'incompatible' (ISO: true).
ISO C++ relevance: is_floating_point_v<long double> is true per ISO §21.3.5.
GitNexus production risk: Conservative false negative only (no wrong edges). For literals 3.14L, the literal path returns 'double' anyway. The gap only triggers for variable arguments declared as long double — which already has no type resolution per Finding 3 from the previous review.
Recommended fix: Add case 'long double': return 'floating' to classifyType. One-line follow-up.
Blocks merge: no.

Finding 2 — logicalCallableKey does not include templateConstraints in global fallback

Severity: Minor / non-regression
Evidence: free-call-fallback.ts:419–427logicalCallableKey builds a dedup key from [filePath, qualifiedName, type, parameterCount, parameterTypes]. Two constrained process<T> overloads (same parameterTypes=['T']) would be collapsed to 1 candidate in pickUniqueGlobalCallable. The global fallback is only reached when both overloads are unreachable via scope-chain walk (i.e., neither overload is included via #include) — an unlikely pattern for C++ SFINAE overloads, which always share a translation unit. Pre-existing behavior, not introduced by this PR.
ISO C++ relevance: N/A — global fallback is a GitNexus approximation layer.
GitNexus production risk: No wrong edges; at worst 0 edges instead of 2 in the degenerate case.
Recommended fix: Add templateConstraintsIdTag(def.templateConstraints) to logicalCallableKey. Follow-up.
Blocks merge: no — pre-existing limitation, same behavior as before this PR.

Finding 3 — settings.local.json contains development-time permissions

Severity: Informational
Evidence: gitnexus/.claude/settings.local.json adds WebFetch(domain:cppreference.net), Bash(gh issue *) etc. — entries that were used during the PR development session for researching cppreference and GitHub issues.
GitNexus production risk: None. This file governs Claude Code's local allowed-tools list, has no effect on the resolver, parser, or CI pipeline.
Recommended fix: Optionally strip development-session entries before merge to keep settings.local.json minimal. Not blocking.
Blocks merge: no.


PR-Specific Assessment Sections

ISO C++ SFINAE / Constraints Rule Compliance

The implementation correctly models SFINAE semantics for the supported subset. The Kleene 3-valued evaluator in constraint-filter.ts:86–130 follows ISO [temp.constr.normal]:

  • AND: short-circuits on first incompatible; returns compatible only when all children compatible; otherwise unknown
  • OR: short-circuits on first compatible; returns incompatible only when all children incompatible; otherwise unknown
  • NOT: flips compatible↔incompatible, passes through unknown
  • unknown predicate: returns 'unknown' (monotonicity safe) ✅

The 'unknown' verdict is monotonicity-safe: unrecognized predicates keep candidates, falling through to the existing isOverloadAmbiguousAfterNormalization suppression. No constraint is ever treated as "satisfied" when it is only unknown.

Curated GitNexus Approximation Boundary

The four-predicate Tier-A registry is precisely documented. The getRegistrySize() export + unit test pins it at 4. Unsupported predicates are explicitly listed in constraint-extractor.ts:12–17 as deferred items. The is_same_v cv-approximation is explicitly flagged in constraint-filter.ts:55–60. No overclaiming of ISO C++ concepts support anywhere in comments or test names. The requires-clause support is correctly labeled "F4 AST shape" throughout, not "full C++20 concepts support."

Constraint Extraction Correctness

Three AST shapes handled:

  • F1 (unqualified enable_if_t<P, int> = 0): template_type branch in extractEnableIfPredicate
  • F2 (std::enable_if_t<P, int> = 0): qualified_identifier unwrap → template_type
  • F4 (leading requires_clause): childOfType(templateDecl, 'requires_clause')parseRequiresClause

Both captures.ts and c-cpp.ts walk to the same template_declaration and call extractCppTemplateConstraints → identical JSON → identical FNV-1a hash → identical ~c:<hash> suffix.

Explicitly not supported (all return {kind:'unknown'}): F3 void-default, F5 trailing requires, requires_expression blocks, alias templates, decltype, user-defined traits, named concepts. All fall through conservatively. The F1 unit test (cpp-constraint.test.ts:36) now correctly uses genuinely unqualified enable_if_t (no std:: prefix), independently exercising the template_type-direct branch.

Constraint Evaluator and Type-Traits Registry Correctness

The registry truth tables are ISO-correct for the claimed subset:

  • is_integral_v: 'integral' || 'bool' || 'char' → true; others → false. bool and char correctly widened via isIntegralClass() helper (constraint-filter.ts:44–46), so is_same_v<bool, int> still returns 'incompatible' (distinct TypeClass enum values) ✅
  • is_floating_point_v: only 'floating' → true ✅
  • is_arithmetic_v: isIntegralClass(c) || c === 'floating'
  • is_same_v: coarse token equality, unknown when either side is 'unknown', with cv-approximation documented ✅

The verdictFromBool guard (constraint-filter.ts:70–73) returns 'unknown' when cls[0] === 'unknown', preventing false 'incompatible' verdicts for unresolvable argument types.

Candidate Filtering Phase and Overload Integration

Phase ordering (overload-narrowing.ts:87–171):

  1. Arity filter
  2. Exact-type filter
  3. Conversion-rank scoring (step 4b — only fires when exact-type returns 0)
  4. Constraint filter (step 4c)

For SFINAE template overloads (parameterTypes=['T']): exact-type filter yields 0 matches (argType 'int''T'). Conversion-rank scoring also yields 0 viable candidates because cppConversionRank('int', 'T') returns Infinity (T not in ARITHMETIC). The code correctly falls through (if (ranked.length > 0) result = ranked — not taken), leaving result as the arity-filtered set. Constraint filter then correctly disambiguates to the single ISO-correct overload.

Coverage of all candidate paths:

  • pickImplicitThisOverload (free-call-fallback.ts:500) → narrowOverloadCandidates with constraint hook ✅
  • No-ADL multi-overload path (free-call-fallback.ts:192) → constraint hook threaded ✅
  • Merged ordinary+ADL path (free-call-fallback.ts:227) → constraint hook threaded ✅
  • pickOverload in receiver-bound-calls.ts:734) → constraint hook threaded ✅
  • pickUniqueGlobalCallable → no constraint filtering (Finding 2, non-regression)

Disabled candidates cannot reach OVERLOAD_AMBIGUOUS because constraint filter runs before isOverloadAmbiguousAfterNormalization in the caller flow.

Graph Identity / Constraint Fingerprint Safety

The FNV-1a 32-bit hash over JSON.stringify(payload) is deterministic for the same structured CppConstraintPayload. The JSON.stringify is applied to the structured typed payload (not raw source text or AST positions), so field-order stability matches the TypeScript object literal construction in extractCppTemplateConstraints — always the same three fields (templateParams, paramArgIndex, expr) in the same key order.

Fingerprinting is gated on templateConstraints !== undefined, so unconstrained functions are unaffected. resolveDefGraphId tries the constraint-fingerprinted key first only when def.templateConstraints !== undefined (ids.ts:93–103). Old nodes without constraints continue to resolve via the regular qualified or simple-name keys.

FNV-1a 32-bit: 4B possible values, collision probability among SFINAE overloads in one file is negligible in practice (2–3 overloads per name). No security concern; this is a disambiguation tag, not a cryptographic hash.

Shared Contract and Cross-Language Safety

templateConstraints?: unknown in SymbolDefinition is fully optional (symbol-definition.ts:41). constraintCompatibility is optional on both RegistryProviders (context.ts:45) and ScopeResolver (scope-resolver.ts). narrowOverloadCandidates gates the constraint step behind hookCtx?.constraintCompatibility !== undefined && argCount !== undefined — absent hook is a complete pass-through. Candidates without templateConstraints are unconditionally kept (def.templateConstraints === undefinedreturn true). No TypeScript structural typing forces non-C++ providers to add no-op stubs. The Rust/Swift/TypeScript forward-reference comments use "future" language, not "current."

Free-call Fallback, ADL, Receiver-bound, and #1606 Conversion-rank Interaction

#1606 preservation: conversionRankFn and constraintCompatibility are threaded as separate named fields in OverloadNarrowingHookCtx (overload-narrowing.ts:64–77). The conflict-resolution commit (828ac16) unified hook passing without dropping either field. The constraint filter step 4c is additive — it runs after step 4b (conversion-rank), and the two hooks are independent. All conversion-rank fixtures remain green (CI confirms).

Free-call fallback SFINAE path (free-call-fallback.ts:187–213): The hasConstraints guard correctly gates the targeted narrow path on constrained candidates only. For non-SFINAE overload sets (hasConstraints === false and no conversion-rank fn), the code falls through to fnDef = ordinary[0] — exactly pre-#1579 behavior.

ADL path (free-call-fallback.ts:225–243): merged ordinary+ADL candidates both receive constraint filtering. ADL blockers (nonCallableFound, blockScopeDeclFound) suppress ADL before merge — unaffected.

Receiver-bound (receiver-bound-calls.ts:734): pickOverload passes constraintCompatibility from the provider subset. Constrained method overloads on explicit receivers are filtered.

helpers.ts Legacy DAG Skip Correctness

Three new entries (helpers.ts:200–202):

'enable_if_t<is_integral_v<T>> overload binds only on integral call sites'
'enable_if_t<is_floating_point_v<T>> overload binds only on floating call sites'
'requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)'

These match exactly the it(...) strings at cpp.test.ts:3114, 3125, 3149. Comments reference #1579. Skips apply only when REGISTRY_PRIMARY_CPP=0.

The fourth test ('emits zero CALLS edges when predicate is outside the Tier-A registry') is correctly not in expected failures — legacy also produces 0 edges for unknown-predicate overloads. The fifth test ('emits exactly 1 CALLS edge to the arity-matching overload...') is correctly not in expected failures — arity filter works in legacy mode too.

No stale entries from prior PRs visible. No duplicate or missing entries.

Tests and CI Coverage

Integration fixtures (4 new):

  • cpp-sfinae-golden: F2 enable_if_t. Assertion: targetIds.size === 2 proves distinct graph nodes; targetStartLines[0] < targetStartLines[1] proves distinct source positions. Strong identity check ✅
  • cpp-sfinae-requires-clause: F4 shape. targetIds.size === 2
  • cpp-sfinae-unknown-predicate: MyCustomTrait_v not in registry → 'unknown' × 2 → both survive → isOverloadAmbiguousAfterNormalization (identical T params) → 0 edges ✅
  • cpp-sfinae-arity-survives-unknown: bad-arity overload dropped first → 1 edge ✅

Unit tests (17 in cpp-constraint.test.ts): extractor shapes (F1, F2, F4, unknown-bail), Kleene truth table (AND/OR/NOT), registry surface + is_integral_v bool/char coverage. Registry-size assertion getRegistrySize() === 4 pins the supported scope. 4 monotonicity rows in overload-narrowing.test.ts verify the language-agnostic contract.

CI (all 6 workflows green): CI (9025 tests), CodeQL, Gitleaks, Dependency Review, Docker Build & Push, PR Autofix all completed success.

Hidden Unicode / Hygiene

No bidi Unicode control characters (U+202A–U+202E, U+2066–U+2069) found in any changed file under gitnexus/src/, gitnexus-shared/src/, or gitnexus/test/. Non-ASCII characters in new files are visible punctuation (, , ) in JSDoc comments — consistent with codebase style. Clean. ✅


Final Verdict

production-ready

This PR correctly implements DoD option 2 (curated type-traits approximation) with honest scope declaration and consistent "degrade not lie" semantics. The golden fixture process(42) / process(3.14) is fixed end-to-end — constraint fingerprinting distinguishes the two overloads in the graph, the Kleene filter correctly marks the incompatible candidate, and the integration tests verify distinct targetId values. The filtering phase order (arity → exact-type → conversion-rank → constraint → ambiguity-suppression) is correct for template overloads with parameterTypes=['T']; the conversion-rank step falls through gracefully when template params produce Infinity rank, leaving the candidate set intact for constraint filtering. The two remaining minor gaps (long double classification, logicalCallableKey missing templateConstraints in global fallback) are both pre-existing conservative false-negatives that produce 0 edges rather than wrong edges. All DoD criteria are satisfied, CI is clean, and hidden Unicode is absent.

@magyargergo magyargergo merged commit a4dfebd into abhigyanpatwari:main May 16, 2026
29 of 30 checks passed
hohaivu pushed a commit to hohaivu/GitNexus that referenced this pull request May 19, 2026
* feat(cpp): SFINAE-aware overload filter — drops candidates whose enable_if_t / requires constraints fail (abhigyanpatwari#1579)

* fix(cpp):  SFINAE follow-ups for is_integral_v/is_arithmetic_v bool and char support, an unqualified F1 test fixture, and parameter-lookup gap documentation (abhigyanpatwari#1579) -> claude feedback

* revert: reverting all changes to .md files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cpp overload resolution: SFINAE / concept-constrained candidate filtering

2 participants