# Autoref and next gen place traits Nadri's Blog post: <https://nadrieril.github.io/blog/2025/12/18/autoref-and-autoderef-for-first-class-smart-pointers.html> Prompted a crate to compute the type of a place expression: <https://github.com/rust-lang/beyond-refs/pull/9> - note that this doesn't support method autoref Things to talk about: - Unifying `Target` types between `Receiver` and `HasPlace` - What is the correct desugaring algorthm for `place.method()`? ## What is the correct desugaring algorthm for `place.method()`? ```rust struct Struct; impl Struct { fn method(self: ArcRef<Self>); } // Example 1: let r: Arc<Struct>; r.method(); r.method(); // good // desugar to Struct::method(@ArcRef *r); Struct::method(@ArcRef *r); // --- Example 2 (Proposal 1, today's behavior): let r: ArcRef<Struct>; r.method(); r.method(); // desugar to Struct::method(r); Struct::method(r); // oops moved out // --- Example 2 (Proposal 2): let r: ArcRef<Struct>; r.method(); r.method(); // desugar to Struct::method(@ArcRef *r); Struct::method(@ArcRef *r); ``` Benno: I dislike that Example 1 works but example 2P1 doesn't. ```rust! // Example 3 (Proposal 2): let r: &own Struct; r.method(); // desugars to: Struct::method(&own *r); // important: don't shorten lifetime ``` ```rust fn identity<'a>(r: &'a own Struct) -> &'a own Struct { &own *r } fn not_allowed<'a>(r: &'a own Struct) -> &'a mut Struct { &mut *r } ``` Nadrieril: is there an issue with reborrowing all the time? Benno: Yes, in the example above with `ArcRef`, we have one unnecessary increment/decrement of the refcount ```rust // --- Example 2 (Proposal 3): let r: ArcRef<Struct>; r.method(); r.method(); // desugar to Struct::method(@ArcRef *r); Struct::method(r); ``` When does the `<Struct as Drop>::drop` get run? In proposal 2 it will be at the end of the code example. In proposal 3 it will be at the end of `method` (if no further increments happen). ```rust // --- Example 2 (Proposal 4): let r: ArcRef<Struct>; r.use.method(); r.method(); // desugar to Struct::method(r.use); Struct::method(r); ``` Proposal 5: in `PlaceBorrow::borrow`, borrowck tells us whether this is the last use of the place or not, and if so we can avoid incrementing the ref count? Benno: do we even want to avoid the increment? Nadri: It feels important because of the drop timing. ```rust // Example 4: let s: Struct; fn Struct::method(self); s.method(); s.method(); // desugar to Struct::method(s); Struct::method(s); //~ ERROR: moved out ``` Nadri: argument against inserting reborrows when the types match: ```rust! trait Trait { fn method(self); } // Arguably these two should behave the same: fn foo<T: Trait>(x: T) { x.method() } fn foo_for_arcref<ArcRef<T>: Trait>(x: ArcRef<T>) { x.method() } ``` All: agreed Nadri: I'd propose that method resolution does Proposal 1 (i.e. don't reborrow if the types match). Possibly another feature could then turn that into the same output as Proposal 3, but that would be a separate "ergonomic refcount" thing Benno: then the proposed autoref algorithm should walk back up the deref chain Algorithm: > Let `p` be a place expression of type `T`, and assume we want to typecheck `p.method()`. We first compute the list `L := [T, T::Target, T::Target::Target, ..]` as long as the types implement `HasPlace`. > > For each type `U` in `L`, we look through all the `impl U` and `impl Trait for U` for a method with the right name. This gives us a list of “method candidates”. If there is an inherent method, pick that, otherwise if there are multiple traits, error. If there are none, proceed with the next type from the list `L`. > > Assume the method takes `fn method(self: X, ..)`. > - If `X` is one of the candidate types above, let `q := ***p` be p suitably derefed to get to that candidate type; we then desugar to `U::method(q)`/`<U as Trait>::method(q)`. > - If `X` does not occur in the candidates, then `X: HasPlace` must be true. Let `q := ***p` be p suitably derefed to get to `X::Target`. We then desugar to `U::method(@X q)`/`<U as Trait>::method(@X q)`. > - If `X::Target` is not one of the candidate types, we go back and pick another method. The last case can happen e.g for `p: &Arc<T>` and `fn method(self: &Rc<Self>)`. Benno: we should have a syntax for "call this inherent method" written as `<Type as Self>::method`. For the case when `Receiver::Target == HasPlace::Target` does not hold in general we only need to change the first step of constructing the list: > Let `p` be a place expression of type `T`, and assume we want to typecheck `p.method()`. We first compute the list `L := flatten [[T, <T as Receiver>::Target, ...], [<T as HasPlace>::Target, <<T as HasPlace>::Target as Receiver>::Target, ...], ...]` as long as the types implement `HasPlace` and/or `Receiver`. Explanation: makes no sense to check `T::ReceiverTarget::HasPlaceTarget`. Nadri: aren't there cases where we look ahead to different candidate types to find ambiguities? Ding: yes, with raw pointers as Receiver, but the check is arbitrary. See [rustc_hir_typeck Code](https://github.com/rust-lang/rust/blob/b115ea2e6a07e440e7cc18b08644fc0a2265391b/compiler/rustc_hir_typeck/src/method/probe.rs#L1389-L1474). I should ask tmandry if we could keep the feature unstable for now. All: that's weird Benno: `HasPlace::Target` both changes the target type and the impl blocks under consideration. `Receiver::Target` only changes the impl blocks. ```rust struct WeirdRef<A, B>(..); impl HasPlace for WeirdRef<A, B> { type Target = A; } impl Receiver for WeirdRef<A, B> { type Target = B; } impl Struct { fn method(self: Self); } impl Struct2 { fn method<T>(self: WeirdRef<T, Struct2>); } let x: WeirdRef<Struct, Struct2> = ...; fn receiver_takes_precedence() { x.method(); // desugars to Struct2::method::<Struct>(x); } fn explicitly_calling_other() { (*x).method(); // desugars to Struct::method(*x); } fn generic<T>() { let x: WeirdRef<Struct, T> = ...; x.method(); // desugars to Struct::method(*x); // problem: if we had instantiated `T = Struct2`, we would have selected a different method } ``` Benno: the problem in `generic` already happens with inherent methods vs trait methods: ```rust impl Trait for Struct { fn method(&self) { println!("Trait"); } } impl Struct { fn method(&self) { println!("inherent"); } } fn generic<T: Trait>(x: &T) { x.method(); // will select the trait method } let x: &Struct; x.method(); // will select inherent method ``` Nadri: what more weird things can we do if the targets differ ```rust! struct ReceiverOnly<T>(..); // !HasPlace struct RawPointer<T>(..); // !Receiver impl Struct { fn method(self: ReceiverOnly<Self>); } let x: &RawPointer<ReceiverOnly<Struct>>; x.method(); // desugars to: Struct::method(**x) ``` Nadri: this is maybe weird: we first try `<&_ as Receiver>::Target` then `<&_ as HasPlace>::Target` and they're somehow considered differently. Not sure if this is useful to think about. Nadri: side-point: importantly PlaceBorrow does not influence method resolution. we first pick a method, then desugar, then typecheck the desugaring. Benno: ```rust struct Ref<T>(T); // HasPlace<Target = T> + !Receiver struct Container<T>(T); // Receiver<Target = T> + !HasPlace impl Struct { fn method<T>(self: OtherRef<Self>); } let x: Ref<Container<Struct>>; x.method(); ``` Nadri: example where we trigger the last line of the algorithm (the "try again if the candidate is not in the list" line) ```rust! struct MyRef<T>(..); // HasPlace and Receiver impl MyRef<T> { fn method(self: &Rc<Self>); } impl Struct { fn method(self); } let x: &Arc<MyRef<Struct>> = ...; // note this is Arc, not Rc x.method(); // desugars to Struct::method(***x); ``` Benno: [basically same example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=9db43eb91d0102b35b709ccbcd280189) ```rust use std::{ops::Deref, rc::Rc, sync::Arc}; struct Foo; impl Foo { fn foo(self: Rc<Self>) {} } impl Deref for Foo { type Target = Bar; fn deref(&self) -> &Self::Target { &Bar } } struct Bar; impl Bar { fn foo(&self) {} } fn foo(p: Arc<Foo>) { p.foo(); // selects method on `Bar` } ``` ## Unifying `Target` types between `Receiver` and `HasPlace` All: HasPlace::Target and Deref::Target should coincide. Probably `trait Deref: HasPlace` Nadri: argument for wanting HasPlace + !Receiver: this limits inherent methods on the type. Argument for Receiver + !HasPlace: maybe autoderef attempts would give weird error messages? Question for the future: - Should `Receiver` and `HasPlace` have matching `Target` types? If yes, the relationship we want between them should probably be `Receiver: HasPlace`. - Gather examples for when `Receiver::Target` and `HasPlace::Target` should differ. - Gather examples for when a type is `Receiver + !HasPlace`, if none are found, use the super-trait relation ship.