Skip to content

fix(resolve): a member call through a typed parameter was pinned to the caller's own class - #248

Merged
joyful-ii-V-I merged 6 commits into
mainfrom
lane/param-receiver-binding
Sep 17, 2026
Merged

joyful-ii-V-I merged 6 commits into
mainfrom
lane/param-receiver-binding

Conversation

@joyful-ii-V-I

@joyful-ii-V-I joyful-ii-V-I commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

The defect

On main (f8e6087c), a C++ member call through a function parameter was bound to the enclosing class's same-named method instead of the parameter type's method. The result was one precise edge, with no amb= and nothing disclosed:

struct Target { int pick( int n ) { return n; } };
struct Decoy
{
    int pick( int n ) { return n; }
    int plainCaller( Target& other ) { return other.pick( 1 ); }   // --callees: Decoy::pick (c.cpp:4) — wrong
    int localCaller() { Target other; return other.pick( 1 ); }    // --callees: Target::pick (c.cpp:1) — right
};

Cause. Rule 2 narrowed a receiver only through LocalBindKind::Type (typed locals). A parameter's written type (ParamType) was captured, but only collectFieldUseSites read it. The call fell through to the name ladder, where the S6-C locality tie-break gives the caller's own class the scope credit. --pin-census shows the site as mech=locality, 2 candidates cut to 1.

The change

Rule 2, and CHA-lite through the same lookup (Narrower::recvVarTypeName), now reads ParamType records lexically. For every name with a ParamType record, buildScopedRecvDecls (in resolve.h, beside the table type and its reader) lists all of that name's declarations in the definition: each VarDecl with its scope span. It then attaches every Type/ParamType record to the declaration whose VarDecl shares its record position (Binding::startByte, new; it sits in the padding after kind, so sizeof is unchanged and pinned). At the call site, the innermost covering declaration decides. An untyped declaration, a tie, or no covering declaration answers nothing. Names with no ParamType record keep the flat varType table and resolve byte-identically.

I measured two limits before choosing them:

  • The obvious fold into the flat per-function table is unsafe. It fixed the defect but minted three new precise wrong edges on the gate fixture (arms 12–14):

    • a range-for variable's type reaching a later auto loop of the same name
    • a same-named field read after the loop
    • a parameter hidden by an untyped loop variable

    The flat table's tombstone also left arm 10 on the locality pin.

  • A qualified written type does not narrow. A written type is recorded as its final segment, and class names carry no namespace. So const std::map<K, V>& ref; ref.lower_bound( q ) narrowed to an unrelated in-repo map. That gave three such edges on a private C++/ObjC++ corpus (129,759 call sites).

    • The qualified text now rides the declaration's Type/ParamType RawBind (importedName, see qualifiedNameText), and the lexical lookup refuses it.
    • That removes all three wrong edges and gives up 11 correct narrows through qualified in-repo types. Those 11 sites keep main's answer.
    • I tried and rejected an include-visibility guard first. Path-precise includes miss include-root spellings ("LinearMath/btVector3.h"), so it refused about 150 correct narrows on that corpus to stop the same three.

kParserVer 96 → 97, with the quality.h mirror. The record format is unchanged, but its content is not: a warm 96 blob holds "" there and would narrow a qualified parameter type.

Measured

Method: --pin-census --no-cache, the main binary (f8e6087c) against this branch.

corpus target changed label-only (same target, now Rule 2)
private C++/ObjC++, 129,759 call sites 587 956
this repo src/ 53 (all split → one Rule-2 pin) 158

Private corpus breakdown:

  • 373 splits narrow: 300 via Rule 2, 73 via the CHA cone.
  • 147 formerly declined calls gain an edge: bound= 80,432 → 80,583, declined= 17,552 → 17,401.
  • 66 pins or splits that didn't contain the parameter's type move to it. 40 of these were unique pins to the one same-file method of the wrong class, e.g. body->setGravity() inside addRigidBody( btRigidBody* body ).
  • 1 edge is lost: a friend function ripwire scopes inside its class, which the parameter's type then names as the caller itself.

I sampled every category and read it against the source. Wall time is unchanged within noise: three cold runs of each binary gave 1.66–2.51 s at load ~12. The output is byte-identical between the dev build and the ASan build.

Gates

test/narrowcheck.sh arms 7–18 run on a generated fixture. Red/green evidence:

