Skip to content

fix(rust): a call on what as_mut() hands back reaches the receiver's declared type - #2710

Merged
colbymchenry merged 1 commit into
mainfrom
claude/rust-as-mut-chains
Oct 20, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/rust-as-mut-chains

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #2701: Rust calls chained on .as_mut() / .as_ref().

What was wrong

A call written x.as_mut().m(…) reaches the resolver by its bare name (m), so it was matched by name alone — #2701's Pin lookup never saw it. On the A/B repos that linked, among others:

call receiver declared main linked it to
tokio self.delay.as_mut().reset(now) ×3 delay: Pin<Box<Sleep>> the calling Interval::reset
tokio this.timer.as_mut().as_pin_mut() ×4 Option<…> LinesStream::as_pin_mut
tokio reader.as_mut().poll_read(…), f.as_mut().poll(cx) ×6 Pin<&mut R>, a generic F BufReader::poll_read, BlockingTask::poll
tokio entry.as_ref().might_be_registered() ×2 NonNull<TimerShared> StateCell::might_be_registered
dioxus self.child.as_mut().map(…), self.server.as_mut().filter(…), x.as_ref().context(…) ×12 Option<…> GenerationalRefMut::map, Store::filter, DiffState::context, …

The change (src/resolution/name-matcher.ts)

matchRustAsMutChainCall, an exclusive hook beside the C++ chain hooks in matchReferenceInner, reads the chain from the call's own column (self.delay.as_mut().reset(, with an optional ? after as_mut()) and resolves the method on what as_mut / as_ref hands back for the receiver's declared type (rustLentCall):

  • a Pin<P> re-pins what it points at → fix(rust): a call through a Pin, Rc or Box self reaches the method Rust picks, inherent first #2701's lookup through Pin (rustPinnedCallOn);
  • a Box, NonNull, Rc, Arc or Cow lends what it holds → that type's method (rustCallOnType);
  • an Option / Result stays one — std's methods, no edge — unless ? takes the value back out (self.client.as_mut()?.next());
  • a type parameter, or another std or dependency type → no project method;
  • a project type's own as_mut (return type not read here), or a receiver whose type isn't written where it can be read (a pin! local, a call's result) → undefined, the name strategies decide as before.

The receiver's type comes from: self (through self: Pin<&mut Self>, self.as_mut() is that same Pin); a field's declaration in its struct (rustFieldDeclaration), also when a pin_project! macro wraps the struct and through pin-project's let me = self.project(), where a #[pin] field is Pin<&mut T>; a local's or parameter's declaration (rustDeclaredTypeText, which now also reads let x: T = … — #2701's reader stopped at the let and missed the annotation). A cheap line check (.as_mut() / .as_ref() on the call's line) keeps the hook off every other bare call.

Validation

Fresh full indexes, main f2f5dee vs this branch, keyed by call site (re-run after rebasing onto 90e2456, which brought #2707's Rust extractor change: the same changed sites, site for site; the branch now sits on a31bf2f, whose one newer commit is C-only):

repo removed retargeted added same target, now by declared type
tokio 15 6 13 27
dioxus 16 1 0 1
ripgrep 1 0 0 0
gleam 1 0 0 0
axum, clap, serde — — — identical
express, gin, flask, leveldb (controls) — — — byte-identical dumps

Every changed site was read:

  • removed (33), all wrong on main: Option / Result methods (map, filter, cloned, is_none_or, as_pin_mut, anyhow's context) guessed onto project types; generic readers, writers, sinks and futures (R, W, S, F); Pin::get_ref → AsyncFd::get_ref; an external Stream / dyn Future / web_sys::Node method; ripgrep's path.as_ref().metadata() and gleam's path.as_ref().replace(…) on P: AsRef<Path> / a string.
  • retargeted (7), all now right: Sleep::reset ×3 (was the calling Interval::reset), TimerShared::might_be_registered ×2 (was StateCell's), Fuse::poll_next (was the calling Peekable::poll_next), dioxus HasAnimationData::as_any through a Box<dyn HasAnimationData> (was SerializedAnimationData::as_any).
  • added (13), all right: pin-projected fields (MaybeDone::poll ×3, Root::poll ×3, Notified::poll, MaybeDangling::poll, Fuse::poll_next), NonNull<TimerShared> calls ×3, Sleep::reset.

Not in this PR: an earlier draft also made the shared Rust/Go call-shape reader stop treating a receiver that merely starts with the method's name (builder().build(), abort_handle…abort()) as a bare call. That fixed a few calls but exposed others to the name guess (dioxus's builder().build() → BuildRequest::build), so it was dropped; the as_mut hook reads its chain itself instead.

Cost: none measurable. The hook's own time, from the resolver's stage profiler (CODEGRAPH_RESOLVE_PROFILE=2) summed over every worker: 180 ms on tokio (8,014 bare calls checked), 61 ms on dioxus (12,609), against 31–36 s of CPU per full index; a call without .as_mut() / .as_ref() on its line costs one regex test. Interleaved full indexes (CPU, median of 3) moved within this machine's 2–4 s run-to-run spread: tokio 31.2 s → 32.1 s, dioxus 34.2 s → 36.1 s.

Tests

__tests__/rust-as-mut-chains.test.ts (7 cases over Pin<Box<T>>, Option<T>, Box<T> and NonNull<T> fields, Option through ?, pin-project fields pinned and not, Pin<&mut R> generic and named, self.as_mut() through self: Pin<&mut Self> with Pin::get_mut): 5 fail on main, the other 2 guard behavior main already has. With #2701's and the other Rust suites: 39/39. Full suite (CODEGRAPH_KERNEL_EXPECT=1, kernel built, 4 workers): 7619 passed, 0 failed, 80 skipped.

Overlap

The peer session working on Rust receivers typed by their declaration (branch claude/rust-receiver-types) handles ident.method refs only and confirmed it doesn't see these bare chained calls; the two touch different functions.

🤖 Generated with Claude Code

…declared type

A Rust call written x.as_mut().m(..) or x.as_ref().m(..) reaches the
resolver by its bare name, so it was matched by name alone. tokio's
self.delay.as_mut().reset(now) on a Pin<Box<Sleep>> field linked to the
calling Interval::reset, its this.timer.as_mut().as_pin_mut() on an
Option to a stream's as_pin_mut, reader.as_mut().poll_read(..) on a
generic R to BufReader::poll_read, and dioxus's
self.child.as_mut().map(..) and self.server.as_mut().filter(..) on
Options to its own GenerationalRefMut::map and Store::filter.

Such a call is now read from the call's own chain and resolved on the
receiver's declared type as as_mut passes it on: a Pin<P> re-pins what
it points at (Rust's lookup through the Pin from the previous change),
a Box, NonNull, Rc or Arc lends what it holds, an Option stays an
Option, whose methods are std's, unless ? takes the value back out
(self.client.as_mut()?.next()), and a type parameter or another std or
dependency type gives no project method. A field is read from its
struct's declaration, also through pin-project's let me = self.project()
(a #[pin] field projects to Pin<&mut T>), even when a pin_project! macro
wraps the struct; a local or parameter from its own declaration, which
now also reads let x: T = .. annotations. A receiver whose type isn't
written where it can be read, a pin! local or a call's result, is left
to the name strategies as before.

A/B on tokio, axum, clap, serde, ripgrep, gleam and dioxus: 33 wrong
links removed, 7 moved to the right method, 13 right links added, none
lost, and 28 same-target links now resolved from the declared type
instead of by name. axum, clap, serde and the controls express, gin,
flask and leveldb are byte-identical.

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