fix(resolve): a member call through a typed parameter was pinned to the caller's own class - #248
Conversation
…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>
|
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 (20)
💤 Files with no reviewable changes (1)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughChangesThe 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
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: Medium Suggested reviewers: Merge Risk: ⚪ Minimal · up to 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)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
…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>
|
Disposition of the review and CodeRabbit items. One push:
🤖 Generated with Claude Code |
…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>
…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>
…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>
… 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>
…ane's base); no conflicts, no pins move Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…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>
…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>
…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>
… 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>
… 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>
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 noamb=and nothing disclosed:Cause. Rule 2 narrowed a receiver only through
LocalBindKind::Type(typed locals). A parameter's written type (ParamType) was captured, but onlycollectFieldUseSitesread 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-censusshows the site asmech=locality, 2 candidates cut to 1.The change
Rule 2, and CHA-lite through the same lookup (
Narrower::recvVarTypeName), now readsParamTyperecords lexically. For every name with aParamTyperecord,buildScopedRecvDecls(inresolve.h, beside the table type and its reader) lists all of that name's declarations in the definition: eachVarDeclwith its scope span. It then attaches every Type/ParamType record to the declaration whoseVarDeclshares its record position (Binding::startByte, new; it sits in the padding afterkind, sosizeofis 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 noParamTyperecord keep the flatvarTypetable 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):
autoloop of the same nameThe 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-repomap. That gave three such edges on a private C++/ObjC++ corpus (129,759 call sites).RawBind(importedName, seequalifiedNameText), and the lexical lookup refuses it.main's answer."LinearMath/btVector3.h"), so it refused about 150 correct narrows on that corpus to stop the same three.kParserVer96 → 97, with thequality.hmirror. 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, themainbinary (f8e6087c) against this branch.src/Private corpus breakdown:
bound=80,432 → 80,583,declined=17,552 → 17,401.uniquepins to the one same-file method of the wrong class, e.g.body->setGravity()insideaddRigidBody( btRigidBody* body ).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.sharms 7–18 run on a generated fixture. Red/green evidence:mainmech=locality)Five gates had controls built on "a parameter has no binding". They now use an untyped
autoreceiver:narrowcheck:control/param.cppbecomescontrol/untyped.cpp.chacheckchaconecheckg5localitycheck: its call no longer reached the tie-break the gate exists to test.resolverhonestycheckF9: itscheck_signalrow had gone vacuous on a single edge.fieldnarrowcheck(h) moves from 7 to 6 becauseshadowParam( Decoy& m_x )now resolves to the parameter's type. Its (s1) arm now also asserts thatPool::acquireis not linked; that assertion is red onmain.Targeted runs on the final binary, 37 gates, ALL PASS:
Checks:
gates=632 pass=627 skip=2 fail=3. All three failures came from this lane, and commit 5 fixes each:showcasecapturecheck(H): lines added tograph.hshifted the zero-slack published seed. The builder moved toresolve.hinstead of re-recording the release capture.qschemetripcheck: thekParserVerre-pin case.gateexitcheckG2: two one-line&& ok || noverdicts, now if/else.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 environmentalargvdiffcheckandeditchecknotecheck;g1freshcheckran against a freshasan/and passed.docs/gatecount_build.py --checkanddocs/limits_build.py --checkboth clean (618 gates; 212 caps).$(git merge-base origin/main HEAD)..HEAD:gating="0".gating="3": actorTypeOf|nodeTextOfclone, andNarrower's constructor at 6 parameters. I fixed both in code;1508bd10restructures and is output-identical bycmp.Narrowerconstructor's intended 4 → 5 contract change, acked through the binary (--ack-only=api-surface, exactly two+ackrows, one per symbol).-fno-sanitize-recover=all): zero reports on the gate, this repo, a census run, and the private corpus.Out of scope, stated
186fcb84).0e2801ac3(--pin-census --no-cache,mainf8e6087cagainst this branch): 79 sites (88 rows) through anIterator*parameter now split five ways over the nestedmemtable/Iteratorclasses, and none of the five is right.amb=,prov="split"), but each is a wrong answer, not a missing one. This is the collision typed locals already have onmain, extended to parameters.Decoy( Target& t ) : v( t.pick( 3 ) )sits outside the parameter's body span and keepsmain's answer, which is the locality tie-break's wrong pin.auto x = make(); x.m()still reaches the locality tie-break.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 whenargCount > params(a documented decision about default arguments on a separate prototype), and the bare-name ladder keeps both too.other.pick<int>( 1 )) inherit this once lane/template-call-edges lands.kParserVercollision: if that lane also bumpskParserVer, the second to land re-bumps (never reuse a number).Pins moved
kParserVer96 → 97 andkIngestParserVerMirror96 → 97test/qschemetrip.hashre-pinned throughUPDATE_GOLDEN=1: its manifest hashes thekParserVerdeclaration, and the new pin equals thecurrent=hash the suite printedfieldnarrowcheck(h)ambiguous=7→ambiguous=6printf_parity, capture or gate-count pins movedsrc/graph.h's diff is line-neutral (five in, five out) so the 0.6.1 capture's--at=src/graph.h:3406seed, the first body line ofrankGraphTeleport, still resolves.Commits
fix(resolve)— the defect, gate arms 7–18, controls moved,kParserVer97, CHANGELOGorigin/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 entriesrefactor(resolve)— quality-delta's clone, parameter-count and complexity rows fixed in code; output byte-identical bycmpchore(quality)— theNarrowerconstructor contract change, acked through the binaryfix(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
Tests