binary result
main 9 FAIL — (7) (8) (10)×2 (11) (12) (13) (14) (15 census mech=locality)
naive flat-table fold 5 FAIL — (10)×2 (12) (13) (14)
lexical lookup without the qualifier guard (17) FAIL
this branch 23 PASS, including under ASan

Five gates had controls built on "a parameter has no binding". They now use an untyped auto receiver:

  • narrowcheck: control/param.cpp becomes control/untyped.cpp.
  • chacheck
  • chaconecheck g5
  • localitycheck: its call no longer reached the tie-break the gate exists to test.
  • resolverhonestycheck F9: its check_signal row had gone vacuous on a single edge.

fieldnarrowcheck (h) moves from 7 to 6 because shadowParam( Decoy& m_x ) now resolves to the parameter's type. Its (s1) arm now also asserts that Pool::acquire is not linked; that assertion is red on main.

Targeted runs on the final binary, 37 gates, ALL PASS:

  • resolver: narrowcheck, chacheck, chaconecheck, localitycheck, chainguardcheck, fieldnarrowcheck, clsrecvcheck, fieldusescheck, fnptrcheck, shadowcheck, pincensuscheck, aritycheck, resolvecheck, resolverhonestycheck, narrowlangcheck, importnarrowcheck, externalvetocheck, qualifiedresolvecheck, objcfieldcheck, usescheck, callerscheck, selectorchaincheck, chainidcheck, cppqualcheck
  • cache / parser version: cacheidentitycheck, qextractionkeycheck, cachesplitcheck, localscountcheck, moduleconstcheck, metalcheck, cudacheck
  • fixture consumers and registry: compactlegendcheck, recallevalcheck, xmlwellformed, manifestcheck, gatecountcheck, limitstablecheck

Checks:

  • Full suite Linux portability: compile, runtime, sanitizer, leak, and gate-semantics fixes from first real CI contact #1 (at the ack commit): gates=632 pass=627 skip=2 fail=3. All three failures came from this lane, and commit 5 fixes each:
    • showcasecapturecheck (H): lines added to graph.h shifted the zero-slack published seed. The builder moved to resolve.h instead of re-recording the release capture.
    • qschemetripcheck: the kParserVer re-pin case.
    • gateexitcheck G2: two one-line && ok || no verdicts, now if/else.
  • Full suite docs: LINEAGE.md — the row-by-row ledger of where the ideas come from #2 (at the final commit eaadb9d0): gates=632 pass=630 skip=2 fail=0 wall=893.3s jobs=6 tree_writes=0 — one clean run, no timeouts. The two skips are the environmental argvdiffcheck and editchecknotecheck; g1freshcheck ran against a fresh asan/ and passed.
  • Generators: docs/gatecount_build.py --check and docs/limits_build.py --check both clean (618 gates; 212 caps).
  • quality-delta, range form $(git merge-base origin/main HEAD)..HEAD: gating="0".
    • It first reported gating="3": a ctorTypeOf|nodeTextOf clone, and Narrower's constructor at 6 parameters. I fixed both in code; 1508bd10 restructures and is output-identical by cmp.
    • The two rows left are the Narrower constructor's intended 4 → 5 contract change, acked through the binary (--ack-only=api-surface, exactly two +ack rows, one per symbol).
  • ASan/UBSan (-fno-sanitize-recover=all): zero reports on the gate, this repo, a census run, and the private corpus.

Out of scope, stated

  • An abstract parameter type narrows onto its namesakes (disclosed in CHANGELOG by 186fcb84).
    • Rule 2 resolves against definitions only. A parameter typed as an interface whose methods are pure-virtual declarations can't narrow to it, and lands on unrelated classes sharing the final name segment.
    • Measured on rocksdb @ 0e2801ac3 (--pin-census --no-cache, main f8e6087c against this branch): 79 sites (88 rows) through an Iterator* parameter now split five ways over the nested memtable/ Iterator classes, and none of the five is right.
    • Before: 25 were a unique override pin, 26 a split over overrides, 28 had no edge.
    • Every one is disclosed (amb=, prov="split"), but each is a wrong answer, not a missing one. This is the collision typed locals already have on main, extended to parameters.
  • Member-initializer lists: Decoy( Target& t ) : v( t.pick( 3 ) ) sits outside the parameter's body span and keeps main's answer, which is the locality tie-break's wrong pin.
  • Untyped receivers: auto x = make(); x.m() still reaches the locality tie-break.
  • Typed locals still read the flat table, qualified-type collision included.
  • The reported arity observation (pick(int) + pick(int,int) both kept for a 1-arg typed-local call) is not a receiver-path divergence. B2.2 drops a candidate only when argCount > params (a documented decision about default arguments on a separate prototype), and the bare-name ladder keeps both too.
  • Template member calls (other.pick<int>( 1 )) inherit this once lane/template-call-edges lands.
  • kParserVer collision: if that lane also bumps kParserVer, the second to land re-bumps (never reuse a number).

