Custom references
Custom references refers to a bundle of features that allow user-defined types to emulate and extend the capabilities of Rust references:
Resources
Field representing types (FRTs)
Note
This article contains parts of the current approach.
Field representing types, abbreviated FRTs, are a feature that allows writing code that is generic over the fields of structs, enums, tuples and variants of enums. They offer a limited form of reflection, as Rust code can inspect the fields of its own types.
Motivation
The most important application of FRTs is Field Projections. There they are one primitive way to construct Projections. FRTs can also be used by normal functions that need to be generic over fields, but do not fit into the field projection framework.
Naming FRTs
FRTs are named using the field_of! macro. They are available for structs,
unions, tuples and variants of enums:
#![allow(unused)]
fn main() {
use std::field::field_of;
struct MyStruct {
a: i32,
b: u32,
}
type A = field_of!(MyStruct, a);
type B = field_of!(MyStruct, b);
union MyUnion {
c: i32,
d: u32,
}
type C = field_of!(MyUnion, c);
type D = field_of!(MyUnion, d);
type E = field_of!((i32, u32), 0);
type F = field_of!((i32, u32), 1);
enum MyEnum {
Var1 { g: i32, h: u32 },
Var2(i32, u32),
}
type G = field_of!(MyEnum, Var1.g);
type H = field_of!(MyEnum, Var1.h);
type I = field_of!(MyEnum, Var2.0);
type J = field_of!(MyEnum, Var2.1);
}
An FRT is visible when the field it represents is visible. In particular, accessing the FRT of a private field from another module results in an error:
#![allow(unused)]
fn main() {
mod inner {
pub struct MyStruct {
a: i32,
pub b: i32,
}
}
type A = field_of!(inner::MyStruct, a); //~ ERROR: field `a` of struct `MyStruct` is private
type B = field_of!(inner::MyStruct, b);
}
The Field trait
FRTs implement the Field trait, which exposes information about the field
that they represent:
#![allow(unused)]
fn main() {
pub unsafe trait Field: Sized {
/// The type of the base where this field exists in.
type Base;
/// The type of the field.
type Type;
/// The offset of the field in bytes.
const OFFSET: usize;
}
}
Note that this trait cannot be implemented manually, so only FRTs implement it.
For example, considering the following type definitions from above:
#![allow(unused)]
fn main() {
struct MyStruct {
a: i32,
b: u32,
}
union MyUnion {
c: i32,
d: u32,
}
enum MyEnum {
Var1 { g: i32, h: u32 },
Var2(i32, u32),
}
}
We have the following:
field_of!(MyStruct, a)has:Base = MyStruct,Type = i32,OFFSET = offset_of!(MyStruct, a).
field_of!(MyUnion, c)has:Base = MyUnion,Type = i32,OFFSET = 0.
field_of!(MyEnum, Var1.g)has:Base = MyEnum,Type = i32,OFFSET = offset_of!(MyEnum, Var1.g).
Using FRTs
FRTs are usually used by APIs that wish to make an operation generically
available for each field of a struct, union, tuple or enum variant. To do so,
the API should introduce a generic parameter that implements the Field trait.
Since the trait cannot be implemented by non-FRTs, it ensures that only real
fields are allowed.
#![allow(unused)]
fn main() {
pub struct VolatileMut<'a, T: Copy> {
ptr: *mut T,
_phantom: PhantomData<&'a mut T>,
}
impl<'a, T: Copy> VolatileMut<'a, T> {
pub fn read_field<F: Field<Base = T>>(&self) -> F::Type {
let ptr = self.ptr.offset();
let ptr = unsafe { ptr.byte_add(F::OFFSET) };
let ptr = ptr.cast::<F::Type>();
unsafe { ptr.read_volatile() }
}
pub fn write_field<F: Field<Base = T>>(&mut self, value: F::Type) {
let ptr = self.ptr.offset();
let ptr = unsafe { ptr.byte_add(F::OFFSET) };
let ptr = ptr.cast::<F::Type>();
unsafe { ptr.write_volatile(value) };
}
}
}
Unresolved questions
- FRTs of structs, unions and tuples always exist in the type, but fields of enums are not necessarily accessible, as the value might not be of that variant. Probably need a separate trait to identify fields that always exist.
Projections
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Backlinks
HasPlace proposal
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
Truly First-Class Custom Smart Pointers | Nadri’s musings
Backlinks
Field projections
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
Project goal tracking issue, Fall 2025, contains many comprehensive updates
Design meeting 2025-08-13: Field projections - HackMD, Aug 2025
pre-RFC Field Projections v3 - HackMD, May 2025 or earlier
Backlinks
Place expression desugaring
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Backlinks
Reborrow
Reborrowing is an action performed on exclusive references which creates a copy of the source reference and marks it disabled for reads and writes. This retains the exclusivity of the reference despite creating a copy, as only one copy can be used at a time.
Today, true reborrowing is only available to Rust’s exclusive &mut T
references. Going beyond references means enabling true reborrowing for
user-defined exclusive reference types by defining a Reborrow trait.
We want to make it possible for both of the following functions to compile: an
exclusive &mut T reference and a user-defined custom exclusive reference
CustomMut<'_, u32> should have equivalent semantics.
Example:
#![allow(unused)]
fn main() {
fn f(a: &mut u32) {
f_x(a);
f_y(a);
}
fn g(a: CustomMut<'_, u32>) {
g_x(a);
g_y(a);
}
}
Use cases
Approaches
The current approach to reborrowing in user-land is based on an explicit method. The current work in the Reborrow traits lang experiment is based on a marker trait.
CoerceShared
Exclusive references call for a shared counterpart, into which an exclusive
reference can be coerced into. For Rust’s references, this is &T. For
user-defined exclusive reference types, a shared counterpart is a second
user-defined type that is freely shareable (read: is Copy). Coercing a
user-defined exclusive reference into a shared reference type requires defining
a CoerceShared trait.
Resources
Tracking Issue for Reborrow trait lang experiment · Issue #145612 · rust-lang/rust
Reborrow traits - Rust Project Goals, Jul 2025
rfcs/text/0000-autoreborrow-traits.md at autoreborrow-traits · aapoalas/rfcs, May 2025
Abusing reborrowing for fun, profit, and a safepoint garbage collector (conference talk with examples), Feb 2025
Backlinks
User-defined exclusive references
In some cases, users want to define their own custom reference types that have
equivalent semantics to Rust’s exclusive &mut T references but cannot, for
whatever reason, be directly expressible using them. For example, an exclusive
reference to unaligned data or an exclusive reference to a part of a matrix
could not be expressed using &mut T references. In other cases, the
exclusivity of the reference may not be a guarantee but more of a suggestion:
eg. for mutable C++ references it may be a good idea to try use them as
exclusive, but exclusivity is not guaranteed and thus using &mut T instead of
a custom type would cause undefined behaviour.
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct CustomMut<'a, T>(*mut T, PhantomData<&'a mut ()>);
}
Backlinks
Wrapper types
Currently Rust does not automatically reborrow Option<&mut T> or similar
wrapper types. Conceptually, there is no reason why &mut T should be
reborrowable but Option<&mut T> should not: the only difference between the
two types is that one can also be null.
With the Reborrow trait, reborrowing wrapper types of exclusive references becomes possible using blanket implementations.
#![allow(unused)]
fn main() {
impl<T: Reborrow> for Option<T> { /* ... */ }
}
Backlinks
Marker types
Sometimes users want to define custom types with exclusive reference semantics that do not contain any pointers at all. This is useful in encoding exclusive access to data in the function’s indirect context. For example, embedded systems sometimes use reborrowable ZST marker types to pass exclusive access to hardware peripherals through their call stacks. The author uses a marker ZST to model garbage collection safepoints, ensuring that unrooted custom references to garbage collectable data in a system with a moving GC are not held past safepoints.
#![allow(unused)]
fn main() {
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
}
Backlinks
Exclusive reference collections
In some cases it can be useful to group up multiple exclusive references into a single collection.
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct MutCollection<'a, 'b, 'c> {
a: &'a mut A,
b: CustomMut<'b, B>,
c: Option<&'c mut C>,
}
}
Reborrowing such a collection as exclusive means simply reborrowing each exclusive reference individually and producing a new collection of the results. This can also be applied recursively:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct BiggerCollection<'a, 'b, 'c, 'd, 'e, 'f, 'g> {
one: MutCollection<'a, 'b, 'c>,
two: MutCollection<'d, 'e, 'f>,
three: CustomMut<'g, G>,
}
}
Backlinks
Marker trait approach
Note
This article contains parts of the current approach.
The current Reborrow traits lang experiment aims to produce a method-less
Reborrow trait. For exclusive reference semantics this is mostly trivial: the
concrete code generated for an exclusive reborrow is equivalent to that of
Copy, and only additional lifetime semantics must be considered on top.
This gives us a trivial, derivable Reborrow trait:
#![allow(unused)]
fn main() {
trait Reborrow {}
}
Its usage is performed through a derive macro:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
}
There are some limitations that we wish to impose on types that derive (or implement) this trait.
- The type must not be
CloneorCopy.
- Alternatively, blanket-implement
Reborrow for T: Copy, butClone + !Copytypes cannot beReborrow. - This limitation is placed to avoid types that are both
CopyandReborrow, as that only makes sense ifCopytypes are considered a “base case” for recursive reborrowing. Clone + !Copytypes generally only make sense if a correspondingDropimplementation exists, and reborrowing cannot be soundly combined withDrop. If a type isClone + !Copy + !Dropthen technically it can be soundly reborrowed, but it’s unlikely that the type itself makes overmuch sense (it is by definition a cloneable, ie. shareable, type with exclusive reference semantics).
- The type must not have a
Dropimplementation.
- Reborrowable types can have multiple owned copies in existence, and each one
of these would need to call
Drop(as we cannot know if we have the only copy or not). This is effectively guaranteed to end up causing a double-free.
- The type must have at least one lifetime.
- Alternatively, if
Copytypes blanket-implementReborrowthen this limitation cannot be placed, but manually derivingReborrowon a lifetime-less type should be an error. - This limitation is placed simply because a lifetime-less type cannot contain the lifetime information that sound reborrowing relies upon.
- The result of a
Reborrowoperation is simply the type itself, including the same lifetime.
- Reborrowing should produce a copy, not eg. a field or a new struct consisting of a subset of the type’s fields.
- If
Reborrowreturns a lifetime that is always shorter than the source lifetime, then values deriving from the operation cannot be returned past it up the call stack. A longer lifetime is of course meaningless. Thus, the same lifetime is what we should get.
Derivation of the Reborrow operation
Since the marker trait approach has no explicit fn reborrow method, the
compiler must derive the correct operation for Reborrow. When exactly one
lifetime exists on the type, that derivation is trivial: it is simply a Copy
and an exclusive reborrow of the singular lifetime. Though, this too can be
questionable:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Bad<'a>(&'a ());
}
Is the above type actually an exclusive reference? Is deriving Reborrow on it
an error? I think it should be, but it’s a little hard to exactly say why:
arguably it’s because reborrowing (re)asserts exclusive access while this type,
by definition, does not have exclusive access to anywhere. But maybe this is
created using a &mut () in which case it sort of does carry exclusive access,
it just cannot re-assert it.
If there are multiple lifetimes, then deriving Reborrow becomes more
complicated. Consider the following type:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Two<'a, 'b>(&'a (), &'b mut ());
}
This type should only exclusively reborrow the second reference.
These questions become a step more complicated once we give up on using Rust references and go into the world of custom types:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct MaybeBad<'a>(PhantomData<&'a ()>);
}
This type may or may not be bad: we simply have no idea. Whether the borrow
inside of PhantomData is &mut () or &() has no effect on whether or not
this type carries an “exclusive lifetime” or not.
This issue seems to call for a new kind of marker type:
#![allow(unused)]
fn main() {
/// Marker type for exclusive reference semantics.
#[derive(Reborrow)]
struct PhantomExclusive<'a>; // explicitly no content; this is purposefully bivariant.
impl<'a> PhantomExclusive<'a> {
/// Capture an exclusive reference into a PhantomExclusive.
fn from_mut<T>(_: &'a mut T) -> Self {
Self
}
/// Create a new unbound PhantomExclusive.
///
/// # Safety
///
/// * The caller must ensure that only one PhantomExclusive is created for
/// whatever that they're tracking.
unsafe new() -> Self {
Self
}
}
}
The compiler would track PhantomExclusive as an exclusive reference without
the pointer bits. Our custom type would then be:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Good<'a>(PhantomData<&'a ()>, PhantomExclusive<'a>);
}
The first PhantomData is there to establish variance while the
PhantomExclusive is included to ensure 'a is an “exclusive lifetime”.
Recursive nature of deriving Reborrow
The derivation of Reborrow (and CoerceShared) has a recursive nature: we can
group up individual exclusive references (be they &mut T or CustomMut<'_, T>
or CustomMarker<'_>) into a single struct and derive Reborrow on it. This
derivation is done on the fields of the type, and when a field is found to be
Reborrow then that field’s reborrow operation becomes a part of the larger
whole’s operation.
This has some complexities regarding what are the “bottom/base cases” of the recursion.
&'a mut Tbottoms out and performs a reborrow on'aCustomMut<'a, T>can be assumed to bottom out and reborrow on'a.Two<'a, 'b>needs to be checked: does it reborrow both'aand'bor only one of them?PhantomData<&'a ()>bottoms out and DOES NOT perform a reborrow on'a: this is aCopymarker.PhantomExclusive<'a>bottoms out and performs a reborrow on'a.
Backlinks
Method-based approach
Warning
This article contains an older approach that has been abandoned.
Today, exclusive references can be implemented in user-land using a method-based approach:
#![allow(unused)]
fn main() {
trait Reborrow {
fn reborrow(&mut self) -> Self;
}
}
This captures the most important features of reborrowing: a source instance
self has exclusive access asserted on it, and a new Self is produced from it
(Some formalisations allow the method to choose its own result type using a
Self::Target ADT: this is arguably mixing up reborrowing with a generalised
Deref operation). However, this approach comes with downsides: the method
requires an explicit &mut self reference which bounds the resulting Self’s
lifetime to the calling function, and the method is user-overridable which leads
to arguably non-idiomatic “spooky action” and a possibility of misuse.
Bounded lifetimes
When the fn reborrow method is called in some outer function fn outer_fn,
The outer function must create a &mut T reference pointing to the value being
reborrowed:
#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: CustomMut<'a, u32>) -> &'a u32 {
// This:
inner_fn(t.reborrow());
// ... is equivalent to this:
let t_mut = &mut t;
inner_fn(t_mut.reborrow())
}
}
This means that the fn reborrow method is given a reference pointing to a
local value, effectively a pointer onto the stack. The compiler must make sure
that this pointer does not outlive the stack, which then means that the lifetime
of the resulting Self created by fn reborrow cannot outlive the function in
which it was created in. In the above example, this means that trying to return
the result of inner_fn will not compile because of the fn reborrow call,
citing “returns a value referencing data owned by the current function”.
Compare this to Rust references: the compiler understands that the result of
reborrowing a &mut produces a new reference that can be extended to the
original reference’s lifetime. This function compiles despite an explicit
reborrow being performed by the &mut *t code.
#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: &'a mut u32) -> &'a u32 {
inner_fn(&mut *t)
}
}
We can make the code not compile by explicitly creating a &mut t reference:
#![allow(unused)]
fn main() {
fn outer_fn<'a>(mut t: &'a mut u32) -> &'a u32 {
// no longer compiles: returns a value referencing data owned by the current
// function
inner_fn(&mut t)
}
}
In user-land code bases that use the explicit fn reborrow method, there exists
a way to fix this issue: by simply removing the fn reborrow method call, the
original example code will compile. But knowing of this fix requires some deeper
understanding of the fn reborrow method and the borrow checker: it is not an
obvious or clean fix.
Most importantly, if the Rust compiler is in charge of automatically injecting
fn reborrow calls at appropriate use-sites, then it may not be feasible for
the compiler to perform the analysis to determine if a particular call should be
removed or not. Furthermore, in a post-Polonius borrow checker world it will
become possible for code like this to compile:
#![allow(unused)]
fn main() {
fn inner_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
// ...
}
fn outer_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
let result = inner_fn(t)?;
if result > 100 {
Ok(t)
} else {
Err(t)
}
}
}
This means that a reborrow of t must always happen, yet the lifetime expansion
of the result of inner_fn depends on whether the function returns Ok or
Err. In the Err branch the result’s lifetime must expand to that of the
source t, but in the Ok branch it must shrink to re-enable usage of the
source t. The method-based approach to Reborrow traits will not work here, as
the compiler cannot choose to call the method based on a result value that
depends on the call’s result.
One could argue that the compiler could simply extend the lifetime of the method call’s result, as it should only do good deeds. This may well open a soundness hole that allows safe Rust code to perform use-after-free with stack references.
User-controlled code
The second downside of the method-based approach is that reborrowing is fully invisible in the source code, and having user-controlled code appear where there is none visible is not very idiomatic Rust.
Consider eg. the following code:
struct Bad;
impl Reborrow for Bad {
fn reborrow(&mut self) -> Self {
println!("I'm bad!");
Self
}
}
fn main() {
let bad = Bad;
let bad = bad;
}
Depending on the exact implementation choices, this might print out the “I’m
bad” message. This is especially problematic if the compiler chooses to trust
that fn reborrow methods never store their &mut self reference given to them
and allows lifetime extension to happen:
#![allow(unused)]
fn main() {
struct Unsound<'a>(&'a mut &'a u32, u32);
// Note: lifetime for trait here to show that the compiler specially trusts that
// these lifetimes indeed are fully unifiable.
impl<'a> Reborrow<'a> for Unsound<'a> {
fn reborrow(&'a mut self) -> Self {
let data = &self.1;
self.0 = data;
Self(self.0, self.1)
}
}
}
This would absolutely not compile today, but if the compiler did truly trust that reborrow methods can do no wrong then something like this might just pass through the compiler’s intentional blindspot and become a soundness hole.
Backlinks
CoerceShared
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
If exclusive references exist, then shared references are nearly always
necessary as well. Rust’s own exclusive &mut T references automatically coerce
into shared &T references as necessary, and we want to enable this same
coercion for custom user-defined exclusive references as well. For this purpose
we define a CoerceShared trait.
Use cases
To be fleshed out. All the same cases apply as for Reborrow.
Note that some custom reference cases might carry extra metadata (eg. a usize)
for exclusive references which then gets dropped out when coercing to shared.
Approaches
The current approach to shared coercion in user-land is based on an explicit method. The current work in the Reborrow traits lang experiment is based on a marker trait.
Backlinks
Marker trait approach
Note
This article contains parts of the current approach.
Coercing a custom user-defined exclusive reference into a user-defined shared
reference is a slightly involved affair: the two types are obviously going to be
different types, just like &mut T and &T are different, but they must also
be similar enough that a pure marker trait can relate the types to one another.
From a user’s perspective, coercing an exclusive reference into shared is a simple operation: just name the source exclusive reference type and the target shared reference type.
This gives us the following trait definition:
#![allow(unused)]
fn main() {
trait CoerceShared<Target: Copy>: Reborrow {}
}
Its usage is done either manually through an impl Trait statement, or possibly
by deriving CoerceShared on the source exclusive reference type:
#![allow(unused)]
fn main() {
// impl Trait statement
#[derive(Reborrow)]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
struct CustomMarkerRef<'a>(PhantomData<&'a ()>);
impl<'a> CoerceShared<CustomMarkerRef<'a>> for CustomMarker<'a> {}
// derive macro
#[derive(Reborrow, CoerceShared(CustomMarkerRef))]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
struct CustomMarkerRef<'a>(PhantomData<&'a ()>);
}
As with the Reborrow marker trait, some limitations are placed on the trait although this time most of them are expressed on the trait directly.
- The type implementing
CoerceSharedmust also implementReborrow.
- Coercing an exclusive reference into a shared reference doesn’t make sense if the source type is not an exclusive reference.
- The result of the
CoerceSharedoperation must be aCopytype.
CoerceSharedcan be performed any number of times on the same value, always producing a byte-for-byte copy (ignoring any padding bytes). These results are thus copies of one another, so it must also be possible to performCoerceSharedonce and produce copies of that result.
- The result of the
CoerceSharedoperation must have at least one lifetime.
- A lifetime-less type cannot contain the lifetime information that sound reborrowing relies upon.
- The lifetime of the result must be equivalent to the source.
- If
CoerceSharedreturns a lifetime that is always shorter than the source lifetime, then values deriving from the operation cannot be returned past it up the call stack. A longer lifetime is of course meaningless. Thus, the same lifetime is what we should get.
- The target type must be relatable to the source type field-by-field.
- In order for the marker trait to be derivable by the compiler, its contents must be dericable from the source and target types. This is most reasonably performed field-by-field.
For exclusive reference types that have at most one data field and exactly one
lifetime, coercing into a shared reference type that has the same data field and
exactly one lifetime, the derivation of CoerceShared is trivial. For types
that have multiple fields and/or multiple lifetimes, the derivation becomes more
complicated.
Backlinks
Method-based approach
Warning
This article contains an older approach that has been abandoned.
Today, coercing exclusive references to shared references can be implemented in user-land using a method-based approach:
#![allow(unused)]
fn main() {
trait CoerceShared {
type Target: Copy;
fn coerce_shared(&self) -> Self::Target;
}
}
This approach suffers from the downsides as method-based Reborrow
does. In addition, it is not possible to fix the
lifetime issues by simply not calling the fn coerce_shared method as that
would mean trying to use a Self type where Self::Target is required.
The way to fix this is to define an Into<Self::Target> method that consumes
the source exclusive reference and produces a shared reference with the same
lifetime as a result. Then, instead of calling the fn coerce_shared method the
fn into method is called instead.
Associated type or type argument
In general, there is no reason for why an associated type would be preferable
versus a type argument in the CoerceShared trait: especially with exclusive
reference collections it might make sense for a single reborrowable type to have
multiple CoerceShared targets. If the compiler automatically injects the
correct fn coerce_shared method call, then an associated type becomes
preferable.
The problem is that if the requirements for implementing CoerceShared are not
strict enough and a type argument is used, then the trait could become a vehicle
for generic automatic value coercion. For example:
#![allow(unused)]
fn main() {
struct Int(i64);
impl CoerceShared<i64> for Int {
fn coerce_shared(&self) -> i64 {
self.0
}
}
impl CoerceShared<u64> for Int {
fn coerce_shared(&self) -> u64 {
self.0 as u64
}
}
impl CoerceShared<i32> for Int {
fn coerce_shared(&self) -> i32 {
self.0 as i32
}
}
impl CoerceShared<u32> for Int {
fn coerce_shared(&self) -> u32 {
self.0 as u32
}
}
// ... and so on ...
}
Backlinks
Receiver
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
The Receiver trait enables “arbitrary self types” by doing two things:
- Defining when a smart pointer type is allowed to be a method receiver.
- Generalizing method receivers past types that implement
Deref.
Resources
3519-arbitrary-self-types-v2 - The Rust RFC Book
- Tracking issue for RFC 3519:
arbitrary_self_types· Issue #44874 · rust-lang/rust - Arbitrary self types v2: stabilize by adetaylor · Pull Request #135881 · rust-lang/rust
Backlinks
Autoref
Note
This article contains parts of the current approach.
Autoref is the technical term used to describe the insertion of automatic borrowing of variables to calling methods. For example:
struct MyStruct;
impl MyStruct {
fn method(&self) {}
}
fn main() {
let my_struct: MyStruct = MyStruct;
my_struct.method();
// this works, because it is desugared into:
MyStruct::methods(&my_struct);
}
Going beyond references means adding support for autoref to custom types. The HasPlace proposal provides a way to borrow places, which we will explicitly use in this section. However, using that specific mechanism for borrowing is not required; autoref can work with other approaches as well.
Receiver::Target and HasPlace::Target
The HasPlace trait has an associated type called Target, which is the type of the place when dereferencing Self. A priori it is a different type from the associated type Target on the Receiver trait, which is responsible for allowing a type in the receiver position of a method. We have not yet settled the question on the relationship between the two Target types. The options are:
- Merge the
HasPlaceandReceivertraits. - Make
Receivera supertrait ofHasPlace. - Make
HasPlacea supertrait ofReceiver. - Keep them separate.
Options 1 and 2 are not a good idea, because implementing Receiver prevents a type from introducing inherent methods without breaking downstream users. For this reason we only consider options 3 and 4.
Option 3 could result in error messages that are confusing, since implementing HasPlace makes *p a valid expression (for p: Self ). However, any operation on *p (such as reading, writing, and borrowing) require additional traits to be implemented. If none are implemented, it could be strange to allow *p in the first place.
Option 4 has the disadvantage of making the model more complex; there are two Target types that one has to keep track of when a type implements both differently. Unless we discover a use-case for the diverging types, we will probably choose option 3.
A note on Deref
The discussions surrounding Receiver also mention Deref and there was a plan to add a supertrait relationship Deref: Receiver. HasPlace essentially supersedes Deref, which therefore takes it out of this question. We would like to make Deref: HasPlace, but that depends on the exact shape of HasPlace and the interaction with DerefMut.
Algorithm
An important idea behind this algorithm is that we make method resolution only dependent on Receiver and HasPlace. PlaceBorrow makes an appearance later, but does not drive method resolution. So we first compute the resolution algorithm and then check later if any place operations that we would need to perform are legal. If they aren’t, we error at that stage and do not go back to change the method we selected.
The algorithm gets invoked on all method calls. They are generally of the shape p.method() where p is a place expression. The method call can of course also have arguments, but they are ignored in the algorithm.
We first constructs a list of candidate types. This depends on whether the Target types of HasPlace and Receiver are unified or not.
- If they are unified, we compute the list
L := [T, T::Target, T::Target::Target, ...]. The computation of this list is described by the following code snippet:#![allow(unused)] fn main() { iter::successors(Some(T), |ty| { if ty.implements_has_place() { Some(ty.has_place_target()) } else { None } }) } - If they are separate, we compute the list
The computation of this list is described by this code snippet:L := flatten [ [ T, <T as Receiver>::Target, <<T as Receiver>::Target as Receiver>::Target, ... ], [ <T as HasPlace>::Target, <<T as HasPlace>::Target as Receiver>::Target, <<<T as HasPlace>::Target as Receiver>::Target as Receiver>::Target, ... ], [ <<T as HasPlace>::Target as HasPlace>::Target, <<<T as HasPlace>::Target as HasPlace>::Target as Receiver>::Target, <<<<T as HasPlace>::Target as HasPlace>::Target as Receiver>::Target as Receiver>::Target, ... ], ... ]#![allow(unused)] fn main() { iter::successors(Some(T), |ty| { if ty.implements_has_place() { Some(ty.has_place_target()) } else { None } }) .flat_map(|ty| iter::successors(Some(ty), |ty| { if ty.implements_receiver() { Some(ty.receiver_target()) } else { None } })) }
The second step in the algorithm is to iterate over the list of candidate types. Let U be the type that we are considering. We look through all impl blocks of the shape impl U and impl Trait for U (including generic ones such as impl<V> Trait for V where V can be substituted by U). This gives us a set of method candidates. If there is an inherent method, we pick that and continue with the next step. If there is a single trait method, we pick that. If there are multiple trait methods, we fail with an ambiguity error. If there are none, we proceed with the next element in the type candidate list.
The third step inspects the method, which has a general shape of fn method(self: X) again with function arguments omitted. Now we inspect X:
- If
Xoccurs in the candidate list that we walked to arrive at this method, we letq := *...*pbe suitably derefed to get toX, which is the number ofHasPlace::Targetwe go through. We then desugar the method toU::method(q)or<U as Trait>::method(q). - If
Xdoes not occur in the already considered candidates thenX: HasPlacemust be true. If that’s not the case, we emit an error.- If
X::Targetoccurs in the already considered candidate, we then letq := *...*pbe suitably derefed to get toX::Target. We then desugar toU::method(@X q)or<U as Trait>::method(@X q). - If
X::Targetdoes not occur in the list of already considered candidates, then we continue with the nextimplor type from the candidate list.
- If
Note that an alternative that we should consider is to error in the last case.
Note
The current algorithm for method resolution in Rust includes a final step where it applies array unsized coercions. See here for more information.
In this algorithm, we could add the same coercions at the end of each
HasPlacechain. An alternative would be to implementDereffor arrays with their target being the correct slice.
Examples
Direct call
#![allow(unused)]
fn main() {
impl Example { fn method(self: Arc<Self>); }
let example: Arc<Example>;
example.method();
// desugars to:
Example::method(example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: Arc<Self>)- found
X = Arc<Example>in candidate list at index 0 => no derefs are added and no borrow takes place
- found
- found inherent
Calling method twice will in this case result in an error, since Arc: !Copy. This is the same behavior as today. Reborrowing will also not change this for Arc, since that would require running custom code.
Basic reborrow
#![allow(unused)]
fn main() {
impl Example { fn method(self: ArcRef<Self>); }
let example: Arc<Example>;
example.method();
// desugars to:
Example::method(@ArcRef *example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: ArcRef<Self>)X = ArcRef<Example>not found in the candidate list, butX: HasPlaceX::Target == Examplefound at index 1 in candidate list,- => one deref is added and borrow using
ArcRef
- => one deref is added and borrow using
- found inherent
In this example, calling method twice will result in no error, since @ArcRef creates a new reference and increments the refcount.
No nested borrows
#![allow(unused)]
fn main() {
impl Example { fn method(self: &ArcRef<Self>); }
impl Trait for Example { fn method(self: &Self); }
let example: Arc<Example>;
example.method();
// desugars to:
<Example as Trait>::method(&*example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: &ArcRef<Self>)X = &ArcRef<Example>not found in the candidate list, butX: HasPlaceX::Target == ArcRef<Example>not found in candidate list- => continue with next impl/type
- found trait
fn method(self: &Self)inTraitX = &Examplenot found in the candidate list, butX: HasPlaceX:: Target == Examplefound at index 1 in candidate list,- => one deref is added and borrow using
&
- => one deref is added and borrow using
- found inherent
This example illustrates that we cannot “go through” multiple HasPlace::Target types and borrow them. This is because we only have an Arc and no ArcRef in memory where we could take a & of.
No “looking ahead” in the candidate list for borrowing
This example only works when Receiver and HasPlace can have divergent Target types.
#![allow(unused)]
fn main() {
struct Weird<A, B>(...);
impl<A, B> HasPlace for Weird<A, B> { type Target = A; }
impl<A, B> Receiver for Weird<A, B> { type Target = B; }
impl<A, B, P: Projection<Source = A>>
PlaceBorrow<P, Weird<P::Target, B>> for &A { ... }
impl &Example { fn method(self: Weird<Example, Self>); }
let example: &Example;
example.method();
//~^ ERROR: no method `method` found for `&Example`
}
Algorithm computation. Candidates: [&Example, Example]
&Example- found inherent
fn method(self: Weird<Example, Self>)X = Weird<Example, &Example>not found in candidate list, butX: HasPlaceX::Target == Examplenot found in candidate list (we only check up to the point where we currently are at!)- => continue with next impl/type
- found inherent
Example- no impl block contains a
fn method(self: X)
- no impl block contains a
- Error, since the end of the list is reached.
Place wrapper
#![allow(unused)]
fn main() {
impl Example { fn method(self: Pin<&mut MaybeUninit<Self>>); }
struct Parent { example: Example }
let parent: Pin<Box<MaybeUninit<Parent>>>;
parent.example.method();
// desugars to:
Example::method(&pin mut (@%MaybeUninit (**parent).example));
}
Note
The place expression
parent.exampleis desugared to@%MaybeUninit (**parent).example, which has the typeMaybeUninit<Example>, see place expression desugaring. Place expressions are passed to the method resolution algorithm in their desugared form.
Algorithm computation. Candidates: [MaybeUninit<Example>, Example]
MaybeUninit<Example>- no impl block contains a
fn method(self: X)
- no impl block contains a
Example- found inherent
fn method(self: Pin<&mut MaybeUninit<Self>>)X = Pin<&mut MaybeUninit<Example>>not found in candidate list, butX: HasPlaceX::Target == MaybeUninit<Example>found in candidate list at index 0- => no derefs are added and borrow using
Pin<&mut MaybeUninit<Example>>
- => no derefs are added and borrow using
- found inherent
Deep deref
#![allow(unused)]
fn main() {
impl Example { fn method(self: &Self); }
let example: Box<Box<Box<Box<Example>>>>;
example.method();
// desugars to:
Example::method(&****example);
}
Algorithm computation. Candidates: [Box<Box<Box<Box<Example>>>>, Box<Box<Box<Example>>>, Box<Box<Example>>, Box<Example>, Example]
Box<Box<Box<Box<Example>>>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Box<Box<Example>>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Box<Example>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Example>- no impl block contains a
fn method(self: X)
- no impl block contains a
Example- found inherent
fn method(self: &Self)X = &Examplenot found in candidate list, butX: HasPlaceX::Target == Examplefound in candidate list at index 4- => 4 derefs are added and borrow using
&
- => 4 derefs are added and borrow using
- found inherent
Resources
- Autoref and next gen place traits | Meeting by Xiang, Nadri and Benno, Jan 2026
- Autoref and Autoderef for First-Class Smart Pointers | Nadri’s musings, Dec 2025
- Ensure
arbitrary_self_typesmethod resolution is forward-compatible with custom autoref · Issue #136987, Feb 2025- issue comment with design sketch (see “Summary sketch”)
Backlinks
In-place initialization
Initializing values in-place without copying them. This eliminates unnecessary copies and allows for self-referential datastructures.
Range of use cases
TODO: Cover the range of use cases like
- Pinned vs unpinned
- Constructor functions
- Fallible
Approaches
Potential design axioms
TODO: Add more or remove.
- Support abstraction through function boundaries
Resources
In-place initialization - Rust Project Goals, Fall 2025
#t-lang/in-place-init > in-place initialization: RfL design wishes - rust-lang - Zulip
Init expressions
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
Init expressions / In-place initialization, Jun 7 2025
Backlinks
Out pointers
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
In-place initialization via outptrs, Jul 8 2025
- Introduced
InPlace<T>as aBoxwith custom allocator. - Very influential.
Thoughts on “out”-pointer, Nov 12 2025
Backlinks
Placing functions
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
placing functions, July 2025
Placing Arguments, August 2025
Backlinks
Guaranteed value emplacement
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
RFC Draft: Guaranteed Value Emplacement - HackMD
Backlinks
Running Examples
This chapter regroups examples we discuss regularly:, so that we can be clear about the intended APIs etc.
ArcBox
An ArcBox is a Box backed with a reference-counted storage.
It supports field projections, on top of everything else that Box should have.
#![allow(unused)]
fn main() {
/// A `Box` backed with a reference-counted storage. Supports field projection.
// Safety: we're the unique pointer to this `T`.
// If any field is borrowed by an `ArcBoxMap`, then this value is no longer safe, obviously.
pub struct ArcBox<T>(Arc<T>);
/// A subplace of an `ArcBox`.
pub struct ArcBoxMap<T> {
/// Pointer to the reference counts. This keeps the backing storage allocated while any
/// `ArcBoxMap` to it exists.
header: NonNull<ArcHeader>,
/// Pointer to the subplace we care about.
val: NonNull<T>,
}
// Both support: read, write, move out, drop, borrow fields as `&`, `&mut`
// Both also support projecting to `ArcBoxMap`.
impl<T> ArcBox<T> {
pub fn new(x: T) -> Self { .. }
// Implemented trivially, since we can only get a full `self` if none of the fields are borrowed
// by an `ArcBoxMap`.
pub fn into_arc(self) -> Arc<T> { .. }
}
// `ArcBox` has no need for a `Drop` impl, it reuses the one from `Arc`.
// Drops the pointed-to value, decrements the refcount.
impl Drop for ArcBoxMap { .. }
}
Example usage:
#![allow(unused)]
fn main() {
let x: ArcBox<Foo> = ...;
let field = @ArcBoxMap x.field;
do_something(&mut x.other_field);
do_something(&mut field.whatever);
// at end of scope:
// - `field` drops normally;
// - dropck must drop `x.other_field`;
// - dropck must dispose of the empty `ArcBox` (that's not its normal `Drop`).
}
UniqueArc
UniqueArc is an Arc while it is known to be uniquely owned. Typically used for initialization,
after which it can be turned into a normal Arc.
#![allow(unused)]
fn main() {
// Safety: we're the unique pointer to this `T`.
// While this is live, the strong count is 0 so that we can give out other `Weak` pointers to this
// and prevent them from being upgraded.
pub struct UniqueArc<T>(Weak<T>);
// Supports: read, write, borrow fields as `&`, `&mut`
impl<T> UniqueArc<T> {
pub fn new(x: T) -> Self { .. }
pub fn downgrade(&self) -> Weak<T> { .. }
// This sets the strong count to 1.
pub fn into_arc(self) -> Arc<T> { .. }
}
}
ACP 700 proposes a way to field-project
a UniqueArc: first we change how strong counts work. Instead of just a strong count, we cram two
counters into the u64, one for shared refs and one for unique refs, and update the rest of the
logic accordingly. Can’t upgrade a weak pointer if there are any unique refs.
#![allow(unused)]
fn main() {
impl<T> UniqueArc<T> {
// Checks the unique-ptr count.
pub fn try_into_arc(self) -> Result<Arc<T>, NotUnique> { .. }
}
/// A subplace of a `UniqueArc`.
pub struct UniqueArcMap<T> {
/// Pointer to the reference counts.
header: NonNull<ArcHeader>,
/// Pointer to the subplace we care about.
val: NonNull<T>,
}
// Supports: read, write, borrow fields as `&`, `&mut`, reborrow fields as `UniqueArcMap`
}
The issue then is:
- If we don’t give
UniqueArcMapspecial borrowck behavior, then disjoint borrowing must be done with methods that split the borrow; that’s sad. - If we do give
UniqueArcMapspecial borrowck behavior, then we can prevent multiple@UniqueArcMap x.fieldto the same fields. However, we can’t get back aArc<T>anymore:
#![allow(unused)]
fn main() {
let x: UniqueArc<Foo> = ...;
let field = @UniqueArcMap x.field;
// can't access `x.field` anymore
// in particular, can't call a method on `x` itself, since that may access `x.field`:
let arc = x.try_into_arc()?; // ERROR `x.field` is borrowed
}
SharedMutableRef
This is a funky counter-example to how one might think borrows must chain: it’s a shared reference through which you can write.
Writing ability is justified by the !Send and !Sync impls: this can only be used on a single
thread.
#![allow(unused)]
fn main() {
struct SharedMutableRef<'a, T>(NonNull<T>, PhantomData<&'a mut T>);
impl !Send for SharedMutableRef<'a, T> {}
impl !Sync for SharedMutableRef<'a, T> {}
// Supports: local T -> SharedMutableRef<T> exclusive borrow
// Supports: &mut T -> SharedMutableRef<T> exclusive reborrow
// Supports: SharedMutableRef<T> -> SharedMutableRef<T> *shared* reborrow
// Supports: read, write
// Importantly doesn't support &T or &mut T reborrows
}
Example usage:
#![allow(unused)]
fn main() {
let mut x = 42;
let weird = @SharedMutableRef x;
let reborrow1 = @SharedMutableRef *weird;
let reborrow2 = @SharedMutableRef *weird; // look it's a shared ref
*reborrow1 += 1; // hehe
*reborrow2 += 1;
// `x` is inaccessible while any of the above refs are live
}