Repository navigation
fix(graph): an internal-linkage definition no longer stands for another file's declaration (parser version 96) - #216
Conversation
… internal-linkage definition in an including TU CodeRabbit's post-merge finding on redhat-et#139 (src/graph.h, outside the diff), confirmed by fixture: the decl-to-def widening proves a candidate by FILE (its TU #includes the declaring header), so a TU that includes api.h for its own reasons and defines an unrelated `helper` in an anonymous namespace, or as a namespace-scope `static`, is served as the header's implementation. The shape that compiles is an overload — helper(double) / helper(long) beside the declared helper(int) — and by name both were kept. Arm (B) covered TUs that do NOT include the header; this is the gap it left. On main's binary: FAIL (B2) api.h:helper serves localUse statUse — an anonymous-namespace / static helper in a TU that includes api.h (internal linkage, gathered by name) Expected: count="1" naming useApi, equal to api.cpp:helper's answer, with the two dropped definitions DISCLOSED as unproven_defs="2". Controls: the bare name unions all three; the .cpp-qualified answer is unchanged. Claude-Session: https://claude.ai/code/session_01Myz7oQxAjzctp9b2uSr6Pi
…er file's declaration (parser version 96)
Every C and C++ definition now carries a syntactic `internalLinkage` bit (ingest_names.h::
cppInternalLinkage): inside an anonymous `namespace { }` at any depth, or carrying a namespace-scope
`static`; a class-scope `static` member has external linkage and is not marked. It rides RawDef and the
per-file cache record as one u8 after `recovered` (kMinDefRecordBytesLean 78 -> 79) and Symbol as a
bit-field sharing testScope's byte, so sizeof( Symbol ) and its static_assert are unchanged.
The widening's keep loop — keepProvenCandidates, split out of declToDefFollowThrough so the per-symbol
test does not push it over the complexity bar (the same reason H1 split its scan out) — keeps an
internal-linkage candidate only for a declaration in its own file, and COUNTS a rejected one in
unproven_defs= like any other drop: the reader still learns that same-named definitions exist which
no row covers, and the bare-name selector still shows them, as the legend says. Clause 3 of the H1
note. Placing the test in the gather instead would have silenced the residue the (E2) arms assert.
Measured: fixture api.h:helper count="3" -> count="1" with unproven_defs="2". This repo at 1cf3086
(frozen worktree): default map byte-identical, 0 of 11 header-qualified --callers selectors over
src/ingest.h's declarations moved. decltodefcheck 226/226; decltodefcheck, declinedlistcheck,
cachefuzzcheck, cachehashcheck also under ASan/UBSan; determinism x2; xmllint clean.
kParserVer 95 -> 96 and kCacheVersion 21 -> 22 with quality.h's two mirrors in the same commit;
test/qschemetrip.hash re-pinned with its log entry. --quality-delta flags short-horizon-churn
churn="self" on declToDefFollowThrough — the function the fix lives in, touched by redhat-et#63, H1 and now
this; disclosed, not acked.
Claude-Session: https://claude.ai/code/session_01Myz7oQxAjzctp9b2uSr6Pi
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (11)
Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughThe change records C/C++ internal linkage on definitions, persists the flag in caches, and prevents header-qualified declaration-to-definition matching from selecting unrelated internal-linkage definitions. Regression tests cover anonymous-namespace and static overloads. ChangesInternal linkage declaration-to-definition matching
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Low Sequence Diagram(s)sequenceDiagram
participant Query
participant declToDefFollowThrough
participant Symbol
participant Result
Query->>declToDefFollowThrough: submit declaration-qualified query
declToDefFollowThrough->>Symbol: read internalLinkage
declToDefFollowThrough->>Result: retain valid definitions and count discarded ones
Result-->>Query: callers and unproven_defs
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The change prevents declarations from resolving to unrelated internal-linkage definitions while retaining valid same-file matches. No unresolved merge-blocking risk is identified. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 9 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
|
Full battery, as promised. Both reds are the runner's, not the change's, and each is green rerun alone on the same head:
So 626 of 626 on this head. Everything in the description stands: |
|
Thank you, @andriytyurnikov — this is exemplary work, and the part I most want to call out is the correction you made to the finding itself. The automated review described a same-signature anonymous The red-first discipline is exactly right: arm (B2) lands in its own commit, and I reproduced it — on main's binary it is the only failure of 226 arms, with your exact message, and 227/227 pass on yours. I also like that you let the existing (E2) residue arms decide where the linkage test goes. Putting it in the gather would have silenced the disclosure; putting it in the proof loop keeps a rejected candidate counted in Two things worth noting from review. Your fix reaches further than the PR claims: on the B2 fixture Two small things before this lands. Once CI is green this goes in for 0.6.1, and you'll be credited in the CHANGELOG and the release notes. |
|
Thanks for this — the fix is the right shape and the gate arm is the part I'd have asked for. A note on why it looks stalled, because the reason is not your code. No CI has run on this branch, and none will until the conflict is resolved. GitHub creates no workflow run at all for a conflicting pull request — not a failed run, no run — so the checks list is empty rather than red. That is also why nothing here is waiting on a maintainer approval. The conflict is one file and it is trivial. Your branch is 2 ahead and 32 behind A plain Two things I confirmed while looking, in case they save you a round: the |
…th 1-bit Symbol fields from any non-zero byte Review on redhat-et#216: the new RawDef field had landed between `recovered` and the two comment lines that continue its explanation, so those lines read as if they documented `internalLinkage`. Moved below. And the squeeze from RawDef's u8 into Symbol's 1-bit fields is where a cached byte gets truncated — a blob carrying 2 would read as 0, which for testScope silently drops its old "any non-zero means the convention encloses it" reading. Coerce `!= 0` to 1 at that assignment for both fields, so a corrupt or hand-edited record errs toward the claim rather than past it. Extraction writes 0/1 either way. Claude-Session: https://claude.ai/code/session_01Myz7oQxAjzctp9b2uSr6Pi
…h sides kept, main's --expand pricing section first, the internal-linkage section after it Only CHANGELOG.md conflicted: both sides insert a new section directly under [Unreleased]. Every other file this branch touches is untouched by main in the range, and parser version 96 / cache version 22 are still the next free numbers on main (kParserVer 95, kCacheVersion 21 there). A plain merge, no rebase. Claude-Session: https://claude.ai/code/session_01Myz7oQxAjzctp9b2uSr6Pi
|
Thanks for the merge — that was exactly what was needed. CI is running now (it did not need a maintainer approval this time), so those commits are getting their first real run across all six legs. The review I owe youI read this properly earlier and it holds up. The things I checked specifically: The fix is shaped the way the finding deserves. And the disclosure is the part I liked most. A rejected candidate is counted into The gate arm is not vacuous, which is the thing I check hardest. It asserts controls first — the bare selector unioning all three translation units, then the Pins moved in lockstep in one commit, which is the house rule: parser 95→96, cache 21→22, both mirrors, the lean-record byte minimum, and the trip hash re-pinned with a dated log entry. I verified against main that 96 and 22 are still the next free numbers. The one thing I wanted an actual build to confirm is the On timing, and I want to be straight with you rather than vagueThis lands after the 0.6.1 tag, not in it. Two specific reasons, neither about the quality of the work:
Your merge was not wasted work: it got you out of the conflicted state, it got you CI, and it means you are ready to land first rather than last. You will be credited in the changelog when it does. If CI comes back red on something real, I will bring you the actual log text rather than a guess — a shard failed on an unrelated branch earlier today and the log named a known flake on a leg the documentation does not mention, so I would rather read it than infer it. |
|
All three taken, on the branch as two commits — no rebase.
On the merged head Thank you for the |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
Merged — thank you. This closes the last of the decl→def family: #173 stopped a header selector answering with bodies it could not tie to that header, #190 made the focus verbs say what they dropped, and this one stops an internal-linkage definition standing for another file's declaration. That was a wrong answer on every |
Five conflicts, each resolved by its own rule, and FOUR of the five were files where neither side's content is correct for the merged tree. test/regression.sh — UNIONED. Both sides carried 616 and the union is 617: ours adds situshapecheck, main's adds #212's recentscopecheck. Taking either side gives a consistent COUNT over the wrong member set. docs/gatecount_build.py — RAN, not hand-written: wrote 617 across 3 FILES CHANGED (README.md, docs/EVALS.md, present/deck5_ripwire_build.js). Both sides had rewritten those to an identical "616", so git merged them CLEAN while the loop went to 617. That is the published-count skew, and only running the generator finds it. docs/LIMITS.md — REGENERATED, and the number is neither side's. Main's file says "114 of 211 caps", ours said "113 of 211"; the merged src/ holds 212, because this lane adds one cap of its own (kSituSiblingRowsShown = 8, the [1] sibling row cap). --check then passes at 212 caps / 114 classified. docs/TUNING.md — took MAIN's side, which is the newer census prose, then verified it against the regenerated LIMITS population rather than trusting the text. docs/COMMANDS.md — REGENERATED (--bin ./build/ripwire; it refuses without one when build/ holds two executables): 176 flags in 7 sections, and --check reports "documented set == binary set". test/printf_parity.manifest — RE-RECORDED through the gate's own UPDATE_GOLDEN flow, never by picking a side, and for the second time in this queue the answer was neither side's: pack_task d8b0cd1c -> 11bcf9c2 and help_all 2de8db3c -> eac1e590, moved={pack_task help_all}, 40 unchanged. src/cli.h — a real code merge, verified by READING both sides rather than by the clean text merge. The two edits are disjoint: ours rewords the run= help text ("Spelled RELATIVE to the root= the document declares … a MULTI-ROOT run declares no single root, so it stays absolute"), main's adds #212's whole --in=DIR block plus its inDir field. Both are present in the result, and the retired wording ("Spelled with the same root you scanned") is gone. .ripwire_quality_acks — population CHECKED rather than assumed. Rows: base 1285, ours 1297, main 1296, merged 1308, which is exactly base + 12 + 11, so the text merge produced a clean union with nothing lost or doubled. Duplicate-key shape is pre-existing and additive (base 239, each side +1, merged 241). This lane's own A3 ack ebaa8de8 appears exactly once. CHANGELOG.md — kept BOTH sides; 27 disjoint entries under [Unreleased]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version moves in the one place that holds it — `project( ripwire VERSION … )` — which `src/version.h` is generated from and `test/versioncheck.sh` pins against the binary's own `--version`. The ~20 other files that mention 0.6.0 are historical claims about when a requirement started, gate fixture data, regenerated captures or source comments, and bumping them would corrupt three different things. The CHANGELOG's 0.6.1 section is the curated release narrative for everything since the v0.6.0 tag, with the per-lane entries that accumulated under [Unreleased] folded in: the ones the narrative already covers are superseded by it, and the ten it did not cover are carried, condensed to a paragraph each so the extracted release notes stay inside GitHub's body limit. [Unreleased] is left as an empty stub. Resolved rather than shipped as markers: the pending redhat-et#216 note is now the parser-version-96 upgrade note plus that PR's own entry, credited to its author, since the owner pulled it into this release; redhat-et#173's known over-retention sentence goes, because redhat-et#216 fixed it, and the 0.6.2 plan says what is left of it. The provisional loop figures are cut rather than published, and the sentence that promised them no longer does. Two shapes the draft stated from an earlier tree are corrected against the binary: the `--in=` map stub is `<symbols stubbed="1" would_show=N next=/>`, not `total=`/`shown="0"`, and it measures 68 B, not 61. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeRabbit's post-merge finding on #139 (
src/graph.h, outside the diff): the decl-to-def widening behind everyfile:nameselector retains a definition on FILE evidence alone, so a translation unit that#includesapi.hfor its own reasons and defines an unrelatedhelperin an anonymous namespace is served as the header's implementation. Confirmed by reading and by a fixture. One correction to the finding's framing: a same-signature anonymoushelperin an including TU makes every unqualified call ambiguous, so the shape that compiles is an overload —helper(double)in the anonymous namespace, or a namespace-scopestatic helper(long), beside the declaredhelper(int). Both were gathered by name and both were wrong.The rule
Internal linkage makes a definition visible to its own translation unit alone, so no other file's declaration can stand for it. Symbol carried no linkage information at all, so the fix needs an extraction bit.
Change
internalLinkagebit (ingest_names.h::cppInternalLinkage): inside an anonymousnamespace { }at any depth, or carrying a namespace-scopestatic. A class-scopestaticmember has external linkage and is not marked. RidesRawDefand the per-file cache record as one u8 afterrecovered(kMinDefRecordBytesLean78 → 79);Symboltakes it as a bit-field sharingtestScope's byte, sosizeof( Symbol )and its static_assert are unchanged.keepProvenCandidates, split out ofdeclToDefFollowThroughso the per-symbol test does not push it over the complexity bar, the same reason H1 split its scan out) keeps an internal-linkage candidate only for a declaration in its own file. A rejected one is COUNTED inunproven_defs=like any other drop: the reader still learns that same-named definitions exist which no row covers, and the bare-name selector still shows them, as the legend already says. This is clause 3 of the H1 note.kParserVer95 → 96 andkCacheVersion21 → 22 withquality.h's two mirrors in the same commit;test/qschemetrip.hashre-pinned with its log entry. Old caches are rejected and rebuilt through the existing degrade path.Gate, red first
test/decltodefcheck.sharm (B2): a header, its defining.cpp, one real caller, and two including TUs with an anonymous-namespace overload and astaticoverload. On main's binary:With the fix:
Arm (B) already covered TUs that do NOT include the header; (B2) is the gap it left. The existing (E2) residue arms, which expect the two anonymous helpers of arm (B)'s corpus to be disclosed as dropped, are what decided that the linkage test lives in the proof loop rather than the gather: 226 of 226 arms pass.
Measurements
--callers=api.h:helpercount="3"(api.cpp:helpersaidcount="1")count="1"naming useApi,unproven_defs="2"1cf3086e(frozen worktree), default map--callersselectors oversrc/ingest.h's declarationsThe tree has no colliding internal-linkage overload, so the own-tree numbers are a no-movement control, not a demonstration. No larger C++ corpus is on this machine (the local bitcoin checkout is empty); a second number on llvm would be welcome.
Verification
decltodefcheck226/226,declinedlistcheck30/30,cachefuzzcheck104/104,cachehashcheck7/7 — each also under ASan/UBSan (-fno-sanitize-recover=all), plus raw ASan runs of the default map, the fixture answer, and a cold+warm cache round trip: zero bytes on stderr.qschemetripcheck,qextractionkeycheck,cacheidentitycheck,gateexitcheck,manifestcheck,testscopecheckgreen.xmllint --noouton the fixture and this tree.test/pargates.py -j 3, 612 gates): running on my machine as this opens; verdict to follow in this thread.Disclosed
--quality-deltaexits 2 with one gating row:short-horizon-churnondeclToDefFollowThrough,churn="self". The function was written by #63 (09-08), rewritten by H1 (09-11), and this is its third touch in a week — the finding is about exactly the function the fix has to live in. Not acked; the change shrinks the function (the keep loop moved out) rather than growing it.Also on #139's review
CodeRabbit's other outside-diff finding there, the declined-candidate list growing with calls × candidates, is closed by #208 (one interned copy per distinct list, a uint32 ceiling behind
DEGRADED_PATH_ALERT,verifyOffsetCsr, andtest/declinedlistcheck.sh); verified green on main before starting this. Nothing to do here beyond a reply on that thread.