Pins moved

  • kParserVer 96 → 97 and kIngestParserVerMirror 96 → 97
  • test/qschemetrip.hash re-pinned through UPDATE_GOLDEN=1: its manifest hashes the kParserVer declaration, and the new pin equals the current= hash the suite printed
  • fieldnarrowcheck (h) ambiguous=7ambiguous=6
  • no legend, byte, printf_parity, capture or gate-count pins moved
    • src/graph.h's diff is line-neutral (five in, five out) so the 0.6.1 capture's --at=src/graph.h:3406 seed, the first body line of rankGraphTeleport, still resolves.

Commits

  1. fix(resolve) — the defect, gate arms 7–18, controls moved, kParserVer 97, CHANGELOG
  2. merge of origin/main (a55b118e, ci(release): Intel macOS binaries end with 0.6.1; the installer says so instead of failing on a missing asset #240); CHANGELOG keeps both Unreleased entries
  3. refactor(resolve) — quality-delta's clone, parameter-count and complexity rows fixed in code; output byte-identical by cmp
  4. chore(quality) — the Narrower constructor contract change, acked through the binary
  5. fix(lane) — suite Linux portability: compile, runtime, sanitizer, leak, and gate-semantics fixes from first real CI contact #1's three reds (details below)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed incorrect method resolution for calls made through typed parameters and other scoped receiver variables.
    • Improved handling of nested scopes, lambda parameters, range-based loop variables, and reference locals.
    • Qualified receiver types are no longer incorrectly narrowed to unrelated classes.
    • Updated cached analysis data to ensure results remain accurate after this change.
  • Tests

    • Added coverage for receiver narrowing, lexical shadowing, ambiguity handling, and cache consistency.

joyful-ii-V-I and others added 5 commits September 16, 2026 13:39
…he caller's own class

`int Decoy::plainCaller( Target& other ) { return other.pick( 1 ); }` answered
--callees=plainCaller with ONE edge to Decoy::pick: precise, no amb=, wrong. Rule 2 narrowed
a receiver only through a typed local (LocalBindKind::Type); a parameter's written type
(ParamType) was captured but read only by collectFieldUseSites, so the call fell through to
the name ladder and the S6-C locality tie-break gave the caller's own class the scope credit
(pin census: mech=locality, 2 -> 1).

The fix: Rule 2, and CHA-lite through the same lookup (Narrower::recvVarTypeName), reads
ParamType records LEXICALLY. graph.h buildScopedRecvDecls lists, for every name with a
ParamType record, all its declarations in the definition (each VarDecl with its scope span)
and attaches each Type/ParamType record to the declaration whose VarDecl shares its record
position (Binding::startByte, new, in the padding after `kind`: sizeof unchanged, pinned).
The innermost declaration covering the call site decides; an untyped one, a tie, or no
covering declaration answers nothing. Names with no ParamType record keep the flat varType
table byte-identically.

Why not the obvious fold into varType: measured first. It fixed arms 7/8/11/15 and minted
three NEW precise wrong edges on the gate fixture (arms 12-14: a range-for variable's type
reaching a later `auto` loop of the same name, a same-named field read after the loop, a
parameter hidden by an untyped loop variable), and arm 10's tombstone kept the locality pin.

Qualifier guard: a written type is its final segment and class names carry no namespace, so
`const std::map<K, V>& ref; ref.lower_bound( q )` narrowed to an unrelated in-repo `map` —
three such edges on a private 129,759-call-site C++/ObjC++ corpus. A declaration's qualified
written type now rides its Type/ParamType RawBind (importedName, ingest_binds.h
qualifiedNameText) and the lexical lookup refuses it: removes all three, forgoes 11 correct
narrows through qualified in-repo types (those keep main's answer). kParserVer 96 -> 97 (+
quality.h mirror): the record format is unchanged, its content is not, and a warm 96 blob
would narrow a qualified type. An include-visibility guard was measured first and rejected:
path-precise includes miss include-root spellings and it refused ~150 correct narrows there.

Measured, --pin-census --no-cache, main f8e6087 binary vs this change:
  private corpus: 587 of 129,759 sites change target — 373 splits narrow (300 Rule 2,
    73 CHA cone), 147 declined calls gain an edge (bound 80,432 -> 80,583), 66 pins/splits
    move to the parameter's type, 1 edge lost (a friend function scoped inside its class
    self-loops); 956 more keep their target, now decided by Rule 2. Every category sampled
    and read against the source. Wall time within noise (3 cold runs each, 1.66-2.51 s).
  this repo src/: 53 splits -> one Rule-2 pin; nothing else moves target.

