Skip to content

fix(cpp): structured bindings and function wrappers give their types to calls on them - #2540

Merged
colbymchenry merged 3 commits into
mainfrom
claude/cpp-function-binding-receivers
Oct 13, 2026
Merged

colbymchenry merged 3 commits into
mainfrom
claude/cpp-function-binding-receivers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Stacked on #2536 (base branch claude/cpp-const-declarators). Merge #2536 first; GitHub then retargets this PR to main.

Summary

C++ receiver inference had no type for three kinds of declaration, so calls on them were guessed by the method's name:

  • A name a structured binding declares. facebook/rocksdb's MultiCfIteratorImpl writes for (auto& [cfh, iter] : cfh_iter_pairs_) { iter->Valid(); … }. A declaration form is protocolbuffers/protobuf's auto [dst_ext, is_new] = Insert(arena, dst_number); dst_ext->Free();.
  • What a std::function returns. For example create_iterator()->Valid() on a std::function<Iterator*()>. The declarator pattern didn't read a wrapper type at all, since its template argument has parentheses.
  • A class member whose type is written on the line above its name. cfh_iter_pairs_ above is one: std::vector<std::pair<ColumnFamilyHandle*, std::unique_ptr<Iterator>>> on one line, cfh_iter_pairs_; on the next.

