Repository navigation
fix(ingest): a C++ member call with explicit template arguments minted no call reference - #243
Conversation
…d no call reference `r.get<K>( 1 )`, `p->get<K>( 1 )` and `x.template get<K>()` produced no reference at all. The callee parses as call_expression function: (field_expression field: (template_method name: (field_identifier))) and, behind the `template` keyword, field: (dependent_name (template_method ...)). No C++ reference pattern bound either shape, so the drop happened at EXTRACTION, before ambiguous=/unresolved= could see it. Measured 2026-09-16 on main 3bf884e: a four-line repro answered --callers=get count="0" beside --callers=plain count="1"; --quality-delta reported a method reached only this way as dead-code. The same family had two more wrong answers on the QUALIFIED dependent form, which did extract: `X::template make<int>()` minted a reference NAMED `template make` (resolves to nothing), and `X::template Rebind<int>::f()` keyed its qualifier as `template Rebind`, so the canonical tier missed and the call split over a same-named definition in another scope. Fix: - queries/cpp/tags.scm: one alternation pattern binds both member shapes, @name on the field_identifier. - src/ingest_binds.h: receiverOf reads calleeAccessParent, which steps over template_method and dependent_name to the field_expression. Without it `other.pick<int>()` classified as a BARE call and the enclosing-class rule bound it to the caller's own same-named method. - src/ingest_names.h: skipTemplateDisambiguator (keyword plus whitespace/comments), applied to the qualifier inside immediateScope and to the name in cppResplitRefName — the H4 re-split block moved out of captureTagsFacts, whose complexity is measured; behaviour is unchanged for every other spelling. - kParserVer 96 -> 99 and kIngestParserVerMirror -> 99 in the same commit (open PRs #235 hold 97/98 and #233 98 — next-free rule); kCacheVersion stays 22. test/qschemetrip.hash re-pinned via UPDATE_GOLDEN=1 with a RE-PIN LOG line; kQSnapCacheScheme stays 12. Measured effect (pre-fix vs fixed binary, this repo's src/, 169 files, --no-cache): references 168,457 -> 168,463, edges 18,308 -> 18,309, ambiguous=/unresolved= unchanged. The six references are exactly the `r.pod<T>()` reads in src/gitoracle.h's loadOracleCache: --uses=pod 1 -> 7. Gate: test/cppqualcheck.sh §12 with a new corpus test/cppqualtmplfix/member.cpp — one hand-read literal per spelling (member dot/arrow, x.template, p->template, this->template, nested `>>` arguments, a chained receiver, X::template, X::Y::template, a comment after the keyword), four controls the old patterns already bound (free, qualified, non-type argument, 3-segment static), receiver/arity/qualifier decoys, the fixture header, and a repo-root arm (--callers=pod names loadOracleCache). - RED on the pre-fix binary: 19 of the 24 new checks FAIL (71 PASS / 19 FAIL); header edges=6 ambiguous=1. - RED on a MUTATION build (three one-line reversions of the fix, 8 FAIL): the raw parent in receiverOf binds tqDecoyCaller to the decoy :110; an inert skipTemplateDisambiguator zeroes the three dependent --uses, fails the probe name arm and splits tqCallRebind; callArity's hop bound 4 -> 3 splits tqDepArityCaller. Reverted; the rebuilt binary is byte-identical to the fixed one. - GREEN: 90/90 plain and ALL PASS under the ASan build; sanitizer-clean over the fixture, callformfix/cpp and src/. test/callformcheck.sh row 11 (`b.template memberTmpl<int>()`) was documented-absent at literal 0 with a probeBlind; it now pins 1 with a probeSees (red on the pre-fix binary: 2 FAIL). Not fixed here, and routed around in the fixture rather than pinned (both on main already, both shared by the plain member call): Rule 2 reads no PARAMETER type, so a call on a `Target&` parameter inside a class with a same-named method binds the enclosing class; and a typed receiver splits over a same-class overload set with no arity prune. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (11)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe parser now extracts C++ member calls with explicit template arguments, preserves receiver classification, removes ChangesC++ member template call handling
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Low Suggested reviewers: Merge Risk: ⚪ Minimal · up to The member-template extraction update has matching version metadata and regression coverage, with no supported merge-blocking issue identified. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 34.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 43 functions across 9 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
CodeRabbit pass read, head
CI is green on the same head: every release shard, both asan legs, rhel and fallback-emitter. |
|
Independent review of What was verified:
To fix:
Not in this PR:
CodeRabbit: 0 actionable; the docstring-coverage warning is declined. Re-pins: |
… no pin, and the CHANGELOG measured only src/ Review of #243 (F1, F4). Two gaps, no code change. F4 — the family's documented-absent spellings were unpinned, although callformcheck.sh's own header says those rows are the point: a later widening that starts binding one of them, correctly or not, moved no pin. test/cppqualtmplfix/member.cpp gains a §12e block at the END (no existing line literal moves), and test/cppqualcheck.sh gains arm (g): each spelling behind a SOURCE presence guard, then a literal zero. - r.f<0>( x ) without `template`: tree-sitter reads two comparisons -> --callees=tqCallLiteralArg 0, and --uses=tqLiteralArgTmpl is exactly one role="read" row at member.cpp:158, no call row. - r.Base::f<T>() and p->Base::f<T>() -> --callees=tqCallBaseQual 0; the plain twin r.Base::f() -> 0 too, so the gap is the qualified-field family, not a template one. - r.operator()<T>() -> --callees=tqCallOperatorTmpl 0. All six measure 0 on the pre-fix binary f8e6087 and on the fix alike: fences, not evidence. Header literal symbols 43 -> 54 (3 structs, 4 member functions, 4 callers), edges stay 19. The first cut of the read-row arm FAILED on the fix for a gate reason: `role="call"` also occurs in the full legend text, so the no-call check now greps the <u> rows only. F1 — the CHANGELOG's only measurement was this repo's src/ (+6 refs, +1 edge), which reads as negligible. Measured with the pre-fix (f8e6087) and fix (built_from=1171f7757) binaries, --no-cache, map header plus ripwire_probe's reference total, on llvm-project 4d5358b1d clang/include + clang/lib (2,515 files): references 1,855,925 -> 1,866,100 (+10,175), edges 409,860 -> 413,767 (+3,907), ambiguous 81,601 -> 82,573 (+972), unresolved 3,893 unchanged; --callers getAs 48 -> 492, hasAttr 45 -> 377. On clang/lib/AST the reference delta (246,367 -> 248,394, +2,027) equals the new shape's --match hits (2,027, hits_capped="0"). The entry now also says the new sites inherit the plain member call's resolution, wrong binds included: FD->hasAttr<PackedAttr>() in ASTContext::getDeclAlign binds Type::hasAttr(attr::Kind) at lib/AST/Type.cpp:2026 with no amb=, and in the reviewer's reduced repro the plain FD->plainAttr() binds Type::plainAttr(int) on f8e6087. Every number above re-measured in this lane, not copied from the review. Wording that goes stale in train 2: #248 makes Rule 2 read parameter types, so "Rule 2 reads no parameter type" is dropped from the CHANGELOG, and the gate/fixture comments now date it ("measured on main f8e6087 ... #248 changes that"). Gates: cppqualcheck 96/96 plain and ALL PASS under asan/ripwire; pre-fix binary 77 PASS / 19 FAIL (the same 19 as before). callformcheck, gateexitcheck, manifestcheck ALL PASS; gatecount_build --check (618) and limits_build --check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Disposition of the independent review of
Checks at
No CodeRabbit re-review requested: this push is tests and docs. |
|
This PR is part of integration train 2b, #275, and will show as merged when that train lands. |
…ntity with the template arguments in its scope
`template <class T> void Box<T>::grow() {}` minted `sc="Box<T>"` beside the in-class declaration's
`sc="Box"`, so one member was two identities: --callers=Box::grow resolved to the declaration and
answered count="0" while `use( Box<int>& b ) { b.grow(); }` sat in plain sight, with --impact and
--uses the same. qualifierOf and enclosingScopeOf read the scope node's TEXT, so a multi-line list put
its line break into the census id, and `Slot<std::string>` (a `::` inside the list) was cut by
immediateScope to `string>`. The reference side had the twin at two segments: `Factory<int>::make()`
qualified as `Factory<int>`, keyed nothing, and split onto an unrelated `Decoy::make`.
Fix: a scope tree-sitter hands over as a `template_type` keeps only its `name:` child
(cppScopeSegmentText), in the qualified declarator, a class specialization's name, and each link of a
qualified class name (cppScopeNameText, which returns the written text unchanged when no link is a
template). Structural, so nothing inside the list can unbalance it.
Decision: a specialization's member keys the PRIMARY template's member (`template<> void
Box<int>::grow()` is one more definition of Box::grow, joined like an overload). The resolver does no
template-argument deduction, so no call site can reach a `Box<int>` identity; the argument spelling is
not canonical; Rust `impl<T> Foo<T>` and the C++ 3-segment ref re-split already strip. The full
argument is in the gate header.
kParserVer 96 -> 100 with kIngestParserVerMirror (97..99 are held by open lanes redhat-et#248/redhat-et#244, redhat-et#235/redhat-et#233,
redhat-et#243); kCacheVersion stays 22; qschemetrip re-pinned with a log entry. kMaxQualifierHops hoisted so the
two chain walkers share one cap (LIMITS.md unchanged at 212).
Measured (--pin-census, same corpus both binaries): dgl f0b7cc9, 343 C/C++/CUDA files — scope ids with
an argument list 156 -> 0; 77 of 32,628 decided sites change: 48 corrected targets + 1 wrong split
gone, 22 same id now split (primary + specialization both define the member: the join's cost,
disclosed as amb=), 6 mechanism label only; header edges 20,829 -> 20,745, ambiguous 1,891 -> 1,899.
This repo: 4 symbols move (dynamic_map.hpp node_rank specializations), no edge changes.
Gate: test/cpptmplscopecheck.sh (new, registered in regression.sh, shard weights, gate count 619).
Red on b1489df: 27 of 36 FAIL (the 9 passes are the control, presence guards, determinism and two
by-construction arms). Green: ALL PASS. Pin moved: test/stdqualcheck.sh §3 `hash<Mine>` -> `hash`
(fixture comment updated in place, line count unchanged).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ation/train-2b Conflicts and resolutions (rerere disabled): - src/graph.h (S6-C tie-break): redhat-et#254 capped the shared credit (`std::min( sharedLocality(...), localityCap )`) and redhat-et#256 replaced sharedLocality with localityRank (doubled share + own-scope bonus). Combined: localityRank now takes the cap as `shareCap` and applies it before doubling. The cap binds only an untyped NamedVar receiver, the bonus only an unqualified bare or `this->` call, so no call meets both. - src/resolve.h: adjacent insertions — train 2's receiver/std helpers (namesStdType … buildScopedRecvDecls) kept first, then redhat-et#256's localityRank and appendTemplateFamilyKey. - src/ingest_sidecap.h + src/ingest_names.h, the redhat-et#243 x redhat-et#256 recipe (ffa17084/e24f6c51): redhat-et#243's deletion of the H4 re-split block kept; inside redhat-et#243's cppResplitRefName both qualifier lines use cppRefQualifierText; cppResplitRefName moved below cppRefQualifierText; cppRefQualifierText steps over skipTemplateDisambiguator on its last segment (cppqualcheck §12 (d)'s tqCallRebind arm needs it). - src/ingest_cache.h / src/quality.h: kParserVer and its mirror -> 102 (the PR declared 100; after redhat-et#243's 101). - test/qschemetripcheck.sh: redhat-et#256's RE-PIN LOG entry renumbered to 102, above redhat-et#243's. test/qschemetrip.hash: placeholder kept, re-derived once on the final tree. - test/regression.sh: loop is the union (cpptmplscopecheck + crashsweepcheck); docs/gatecount_build.py wrote 622. - .github/pargates-shard-weights.json: union (cpptmplscopecheck.sh 4.4, crashsweepcheck.sh 15.0). - CHANGELOG.md: main's side kept at the conflict; redhat-et#256's entry placed after redhat-et#243's, heading renumbered to 102. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… no lane hashed the merged declaration lines kParserVer and its mirror are 102 after the train's merge-order assignment (redhat-et#244 100, redhat-et#243 101, redhat-et#256 102); each lane's pin hashed its own number over its own base. One RE-PIN LOG entry for the train; kCacheVersion 22 and kQSnapCacheScheme 12 unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t#244's Lit* helpers and the gate-local runner idiom --quality-delta=$(git merge-base origin/main HEAD)..HEAD on the train reported gating=10: - 8 rows on PR redhat-et#244's model.h helpers isJsTsLitRecv / jsLitCtorName / isJsTsBuiltinCtor, which match short enum switches and OR-of-equality predicates elsewhere by shape, not by shared logic; - 2 new-clone-of-reused-helper rows on the gate-local run()/cnt()/defs() idiom, whose count moved 3 -> 5 because redhat-et#243's and redhat-et#256's gates now share one tree. redhat-et#256's own ack at 3 is healed to 5. Acked through the binary (--quality-ack --ack-only), ledger +10/-1; the range then reads gating=0 acked=13. Minor, non-gating rows remain: buildGraph cx 780 -> 794, receiverOf cx 4 -> 12, collectFieldUseSites LOC 183 -> 189, captureSpecializationHeader params=8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… reduced CI, redhat-et#270 std member uses) into integration/train-2b Conflicts and resolutions (rerere disabled): - src/resolve.h: the field lane (a23f4ec) split recordFlatRecvType into recordFlatRecvTypeFact + a thin Binding wrapper; redhat-et#270 reworded recordFlatRecvType's comment because collectFieldUseSites now calls it too. The field lane's split is kept and redhat-et#270's wording moves onto the wrapper, which redhat-et#270's call site (graph.h collectFieldUseSites) still uses. - CHANGELOG.md: redhat-et#270's entry placed with main's entries, before this train's (redhat-et#243, redhat-et#256, field lane). Shared products re-derived on the merged tree, neither side taken: kParserVer/mirror stay 102 (redhat-et#270 does not bump); test/qschemetrip.hash 1b26f3c0 re-checks ALL PASS (no manifest function or declaration line changed); the gate loop still names 622 (redhat-et#269 adds no gate script); docs/LIMITS.md matches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…'s head) At the coordinator's direction 1b now sits on train 2b (redhat-et#243, redhat-et#244, redhat-et#256, field-final-segment, plus 2b's own reconciles). When redhat-et#275 merges, 1b's diff against main shrinks to its own delta. Main 13a1916 was already in 1b. Conflicts, all shared products: - test/regression.sh: the loop is the union. - README.md, docs/EVALS.md, deck5: gate-count markers only (630 vs 625). Main's side is kept here and the count is regenerated in the next commit. Everything that merged clean without a textual conflict is re-derived on the merged build in the commits that follow, not trusted: the ack ledger, qschemetrip (2b 31d4ca8d), kParserVer (102), cppqualcheck's readWholeFile, hazardpatterncheck's registry, printf_parity, LIMITS/TUNING and shard weights. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Defect
A C++ member call whose callee carries explicit template arguments minted no call reference at all:
r.get<K>( 1 ),p->get<K>( 1 ),x.template get<K>(),this->template get<K>(). The callee parses asfield_expression field: (template_method …)— inside adependent_namebehind thetemplatekeyword — and no C++reference pattern bound either shape. The drop happened at extraction, before
ambiguous=/unresolved=could countit.
Measured 2026-09-16 on main
3bf884e2, a four-line repro:--callers=get --no-cache→count="0";--callers=plain→count="1". Every verb reading the call graphunder-counted these calls (
--callers,--uses,--impact,--safe-delete), and--quality-deltacould call amethod reached only this way
kind="dead-code".Two more wrong answers in the same family, on the qualified dependent form (which did extract):
X::template make<int>()minted a reference namedtemplate make, which resolves to nothing.X::template Rebind<int>::f()keyed its qualifier astemplate Rebind, so the canonical tier missed and the callsplit over a same-named definition in another scope.
Free
f<T>( x )and qualifiedns::f<T>( x )(thestd::get<0>( t )shape) were already bound; the gate keeps themas controls.
Change
queries/cpp/tags.scm— one alternation pattern bindsfield: (template_method name: (field_identifier) @name)andfield: (dependent_name (template_method …)). One node matches one branch, so each call is minted once.src/ingest_binds.h—receiverOfreadscalleeAccessParent, which steps overtemplate_methodanddependent_nameto thefield_expression. Without it the new references classified as BARE calls, and theenclosing-class rule bound
other.pick<int>()to the caller's own same-named method.src/ingest_names.h—skipTemplateDisambiguator(the keyword plus whitespace/comments), applied to the qualifierinside
immediateScopeand to the name incppResplitRefName.cppResplitRefNameis the H4 re-split block movedout of
captureTagsFacts(its complexity is measured); every other spelling behaves as before.kParserVer96 → 99 on this branch, withkIngestParserVerMirrorin the same commit.kCacheVersionstays 22;test/qschemetrip.hashre-pinned viaUPDATE_GOLDEN=1with a RE-PIN LOG line;kQSnapCacheSchemestays 12.The landing number is assigned by integration train 2 (coordinator: 101, after fix(resolve): a member call through a typed parameter was pinned to the caller's own class #248 97, fix(resolve): an explicit receiver of unknown type was pinned to the caller's own class, and a std:: type narrowed to an in-repo namesake #254 98, fix(resolve): a member field typed std::string narrowed to an in-repo class named string #257 99, fix(ingest): a class template's out-of-line member keyed a second identity with the template arguments in its scope #256 100);
the branch keeps 99 per the train protocol.
Measured
Pre-fix vs fixed binary on this repo's
src/(169 files,--no-cache, map header +ripwire_probereference count):f8e6087c)The six new references are exactly the
r.pod<T>()reads insrc/gitoracle.h'sloadOracleCache:--uses=pod1 → 7, and--callers=podnow namesloadOracleCachebesideReader::str.On a template-heavy tree the effect is three orders of magnitude larger. Same binaries (fixed =
built_from=1171f7757),llvm-project
4d5358b1d,clang/include+clang/lib, 2,515 files:f8e6087c)--callers=getAs48 → 492,--callers=hasAttr45 → 377. Onclang/lib/AST(155 files) the reference delta(246,367 → 248,394, +2,027) equals the new shape's
--matchhit count exactly (2,027,hits_capped="0"), so extractionmoved only the intended class.
The new sites inherit the plain member call's resolution, wrong binds included.
FD->hasAttr<PackedAttr>()inASTContext::getDeclAlign(ASTContext.cpp:1878) bindsType::hasAttr(attr::Kind)atlib/AST/Type.cpp:2026, notDecl::hasAttr<T>()(DeclBase.h:585), with noamb=. In a reduced five-file repro the plainFD->plainAttr()bindsType::plainAttr(int)the same way onf8e6087c. A resolver defect this change widens the reach of; not fixed here.Gates
test/cppqualcheck.sh§12 — new corpustest/cppqualtmplfix/member.cpp. It has one hand-read literal perspelling: member dot and arrow,
x.template,p->template,this->template, nested>>arguments, a chainedreceiver,
X::template,X::Y::template, and a comment after the keyword. It also has four controls the oldpatterns already bound, receiver/arity/qualifier decoys, the fixture header (
symbols=43 edges=19 ambiguous=0 unresolved=0), and a repo-root arm (--callers=podnamesloadOracleCache).Arm (g), added from the review (F4): the spellings still not bound, each a literal zero behind a source-presence
guard —
r.f<0>( x )(two comparisons; exactly onerole="read"row),r.Base::f<T>()andp->Base::f<T>()(theplain
r.Base::f()is pinned 0 too: the qualified-field family), andr.operator()<T>(). These pass on both binaries byconstruction: fences for a future widening.
Red first:
pre-fix binary:
77 PASS / 19 FAIL. The 19 are every new check except the four controls, the determinism arm and(g)'s six absence pins. Header
edges=6 ambiguous=1.mutation build, the same tree with three one-line reversions (
8 FAIL), each mechanism reddening its own arms:receiverOfreading the raw parent →tqDecoyCallerbinds the decoy:110.skipTemplateDisambiguatorinert → the three dependent--usesread 0, the probe name arm fails, andtqCallRebindsplits onto:133.callArity's hop bound 4 → 3 →tqDepArityCallersplits over both overloads.Reverted: the diff is byte-identical to before and the rebuilt binary is byte-identical to the fixed one.
fixed:
96/96plain; ALL PASS withasan/ripwire.test/callformcheck.shrow 11 —b.template memberTmpl<int>()was pinned documented-absent (literal 0 +probeBlind); it now pins 1 +probeSees. Red on the pre-fix binary: 2 FAIL; green on the fix.Also run:
qextractionkeycheckALL PASS,qschemetripcheckALL PASS after the re-pin,printffmtparitycheckALL PASS(42 verbs),
manifestcheck,chainguardcheck,cudacheckALL PASS;docs/gatecount_build.py --check(618,unchanged — no new gate script) and
docs/limits_build.py --checkclean;--edit-checkonreceiverOf,immediateScopeandcaptureTagsFacts:status="unchanged" incompatible="0".G1:
LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./asan/ripwireovertest/cppqualtmplfix,test/callformfix/cpp,a scratch deep-dependent corpus, and
src/, plusasan/ripwire_probeon the fixture: rc 0, zero sanitizer lines.--quality-delta=$(git merge-base origin/main HEAD)..HEAD:regressions="0" gating="0"(api-new-surface="3"—the three new helpers).
Full suite, one run each, nothing else in flight,
python3 test/pargates.py . ./build/ripwire -j 6:1171f775:gates=632 pass=630 skip=2 fail=0 wall=800.9s6b234717:gates=632 pass=630 skip=2 fail=0 wall=837.8s(queued 2,520 s behind two peer suites)The two skips are the environmental
argvdiffcheckandeditchecknotecheck(asan/is present and fresh, sog1freshcheckran and passed).Not fixed here
specifically is fix(resolve): a member call through a typed parameter was pinned to the caller's own class #248's (train 2, ahead of this PR), so the fixture keeps a LOCAL receiver for the climb arm.
w.m::<T>()has the same extraction drop (review F3) — a follow-up lane, not this PR.a.b < c > (d)now mints a call namedb(F5);X::template operator()<T>()still binds nothing (F6). Both rare.Pins moved
test/qschemetrip.hash—24fd4dd9…→01ea4c63…(kParserVer line; RE-PIN LOG entry intest/qschemetripcheck.sh)kParserVer/kIngestParserVerMirror96 → 99 on the branch; train 2 assigns the landing number (101)test/cppqualcheck.sh§12 fixture header literalsymbols=43→54(commit6b234717, the §12e block; edges stay 19)test/callformcheck.shrow 11: literal 0 → 1🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
templatedisambiguator.Tests