Gate: test/narrowcheck.sh arms 7-18 (generated fixture). RED on main: 10 rows (7, 8, 10 x2,
11, 12, 13, 14, 15). RED on the flat-table fold: 10 x2, 12, 13, 14. RED on the lexical lookup
without the qualifier guard: 17. GREEN on this commit: 23 PASS, and under ASan.
Controls that encoded "a parameter has no binding" moved to an untyped `auto` receiver:
narrowcheck (control/param.cpp -> control/untyped.cpp), chacheck, chaconecheck g5,
localitycheck (its call no longer reached the tie-break it tests), resolverhonestycheck F9
(check_signal had gone vacuous on one edge). fieldnarrowcheck (h) 7 -> 6: shadowParam(
Decoy& m_x ) now resolves to the parameter's type; (s1) also asserts Pool::acquire is not
linked (red on main).

Out of scope, stated: an untyped receiver still reaches the locality tie-break; typed locals
keep the flat table and its qualified-type collision. The reported arity split on a typed
local (pick(int) + pick(int,int) for a 1-arg call) is B2.2's documented argCount > params
rule and reproduces identically on the bare-name ladder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHANGELOG.md: both Unreleased entries kept (#240's Changed, this lane's Fixed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ument constructor and a 30-complexity builder

quality-delta over the merge-base range reported gating=3 on this lane's own rows: ctorTypeOf's
byte-bounds tail cloned nodeTextOf (duplication + new-clone-of-reused-helper), and Narrower's
constructor went 4 -> 6 parameters against a bar of 5. Non-gating but real: the new
buildScopedRecvDecls at cognitive complexity 30, and bindsVisitNode 88 -> 94 with three new
parameters across pushRawBind/emitBind/emitDeclBinds.

- ctorTypeOf and qualifiedNameText read through nodeTextOf.
- ScopedRecvDecls is one struct holding the table and the bindings its indices point into, so the
  Narrower takes one argument for both (5 parameters).
- buildScopedRecvDecls is three single-loop steps: addParamTypedNames, addRecvDeclScopes,
  attachRecvDeclTypes.
- DeclType { name, qualified } travels as one argument. pushTypedBind is now the ONE record body
  and pushRawBind a one-line wrapper with no qualified text, so pushRawBind and emitBind keep
  their signatures, and declaredTypeOf takes the declaration branch's two ternaries out of
  bindsVisitNode. (A first cut stamped importedName after the push, emitFnBind's shape — and
  quality-delta reported it as a 70-token clone of emitFnBind.)

Zero behaviour change, measured: --pin-census and the default map on the private corpus are
byte-identical (cmp) to the previous commit's binary, as is the census over src/; narrowcheck,
chacheck, fieldnarrowcheck, resolverhonestycheck, fieldusescheck, shadowcheck and
cacheidentitycheck ALL PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
quality-delta --quality-delta=$(git merge-base origin/main HEAD)..HEAD reported gating=2 after the
refactor: api-surface contract-change on Narrower::Narrower and its one call site in buildGraph
(4 -> 5 parameters). That is the fix itself — the Narrower has to be handed the lexical
declaration table — so it is acked through the binary (--quality-ack --ack-only=api-surface),
which wrote exactly two +ack rows, one per symbol, and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d, a moved parser-version pin, two one-line verdicts

Full suite at 17a9f5f: gates=632 pass=627 skip=2 fail=3, all three this lane's own.

showcasecapturecheck (H): the 0.6.1 release capture publishes --at=src/graph.h:3406 as the FIRST
body line of rankGraphTeleport, so it has zero slack — the ~100 lines this lane added to graph.h
above it made 3406 resolve to buildGraph. The capture is the release PR's to regenerate (a lane
recording it publishes local branch names), and the code had a better home anyway: the lexical
table builder (buildScopedRecvDecls and its three steps) moves from graph.h to resolve.h, beside
the ScopedRecvDecls type and Narrower::recvVarTypeName that reads it. graph.h's diff is now five
lines in, five out, at the Narrower construction; rankGraphTeleport is back at 3404 on this tree.