What changed (name-matcher.ts)

  • cppStructuredBindingType. A binding's name has the type of the std::pair / std::tuple element at its place (cppBindingElements). The value bound is:

    • an element of the range a range-for over the binding iterates, when the call is in the loop's body (cppForBodyEncloses, as for a loop's variable);
    • or a declaration's initializer that the calling function writes.

    Anything else, such as an aggregate struct, stays unknown and is skipped as before (cppRebindsReceiver).

  • Maps (std::map / unordered_map, abseil's flat_hash_map / node_hash_map / btree_map, …) have std::pair<const K, V> range elements (CPP_LIBRARY_MAPS).

  • cppStatementFrom reads a statement on to its ;, for a binding's initializer and, as before, an auto local's.

  • Function wrappers. The declarator type reads std::function, std::move_only_function, std::copyable_function, absl::FunctionRef and absl::AnyInvocable with their function-type argument (CPP_FUNCTION_WRAPPER_TYPE). cppExpressionType gives a call on such a variable what the function type returns (cppWrapperReturnType), for chains and auto locals.

  • cppDataMemberIn reads a member whose name begins its line when the line above is a type and nothing else (CPP_TYPE_LINE). So a default member initializer continued on the next line (bool ready_ = flag_ && / it_;) isn't taken for a declaration. Inline method bodies were never read for members.

Validation

Site-keyed edge diffs (diff-sites.mjs) of #2536's head 924db7c6 against this branch. Both arms were built from the same tree, kernel included.

repo removed added retargeted metadata-only
google/leveldb 0 0 0 0
fmtlib/fmt 0 0 0 1
protocolbuffers/protobuf 22 9 0 0
facebook/rocksdb 98 44 20 32
redis/redis (C, control) 0 0 0 0

Nodes and files are identical between the arms. Every removed group and every added, retargeted and metadata site was read:

  • Removed (120), all wrong guesses. The receiver is a library type that is now read, so its call links nowhere, as such calls do elsewhere:
    • two-line members: rocksdb's std::priority_queue heap_ / erased_heap_ (had reached BinaryHeap / PreparedHeap methods), std::vector<std::unique_ptr<TruncatedRangeDelIterator>> range_tombstone_iters_ (FragmentedRangeTombstoneList::empty), maps' find / erase (toku::omt::find, PreparedHeap::erase), vectors' emplace_back (autovector); protobuf's absl::flat_hash_map members (google::protobuf::Map::find / contains / try_emplace);
    • protobuf's const auto status = do_work(i); status.ok() on an absl::AnyInvocable<absl::Status(int)> (had reached upb::Status::ok).
  • Added (53):
    • protobuf: dst_ext->Free() (auto [dst_ext, is_new] = Insert(…), a std::pair<Extension*, bool>); a->ptr() / b->ptr() after auto [a, b] = GetArenaPairWithOffset(…), whose std::pair<std::shared_ptr<const upb::Arena>, …> return type spans two lines;
    • rocksdb: start.value() / limit.value() after auto [start, limit] = MaybeAddTimestampsToRange(…), OptSlice; two-line members such as flushed_save_points_->size() (std::unique_ptr<autovector<…>>), trace_sequence_f_->Append(…) (WritableFile), compression_types_used.Contains(…) (SmallEnumSet).
  • Retargeted (20), wrong → right:
    • iterator->Valid() / Seek(…) from MemTableRep::Iterator to IteratorBase;
    • collector->NeedCompact() over a two-line std::vector<std::unique_ptr<InternalTblPropColl>> (was UserKeyTablePropertiesCollector's);
    • unpacked_value.ToString() after auto [unpacked_value, unix_write_time] = … (was IndexValue's, now Slice's);
    • cached_range_tombstone_.Size() (CoreLocalArray);
    • trace_sequence_f_->Close() (WritableFile).
  • Metadata-only (33): the same targets at 0.9 by lookup instead of a 0.65 / 0.7 guess (OptSlice::has_value, CacheEntryStatsCollector::GetStats, …).

Tests

__tests__/cpp-binding-and-wrapper-receivers.test.ts has 6 cases checking targets and confidence:

  • the rocksdb loop over a two-line member;
  • a map's value, a declaration's pair element, and a tuple element;
  • std::function called directly and through an auto local;
  • no type for a call after the loop, for an aggregate's binding, for another function's binding, or for a template whose arguments aren't a pair's;
  • a member's type from the line above only when that line is a type.

The red phase on #2536's code fails the first three.

Mutation check. 10 guards were each disabled alone (mutate-fnb.mjs), and each one fails a test:

  • the range-for binding, its loop-body check, a declaration's binding, and the calling function's own declaration;
  • pair/tuple only, and map elements;
  • the member type from the line above, and the type-only line;
  • reading a wrapper's declaration, and the return type of a call on one.

I dropped two checks I'd first written, because neither could fail a test:

  • the member's name beginning its line, since the type-only line already decides;
  • a list of known wrappers in cppWrapperReturnType, since only a wrapper's declaration is read with a function-type argument.

The arm without both checks gives exactly the same graph as the A/B arm above, on all five repos.

Full suite: 6,978 passed, 82 skipped. 9 tests failed in 7 files, all in areas this change does not touch: git index currency, sync, the MCP daemon, stale-index refusal, the unindexed-root policy, worktree detection, and a 60-second function-ref timeout. The run overlapped a mutation run and two repo indexings, and those 7 files pass when rerun alone (140 passed, 1 skipped).

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 12, 2026 22:44
…e gives its type to calls on it

Receiver inference reads a C++ declaration as a type followed by the
name. What may stand between them was read as the type: in rocksdb's
`Iterator* const iter = iter_guard.get();` the type was `const`, which
names nothing, so the scan went on to an earlier function's `iter`, and
in protobuf's `io::EpsCopyOutputStream* PROTOBUF_NONNULL stream` the
type was a class named `PROTOBUF_NONNULL`. A name an annotation follows
(leveldb's `MemTable* imm_ GUARDED_BY(mutex_);`) was not read at all.

- The declarator regex (and the range-for type tail) reads past a cv-
  qualifier or `__restrict` before or after a `*` or `&` (`T* const`,
  `T const&`), the nullability annotations after one (abseil's and
  clang's, and the ALL-CAPS macros c-cpp.ts already blanks for the
  parser), and an ALL-CAPS annotation macro's call after the name.
- matchCppDeclarator hands the readers the type without those
  annotations (cppDeclaratorType); cv-qualifiers stay, since what reads
  a type reads past them. An ALL-CAPS word counts as an annotation only
  after a `*` or `&`, so a type's own name stays (`OS_HANDLE* const h`),
  and template arguments are kept as written.
- The call's own file, its same-named header, a class's members and a
  range-for's variable are read that way; the local-name check for bare
  calls reads past the qualifiers too.
- #2533's test that a method's parameter is no member used `T* const`
  locals, which the scan couldn't read; it now declares no local, since
  one the scan reads decides first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to calls on them

Receiver inference had no type for a name a structured binding declares,
for what a `std::function` returns, or for a class member whose type is
written on the line above its name. rocksdb's MultiCfIteratorImpl writes
`for (auto& [cfh, iter] : cfh_iter_pairs_) { iter->Valid(); … }` over a
member declared across two lines as a
`std::vector<std::pair<ColumnFamilyHandle*, std::unique_ptr<Iterator>>>`;
the calls were guessed by the method's name.

- cppStructuredBindingType reads a binding's type at the name's place: an
  element of the range a range-for over the binding iterates, when the
  call is in its body, or of a declaration's initializer the calling
  function makes, a `std::pair` or `std::tuple` (cppBindingElements). A
  map's range elements are pairs of its key and value (CPP_LIBRARY_MAPS).
- cppStatementFrom reads a statement on to its `;`, for a binding's
  initializer and an `auto` local's.
- The declarator regex reads `std::function`, `std::move_only_function`,
  `std::copyable_function`, `absl::FunctionRef` and `absl::AnyInvocable`
  types, whose template argument is a function type, and
  cppExpressionType gives a call on such a variable what the function
  type returns (cppWrapperReturnType).
- cppDataMemberIn reads a member whose type is the whole line above its
  name (CPP_TYPE_LINE), so a default member initializer continued on the
  next line (`bool ready_ = flag_ &&` above `it_;`) isn't taken for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from claude/cpp-const-declarators to main October 13, 2026 17:40
# Conflicts:
#	CHANGELOG.md
#	src/resolution/name-matcher.ts
@colbymchenry
colbymchenry merged commit bb817d4 into main Oct 13, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-function-binding-receivers branch October 13, 2026 17:41
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