Repository navigation
fix(cpp): a range-based for loop over auto elements has the element type of its range - #2492
Merged
Merged
Conversation
… type of its range #2439 typed a range-for variable when the loop names its type. A loop over `auto` elements names none, so receiver inference kept scanning as for an `auto` local with an unreadable initializer: it reached an earlier declaration of the same name, often another variable's, or fell through to the guess by the receiver's name. facebook/rocksdb's db_impl_write.cc loops `for (auto cfd : cfds)` over a `const autovector<ColumnFamilyData*>&`. The loop's range is now read from its header and typed as written, template arguments and all, since the index keeps only a return type's last name: - a name the calling function declares, or a data member of its class read from the class's own lines (`versions_` from db_impl_write.cc); - an `auto` local's initializer; - `*x`, `x.m`, `x->m`, `this`, and calls whose declaration writes their return type, free or on the object's class (own or inherited); - aliases followed with their template arguments (cppAliasedTypeWritten); - the element: a standard or abseil sequence container's first template argument, protobuf's RepeatedPtrField and rocksdb's autovector's, or what a project class's begin() returns, dereferenced (ColumnFamilySet's iterator's operator* gives ColumnFamilyData*). Nullability annotations are dropped. The element goes through the same declaration record as any declared receiver, so the typed path, #2413's gate for a standard-library element and #2440's held-type lookup for `->` on a smart-pointer element apply unchanged. Classes are looked up with #2440's cppClassWritten, methods through its cppMethodOf, and matchCppHeldTypeCall now reads its held type through the shared cppStdHeldType. A/B on main 8a5ba3d, site-keyed: leveldb identical; fmt +1 / 3 retargeted; rocksdb -68 / +173 / 104 retargeted / 231 confidence only; protobuf -13 / +25 / 7 / 59. Every removal and retarget was a wrong guess; 4 of rocksdb's additions reach MemTableRep::Iterator through the typed path's last-name lookup, as a declared `Iterator* it` does on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 11, 2026
…e class its type names, not a namesake (#2493) Receiver typing resolved a C++ call on a variable, parameter or member by the declared type's last name: `Iterator* iter; iter->Valid()` became resolveMethodOnType('Iterator', 'Valid'), which takes any class named `Iterator` that declares `Valid`. facebook/rocksdb has 22 such classes, and the `Iterator` the code means inherits `Valid`, `Next` and `key` from `IteratorBase`, so nested namesakes like `MemTableRep::Iterator` took the calls (google/leveldb's went to `SkipList::Iterator`). A declaration the calling function or its class writes, called with the operator it takes (`->` on a pointer, `.` on a value or reference), now goes through the same C++ lookup #2440 uses for smart pointers' held types (cppClassWritten from the caller's scopes, then the file's `using`s, aliases followed) and cppMethodOf (the class's own method, else one through its own base edges): - An alias #2399 followed from the caller (function-local typedefs included) is the class it names, by qualified name. An `auto` loop's element type (#2492) is looked up where its range's type is written. - A class found only as the one class of that name the file can see, a template parameter's type, a library type, or a type nothing visible declares: the call goes on as before. - A class with no such method gets no edge, unless it is a class template or a base on the way is one the index doesn't link (a `decltype(…)` base, an alias each `#if` arm declares differently), which may declare it: then as before. - Declarations read from elsewhere, or past one the scan couldn't read, are unchanged. cppClassWritten now tries the file's `using`s before the global scope: protobuf's upb generator writes `using google::protobuf::Descriptor;` inside its namespace, and the PHP extension declares a global C struct `Descriptor` in a header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A C++ call on the variable of a range-based
forloop overautoelements now has the element type of the range the loop iterates, read from the range where that can be done reliably.#2439 typed a loop variable when the loop names its type (
for (ConformanceTestSuite *suite : suites)). A loop overautoelements names none, so receiver inference kept scanning as for anautolocal with an unreadable initializer: it reached an earlier declaration of the same name (often a different variable, sometimes in another function) or fell through to Strategy 3's guess by the receiver's name, with #2413'sshadowedmark set. facebook/rocksdb'sdb/db_impl/db_impl_write.ccloopsfor (auto cfd : cfds)over aconst autovector<ColumnFamilyData*>¶meter.What the range is read from
cppAutoLoopElementreads the range expression from the loop header (cppRangeExpression), works out its written type (cppExpressionType), then its element (cppRangeElementOf). Types are kept as written, template arguments and all, with the scope, file and line they are written at, because the index'sreturnTypekeeps only the last name (FieldNumberOrderreturnsvector).isCppCallersDeclaration), not shadowed. A name the method doesn't declare is read as a data member of its class from the class's own lines (versions_indb_impl_write.cc, declared indb_impl.h, which no same-stem header scan reaches). Anautolocal's initializer is read in turn (const auto& blob_files = vstorage->GetBlobFiles();).*x,x.m,x->m,this: a pointer's pointee or a standard holder's held type; a data member read from the class's lines outside its nested functions and classes (C++ data members are not indexed).f(…),ns::f(…),Class::f(…)looked up from the caller's scopes outwards;x.f(…)/x->f(…)on the object's class, own method or inherited (fix(cpp): a->call on a smart pointer or optional reaches the type it holds #2440'scppMethodOf). The return type is read from the declaration's text.vector,set,deque,Span,InlinedVector,flat_hash_set, …) or of protobuf'sRepeatedPtrField/RepeatedFieldand rocksdb'sautovector; for any other project class, what itsbegin()returns, dereferenced (a pointer, a container's::iterator, or an iterator class'soperator*). That is howfor (auto cfd : *versions_->GetColumnFamilySet())reachesColumnFamilyData*.cppAliasedTypeWritten(cpp-type-aliases.ts), from where the alias is declared: rocksdb'sconst BlobFiles&isstd::vector<std::shared_ptr<BlobFileMetaData>>.->call on a smart pointer or optional reaches the type it holds #2440'scppClassWritten. Clang/abseil nullability annotations (const FieldDescriptor* const absl_nonnull) are dropped from element types.Not read (the scan goes on as before): a map's elements (a pair, whose
first/secondare recorded as bare calls), structured bindings (13 loops in protobuf, 43 in rocksdb), an element a template argument decides, an alias each#ifarm declares differently (rocksdb'sport::Thread), subscripts (files_[level]), and members a header declares below the inline method that loops over them.The element then goes through the same declaration record as any declared receiver (
noteCppDeclaration,cppDeclaredType), so the existing rules apply unchanged: the typed path, #2413's gate for a standard-library element (std::string,std::thread), and #2440's held-type lookup for->on astd::unique_ptr/std::shared_ptr/std::optionalelement.Smart-pointer elements: folded in through #2440
->on a smart-pointer element is the gap #2413 left and #2440 closed for declared receivers. With #2440 merged, a smart-pointer element is recorded as its holder text, so #2440'smatchCppHeldTypeCallresolves->on it with the C++ lookup, and.on it (p.reset()) is the holder's own member, which #2413's gate silences. An earlier version of this change left->on smart-pointer elements alone while #2440 was open: without the held-type lookup, typing the holder lost lucky-right edges in rocksdb'srange_del_aggregator.cc(iter->Valid()overstd::vector<std::unique_ptr<TruncatedRangeDelIterator>>).Overlap with #2440
The held-type reading moved into
cppStdHeldType, next to #2440'sCPP_STD_HOLDERS, andmatchCppHeldTypeCallnow calls it (same behavior). The class lookup, the class-kind set and the base-class method walk are #2440's (cppClassWritten,CPP_CLASS_KINDS,cppMethodOf);cppMemberOwnerfrom #2479 gives the calling method's class.Validation
Setup. Base is main
8a5ba3d0(with #2439, #2440, #2479); the other arm is this branch. Both arms rancodegraph init -ywith one native kernel built from main's source, on this Windows box. Edges are diffed per call site (kind, source, line, col, refName). A probe logged everyautoloop the scan met with its range and the element read.Loops read: rocksdb 528 of the 649
autoloops the scan met, protobuf 158 of 255.Removed (every one was a wrong guess):
std::string.c_str()(34, all to googletest'stesting::internal::FilePath::c_str),.find()(14, totoku::omt::find),.size()(9),std::thread.join()/.joinable()(9, toport::WindowsThread),std::vector<size_t>.empty()(1). Onetable.reset()on astd::unique_ptr<BlockBasedTable>element, which wasTableIterator::reset.std::stringstream::strandstd::vector::size(1 each), and 11 calls on elements of the generatedFieldDescriptorProto,OneofDescriptorProtoandDescriptorProto. The index has no class node for these (only their.pb.ccconstructors), so the existing rule for a type the project doesn't declare drops the guess. All 11 were wrong:upb::OneofDefPtr::name,google::protobuf::Field::name, a C# generator'shas_default_value, andFieldDescriptor::type/json_nameat 0.9 through an earlierconst FieldDescriptor* field.Retargeted (every one wrong → right):
->call on a smart pointer or optional reaches the type it holds #2440:listener->OnFlushCompleted(…)overstd::shared_ptr<EventListener>went from the stress test'sDbStressListenertoEventListener;std::shared_ptr<BlobFileMetaData>elements fromSharedBlobFileMetaData;std::unique_ptr<Iterator>elements fromMemTableRep::Iterator/ArenaWrappedDBItertoIteratorBase;job->Run()fromCompactionJob::RuntoFlushJob::Run.column.name()fromLazyWideColumn::nametoWideColumn::name;blob_file.GetBlobFileNumber()overGetBlobFileGarbages()fromBlobFileAdditiontoBlobFileGarbage;mem->ApproximateMemoryUsage()overautovector<ReadOnlyMemTable*>to the declaredReadOnlyMemTable;edit_list.empty()overautovector<autovector<VersionEdit*>>toautovector::empty.std::unique_ptr<MessageGenerator>,EnumGenerator,OneofGeneratorandExtensionGeneratorlists, which had gone to other generators' methods.test_suite_info->GetTestSuiteName()overstd::vector<ParameterizedTestSuiteInfoBase*>, from the derived template's method at 0.65 to the base's at 0.9.Added: rocksdb 126 calls on loop variables plus 47 chained calls whose receiver is one (
cfd->mem()->GetFirstSequenceNumber(), throughmatchCppCallChain); protobuf 22 plus 3; fmt 1 (gmock'sm.GetDescriber()→MatcherBase::GetDescriber). All checked. 4 are wrong: rocksdb'scolumn_family_test.ccloops over astd::vector<Iterator*>and callsSeekToFirst()/Next(), which reachMemTableRep::Iterator. That is the typed path's lookup by the type's last name, the namesake case #2440 measured for 1,364 declaredIterator* xreceivers; a declaredIterator* itgets the same edge on main.Confidence only: all on the same target, up to 0.9 (rocksdb 114 from 0.7, 92 from 0.65, 25 from 0.8; protobuf 42 from 0.7, 17 from 0.65).
The re-resolve harness used while iterating (both arms' resolvers over one index) agreed with the full A/B site for site; the full A/B adds only the chained calls the harness didn't re-resolve.
Time. rocksdb, indexed interleaved base/fix/base/fix on an idle box: 35 s / 36 s / 36 s / 35 s. Each loop's element is worked out once per calling function and loop.
Tests
__tests__/cpp-auto-range-for-receiver.test.ts, fixtures cut from rocksdb and protobuf. The assertions check the typed path's confidence, since a guess can land on the right method at 0.65/0.7. 7 of the 8 tests fail on main; the eighth, an element a template argument decides, is a guard that passes on both.for (auto cfd : cfds)over anautovectorparameter, where the old scan reached another function'sColumnFamilyHandle* cfd;*versions_->GetColumnFamilySet(): a member another header declares, a smart pointer's->, and a class iterated through itsbegin()and its iterator'soperator*;autolocal's initializer, an alias with template arguments, and astd::shared_ptrelement;std::stringelement's own member links to nothing;std::unique_ptrelement:->reaches the held type,.reset()nothing;Ran:
tsc --noEmit; the new file; everycpp-*,c-cpp-*,c-calls-*andkernel-ccpp-paritysuite plusextraction.test.ts(998/998, kernel rebuilt from main's source); the full suite, 6,913 passed and 82 skipped, with one failure,daemon-pid-reuse.test.ts'sEBUSYrmdir in teardown on Windows, which passed when rerun alone.🤖 Generated with Claude Code