qschemetripcheck: its manifest hashes ingest_cache.h's kParserVer declaration, which moved 96 -> 97
with the quality.h mirror in the same diff (qextractionkeycheck green), so this is the gate's own
re-pin case: UPDATE_GOLDEN=1, and the new pin equals the current= hash the suite printed.

gateexitcheck (G2): narrowcheck's expectRows helper and arm 18 reported through a one-line
'A && ok || no'; both are if/else now.

No behaviour change: --pin-census and the default map over the private corpus are byte-identical
(cmp) to the fix commit's binary. qschemetripcheck, gateexitcheck, showcasecapturecheck,
narrowcheck, chacheck, chaconecheck, localitycheck, fieldnarrowcheck, resolverhonestycheck,
fieldusescheck, shadowcheck, fnptrcheck, cacheidentitycheck, qextractionkeycheck, manifestcheck
ALL PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d9b2ddf3-73e8-4409-9aa4-0ed3008bb02a

📥 Commits

Reviewing files that changed from the base of the PR and between a55b118 and eaadb9d.

⛔ Files ignored due to path filters (1)
  • test/qschemetrip.hash is excluded by !test/*.hash
📒 Files selected for processing (20)
  • .ripwire_quality_acks
  • CHANGELOG.md
  • docs/EVALS.md
  • src/graph.h
  • src/ingest_binds.h
  • src/ingest_cache.h
  • src/ingest_model.h
  • src/model.h
  • src/quality.h
  • src/resolve.h
  • test/chacheck.sh
  • test/chaconecheck.sh
  • test/chaconefix/b.cpp
  • test/chafix/cha.cpp
  • test/fieldnarrowcheck.sh
  • test/localityfix/loc.cpp
  • test/narrowcheck.sh
  • test/narrowfix/control/param.cpp
  • test/narrowfix/control/untyped.cpp
  • test/resolverhonestycheck.sh
💤 Files with no reviewable changes (1)
  • test/narrowfix/control/param.cpp

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The change adds lexical receiver-declaration tracking for typed parameters and locals. Rule 2 and CHA-lite use the in-scope unqualified type, reject qualified or uncertain types, preserve cache compatibility through parser-version bumps, and add regression coverage.

Parameter receiver narrowing

Layer / File(s) Summary
Declaration type recording
src/ingest_binds.h, src/model.h, src/ingest_model.h
Bindings now preserve declaration positions, final type names, and qualified written types for parameters and local declarations.
Scoped receiver resolution
src/resolve.h
ScopedRecvDecls tracks lexical declarations. Narrower uses the innermost valid unqualified declaration for Rule 2 and static receiver typing.
Graph and cache integration
src/graph.h, src/ingest_cache.h, src/quality.h
buildGraph passes scoped declarations to Narrower. Parser version 96 changes to 97 to invalidate older cache data.
Validation and documentation
test/*, CHANGELOG.md, docs/EVALS.md, .ripwire_quality_acks
Tests cover typed parameters, shadowing, untyped controls, qualified types, CHA-lite behavior, and cache determinism. Documentation records the rule change.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: andriytyurnikov

Merge Risk: ⚪ Minimal · up to eaadb

The lexical receiver-narrowing change includes scoped resolution, qualified-type safeguards, cache-version invalidation, and targeted regression coverage. No concrete merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 16 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing incorrect resolution of member calls through typed parameters.
Full details: Docstring Coverage

Explanation

Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 16 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lane/param-receiver-binding

Comment @coderabbitai help to get the list of available commands.

…t-parameter floor was unstated

Review of #248 (CI coordinator, 2026-09-16) found two text defects in this lane's entry; both
re-measured here rather than copied.

Count: test/narrowcheck.sh at this branch against the main binary (f8e6087) exits rc=1 with
NINE FAIL rows — (7) (8) (10)x2 (11) (12) (13) (14) (15). The fix commit's own list named nine;
'ten' was a miscount.

Floor: a parameter typed as an interface whose methods are pure-virtual declarations cannot narrow
to it (Rule 2 resolves against definitions only), and the narrow lands on unrelated classes that
share the final name segment. rocksdb @ 0e2801ac3, --pin-census --no-cache, main f8e6087 vs
eaadb9d: 79 sites (88 rows) through an Iterator* parameter now split five ways over the nested
memtable/ Iterator classes, none right — 25 were a unique override pin, 26 a split over
overrides, 28 had no edge. Receivers read at the source are all 'Iterator* iter' parameters.
(The review's 32 counts unique->disjoint-split only, main 31e788c against the #254 tip.) Also
stated: a member-initializer call through a typed parameter is outside the body span and keeps
main's answer — measured on this binary as main's own wrong locality pin.

CHANGELOG-reading gates (anchorbodycheck, deckcheck, docdemotecheck, prcontextcheck,
recallevalcheck, ripwirepubliccheck, rubysettercheck, traceminecheck) ALL PASS. No code, pin,
kParserVer or qschemetrip.hash change; main is deliberately not merged (integration train 2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joyful-ii-V-I

Copy link
Copy Markdown
Collaborator Author

Disposition of the review and CodeRabbit items. One push: eaadb9d0..186fcb84 (186fcb84, docs only, no code). Every number below was re-measured on this lane's own binaries, not copied from the review.

item disposition evidence
"ten rows red on main" FIXED in 186fcb84 (CHANGELOG) and in the PR body test/narrowcheck.sh at this branch against the main binary (f8e6087c): rc=1 with 9 FAIL rows, (7) (8) (10)×2 (11) (12) (13) (14) (15). The fix commit 3d2d642b's message says "10 rows" but lists nine; it is pushed history, so it stays as is and this row corrects it.
Abstract-parameter floor (rocksdb Iterator*) DISCLOSED in CHANGELOG and the PR body rocksdb @ 0e2801ac3, --pin-census --no-cache, f8e6087c against eaadb9d0: 79 sites (88 rows) through an Iterator* parameter now split five ways over the nested memtable/ Iterator classes, none right.
Before: 25 were a unique override pin, 26 a split over overrides, 28 had no edge.
The sampled receivers are all Iterator* iter parameters. The review's 32 counts unique→disjoint split only, from main 31e788ce to the #254 tip.
Member-initializer floor DISCLOSED in CHANGELOG and the PR body On this branch alone, Decoy( Target& t ) : v( t.pick( 3 ) ) keeps main's answer, the locality tie-break's pin to Decoy::pick. The honest split the review saw comes from #254's locality cap.
CodeRabbit pre-merge check "Docstring Coverage 56.25% < 80%" DECLINE CodeRabbit's default threshold, not a project rule: .coderabbit.yaml sets no docstring option. The house convention (CONTRIBUTING §3) is a // intent comment, and every new function carries one.
CodeRabbit review nothing to answer "No actionable comments"; 0 inline comments.
Merge main / CHANGELOG conflict DEFERRED to integration train 2, per the CI coordinator main is deliberately not merged on this branch.
kParserVer 97 / test/qschemetrip.hash UNTOUCHED, per the CI coordinator Both are resolved once at landing (collision with #244).

🤖 Generated with Claude Code

joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…s a uniquely resolved edge

Relaxing the parameter guard to refuse only std:: types keeps 111 correct narrows through
qualified in-repo types (rocksdb 94, a private C++ corpus 12, src/ 5) and one known wrong one:
narrowcheck arm 24's `ext::map<int, int>& table; table.find( 1 )` pins an unrelated in-repo
map::find where main declines. The typed local twin (`ext::map<int, int> m; m.find()`) did the
same on main already. Nothing on either edge said the qualifier was never checked against the
class's namespace, so a guess read exactly like a uniquely resolved name.

The fix (owner decision relayed by the CI coordinator, scope B): every edge a receiver's
QUALIFIED, non-std written type chose — Rule 2's narrow, parameter or local, or CHA-lite's cone
prune by that type — carries prov="final-segment". The flat per-function table records whether a
declaration wrote the type qualified (resolve.h FlatRecvType / recordFlatRecvType); the lexical
table reads it off the declaration; Narrower::finalSegmentTypeAt answers it per site. The value
takes precedence below split (resolve.h edgeProvenance, which now owns the whole scip > binding
> import > split > final-segment chain graph.h spelled inline), and both the map legend and the
compact legend define it. graph.h stays line-neutral (+32/-32 against #248; rankGraphTeleport at
3404), so the showcase seed --at=src/graph.h:3406 still resolves.

Gate: narrowcheck arm 25 — RED before: (25a) arm 24's wrong edge, (25b) arm 23's correct
qualified parameter narrow, (25c) arm 22's local twin, (25f) map legend, (25g) compact legend.
Controls green before and after: (25d) arm 18's unqualified narrow and (25e) a uniquely named
call carry no prov=. localitycheck, clsrecvcheck, chacheck, chaconecheck, fieldnarrowcheck,
resolverhonestycheck and showcasecapturecheck pass. Byte cost is measured with the collision
guard that follows, on the final binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…d locality pins, and supersede #248's parameter rule by name

Only the C++ precision of the removed locality pins was published (14 of 14 rocksdb, 14 of 16
private), which invites that reading for every language. An independent review sampled the
dynamic-language sites: django 8 of 14 wrong and 6 right, rails 8 of 12 wrong and 4 right, every
right target kept inside the split that replaces it. Both rates are in the entry now, with
Python's `cls` receivers (2 of django's 72) named.

The entry also overturns #248's parameter rule in the same Unreleased section; it now says so
explicitly ("only a written, unqualified type narrows" and "keep their previous answer" are
superseded: a parameter refuses only a std:: type, as a local does). #248's own paragraph is left
to that lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…yped-locality

#248's docs-only follow-up (nine rows red on main, not ten; the abstract-parameter and
member-initializer floors stated). CHANGELOG.md: union — #248's new floors paragraph closes its
own entry, this branch's entry follows unchanged. No #248 line is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
… 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>
@joyful-ii-V-I

Copy link
Copy Markdown
Collaborator Author

This lands through integration train 2, #265, with parser version 97. Resolutions are on that branch, so nothing more is needed here. This PR will show as merged when #265 merges.

@joyful-ii-V-I
joyful-ii-V-I merged commit 1d8192b into main Sep 17, 2026
1 check passed
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
integration: train 2 — param receiver binding (#248), untyped receiver locality + final-segment disclosure (#254), std:: field compose (#257)
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…ane's base); no conflicts, no pins move

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…ype, arms renumbered after #254's 25, identity claims carry no prov="final-segment"

origin/main bcd3b01 brings #248, #254 and #257. Two files conflicted; rerere was off and no recorded resolution was used.

- src/resolve.h: #254 added a namespace-level RecvVarType {name, writtenQualified}, and this lane added a nested
  Narrower::RecvVarType {name, declared}. Git merged the second in beside the first, so it would have shadowed it. Both
  are now ONE namespace-level struct {name, writtenQualified, declared}; recvVarType fills all three, and
  finalSegmentTypeAt is #254's own.
- test/narrowcheck.sh: #254's arm 25 (prov="final-segment") keeps its number. This lane's arms 25-34 become 26-35;
  expectProv/provOf take an optional map file ("${5:-}" under set -u).
- src/graph.h (line-neutral; rankGraphTeleport stays at 3404): an identity CLAIM verified its one class before
  answering, so its edges are not a last-name guess. Two lines change in place: `&& !identityClaim` joins the
  finalSegmentType condition, and its comment is reworded. A class-qualified step-1 narrow keeps the disclosure.
- Arm 36, `hs::DiskHealth& d; d.fine()` claiming the inherited HealthBase::fine through namespace evidence:
  (36a) the edge, (36b) no prov=, (36c) `Skip::Iterator& it; it.key()` keeps prov="final-segment". A build without
  the graph.h condition fails (36b) with prov=[final-segment].
- CHANGELOG entry and ack text: the arm numbers only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
andriytyurnikov pushed a commit to andriytyurnikov/ripwire that referenced this pull request Sep 17, 2026
…no side hashed the merged manifest

Main's pin (37dac79a) hashed parserVer 96 over redhat-et#249's new deserializeSnapshot; redhat-et#248 (db0e8cce, 97), redhat-et#254
(8ca11f67, 98) and redhat-et#257 (01ea4c63, 99) each hashed their own number over the manifest before redhat-et#249. The merged
tree declares 99 over redhat-et#249's function, which none of them hashed. kParserVer is assigned in merge order and
equals what each lane already declared (redhat-et#248 97, redhat-et#254 98, redhat-et#257 99); kCacheVersion stays 22, kQSnapCacheScheme 12.
New pin 95818fe5 == the current= the gate printed before UPDATE_GOLDEN; qschemetripcheck and qextractionkeycheck
ALL PASS on this tree. RE-PIN LOG entry added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aniruddhaadak80 pushed a commit to aniruddhaadak80/ripwire that referenced this pull request Sep 17, 2026
…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>
aniruddhaadak80 pushed a commit to aniruddhaadak80/ripwire that referenced this pull request Sep 17, 2026
… not the deciding mechanism redhat-et#248 relabels

With redhat-et#248 (typed-parameter receivers) on the same tree, the non-template control's `use( Box& b ) { b.grow(); }`
narrows by receiver-rule, while the template twin's `Box<int>& b` keeps its argument list in the written type,
matches no scope, and reaches the SAME single target (box.hpp::Box::grow#2) labelled `unique`. The arm asserted the
C rows byte-identical, so it went red on a label, not an edge. It now masks the mechanism (column 2) and flag
(column 5) columns and still compares every S row and every C row's caller, callee, targets and line; main 31e788c
still fails it (42/64 overall). Follow-up, not in this train: Rule 2 does not narrow a template-typed parameter.
CHANGELOG's redhat-et#256 entry says the census comparison is on identities.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pt-act pushed a commit to pt-act/ripwire that referenced this pull request Sep 18, 2026
… nested classes of the same name

`AssertItersEqual( Iterator* iter1, Iterator* iter2 ) { … iter1->key() … }` (rocksdb) answered with five edges to the
nested Iterator classes inside memtable/'s skip lists, none right: Rule 2 keys a receiver type by its final class-name
segment, a nested class keeps only that segment, and rocksdb::Iterator's pure-virtual methods are never in the
definitions-only map. redhat-et#248 disclosed 79 parameter sites; typed locals share the shape.

Rule 2 now reads calls through class identity (resolve.h ClassIdentity, built from existing ingest facts — no
extraction change, kParserVer unmoved): spans give enclosing classes and member owners, inherit references the class
graph. identityNarrow (1) drops hits owned by a nested class the written type cannot name (C++ lookup outward from the
caller; a qualifier naming the enclosing class), (2) with none left takes the shallowest real ancestor defining the
callee, (3) when the ancestry only declares it resolves to its definitions in the real subclasses — a dispatch split,
kept whole past the locality ladder (graph.h, line-neutral: rankGraphTeleport stays at 3404). Guards, each from a
wrong edge an intermediate build made on a corpus: forward declarations are not classes; namespace-level namesakes are
told apart by include evidence (path-precise, else an include-root target read as a path suffix, else a shared
namespace); a nested class visible through the caller's includes is never dropped (type aliases); identity replaces
an answer only with one it can explain.

Measured, --pin-census --no-cache, stack tip 50129f8 vs this commit: rocksdb 2,659 sites change target, bound
200,036 -> 201,085; llvm-project 37,949 of 1,790,841, bound 1,126,051 -> 1,153,808; private corpus 177, bound
80,582 -> 80,646; src/ 0. Every bucket sampled against source on the final build. llvm cost, two cold runs each:
user 52.4/52.9 s -> 50.9/55.9 s, RSS 2.37-2.47 GB both; byte-deterministic on all corpora.

Gate: test/narrowcheck.sh arms 25-34 (40 PASS). Red on the stack tip binary: (25) (26) (27) (28) (31) (33) (34);
controls (29) (30) (32). Resolver set green: narrowcheck chacheck chaconecheck localitycheck chainguardcheck
fieldnarrowcheck clsrecvcheck fieldusescheck fnptrcheck shadowcheck pincensuscheck aritycheck resolvecheck
resolverhonestycheck narrowlangcheck importnarrowcheck externalvetocheck qualifiedresolvecheck objcfieldcheck usescheck
callerscheck selectorchaincheck chainidcheck cppqualcheck decltodefcheck canoncheck cppbenchcheck showcasecapturecheck
gateexitcheck kotlincheck multirootcheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant