Skip to content

Proposal: Autoreborrow traits #339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/2025h2/autoreborrow-traits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Nightly support for Autoreborrow traits

| Metadata | |
|:-----------------|----------------------------------------------------------------------------------|
| Point of contact | @aapoalas |
| Teams | <!-- TEAMS WITH ASKS --> |
| Teams | <!-- TEAMS WITH ASKS --> |
| Task owners | <!-- TASK OWNERS --> |
| Status | Proposed |
| Tracking issue | |
| Zulip channel | N/A (an existing stream can be re-used or new streams can be created on request) |

## Summary

Bring up a language RFC for autoreborrow traits and land nightly support for the traits.

## Motivation

Reborrowing is an important feature of the Rust ecosystem, underpinning much of the borrow checker's work and
enabling ergonomic usage of references. The built-in, automatic handling of reborrows as a language feature
is, arguably, one of the many keys to Rust's success. Autoreborrowing is not available for user-space types
to take advantage of, which hinders their ergonomics and usage.

Autoreborrowing is a necessary feature for both niche use-cases around lifetime trickery, and for flagship
goals like Pin-ergonomics and the Rust for Linux project. As both of these are proceeding, a parallel track
to pre-empt reborrowing becoming a sticking point seems wise. Luckily, reborrowing is arguably a solved
problem that mostly needs a formal definition and a implementation choice to enter the language formally.

### The status quo

Today, when an `Option<&mut T>` or `Pin<&mut T>` is passed as a parameter, it becomes unavailable to the
caller because it gets moved out of. This makes sense insofar as `Option<T>` only being `Copy` if `T: Copy`,
which `&mut T` is not. But it makes no sense from the point of view of `&mut T` specifically: passing an
exclusive reference does not move the reference but instead reborrows it, allowing the `&mut T` to be reused
after the call finishes. Since an `Option<&mut T>` is simply a maybe-null `&mut T` it would make sense that
it would have the same semantics.

The lack of autoreborrowing is why this is not the case. This can be overcome by using the `.as_deref_mut()`
method but it suffers from lifetime issues when the callee returns a value derived from the `&mut T`: that
returned value cannot be returned again from the caller as its lifetime is bound to the `.as_deref_mut()`
call. For `Pin<&mut T>` this problem has been side-stepped by adding `Pin`-specific nightly support of
reborrowing pinned exclusive references.

But the same issue pops up again for any user-defined exclusive reference types, such as `Mut<'_, T>`. The
user can define this type as having exclusive semantics by not making the type `Copy`, but they cannot opt
into automatic reborrowing. The best they can hope is to implement a custom `.reborrow_mut()` method similar
to the `Option::as_deref_mut` from above. Here again they run into the issue that the lifetime of a
`Mut<'_, T>` always gets constrained to the `.reborrow_mut()` call, making it impossible to return
values derived from a `Mut<'_, T>` from the function that called `.reborrow_mut()`.

An improvement is needed.

### The next 6 months

- Bring up an RFC for autoreborrowing: a
[draft](https://github.com/aapoalas/rfcs/blob/autoreborrow-traits/text/0000-autoreborrow-traits.md) exists.
- Choose the internal logic of recursive reborrowing: based on core marker types, or on interaction with
`Copy`?
- Implement nightly support for non-recursive reborrowing.
- Gather feedback from users, especially `reborrow` crate users.
- Implement nightly support for recursive reborrowing.

### The "shiny future" we are working towards

`Pin` ergonomics group should be able to get rid of special-casing of `Pin` reborrowing in rustc.

Rust for Linux project should be enabled to experiment with custom reborrowable reference types in earnest.

Users of `reborrow` crate and similar should be enabled to move to core solution.

## Design axioms

- Accept the definition of reborrowing as "a memory copy with added lifetime analysis".
- This disallows running user code on reborrow.
- "Reborrow-as-shared" likely needs to run user code; this'd preferably be ignored where possible.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that? Doesn't your draft RFC describe a way to avoid running user code with the CoerceShared trait?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I had forgotten about that! :D

- Must achieve true reborrowing, not a fascimile.
- Current userland reborrowing uses `T::reborrow_mut` functions that achieve the most important part of
reborrowing, temporarily disabled `T` upon reborrow.
- Userland cannot achieve true reborrowing: true reborrowing does not constrain the lifetime of `T`,
whereas userland fascimile does.
- Difference is in whether values derived from a reborrow can be returned past the point of the reborrow.
- Performance of the solution must be trivial: reborrow is checked for at every coercion site. This cannot be
slow.
- Make sure autoreborrowing doesn't become a vehicle for implicit type coercion. Allowing autoreborrowing
from `T` to multiple values could be abused to define a `CustomInt(int128)` that coerces to all integer
types.
Comment on lines +84 to +86
Copy link
Member

@tmandry tmandry Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would doing this limited thing now close the door for a more general coercion mechanism later?

Also, have you considered interactions with an Autoref mechanism for method dispatch on custom reference types? e.g. as described in rust-lang/rust#136987 (comment).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I'd first assume that coercion would only happen with Copy types, and perhaps Clone types, but never with Reborrow: !Copy types. If that holds, then a general coercion mechanism would be nicely separated from autoreborrowing. If that is not the case, then we definitely would end up with some interactions: I'd then assume that if a T: Reborrow + !Copy gets coerced into U that it would first reborrow the original T instead of moving out of it.

So; maybe coercion isn't closed out by reborrowing? They would have an interaction, but at least on first brush it seems like it would be a fairly easy interaction to explain and define.

Regarding Autoref I think I've mostly considered this in the method probing section, in that a Exclusive: CoerceShared<Target = Shared> type will also add Shared to its method probing path. Whether Exclusive or Shared is actually something like Mut<T> and provides an Autoref mechanism for T is then up to the type itself and whether it implements Receiver<Target = T>. In effect, CoerceShared opens up one extra "direction" that method lookup can take, but it shouldn't have further interacitons beyond that. ... I think :)

- Autoreborrow traits should use an associated type instead of a type parameter.
- Autoreborrowing at coercion sites should not dovetail into eg. an `Into::into` call.

## Ownership and team asks

| Task | Owner(s) or team(s) | Notes |
|------------------------------|---------------------|-------|
| Discussion and moral support | ![Team][] [lang] | Normal RFC process |
| Standard reviews | ![Team][] [compiler]| Trait-impl querying in rustc to replace `Pin<&mut T>` special case |
| Do the work | @aapoalas | |

### Design autoreborrow internals

The basic idea of autoreborrowing is simple enough: when a reborrowable type is encountered at a coercion
site, attempt a reborrow operation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check my understanding: The reborrow operation always occurs, even if it's not needed for the code to compile. Is that correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe that aligns with how rustc currently works. eg. If you look at this snippet's MIR then the two func(a) calls (bb3 and bb4) look identical to one another. Likewise, if you change func to take a &u32 instead of &mut u32 then the bb3 and bb4 still look identical but now they include a &(*_2) shared reborrow.

Since a reborrow (of references) is just a trivial copy plus lifetime analysis, and the lifetime of a reborrowed reference is equal to the source reference (because this is "true reborrowing"), rustc doesn't need to perform any analysis on whether a reborrow is really needed or if a move would suffice. In both cases the result is exactly the same after all; a trivial copy.


Complications arise when reborrowing becomes recursive: if a `struct X { a: A, b: B }` contains two
reborrowable types `A` and `B`, then we'd want the reborrow of `X` to be performed "piecewise". As an
example, the following type should, upon reborrow, only invalidate any values that depend on the `'a` lifetime while any values dependent on the `'b` lifetime should still be usable as normal.

```rust
struct X<'a, 'b> {
a: &'a mut A,
b: &'b B,
}
```

To enable this, reborrowing needs to be defined as a recursive operation but what the "bottom-case" is, that
is the question. One option would be to use `!Copy + Reborrow` fields, another would use core marker types
like `PhantomExclusive<'a>` and `PhantomShared<'b>` to discern the difference.
Comment on lines +114 to +116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would an attribute on the fields themselves work?

I would guess most structs would only have one field that gets reborrowed at a time, but some would have more than one. Does that sound right to you?

Copy link
Author

@aapoalas aapoalas Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure an attribute would be sound: a Mut<T> should always reborrow as exclusive, regardless of whether the enclosing struct has an attribute on that field or not. Likewise, a wrapper type like Option<T> should reborrow as exclusive if T has exclusive semantics.

eg. Imagine a struct like this:

#[derive(Reborrow)]
struct Foo<'a, 'b> {
  #[reborrow::exclusive]
  a: &'a mut u32,
  // oops, forgot the attribute
  b: &'b mut i32,
}

If rustc allowed this to be reborrowed such that any usage of lifetime 'b would be considered a "shared lifetime reborrow", then it could conceivably be possible to magic multiple &mut i32s into existence, resulting in aliased mutations.

Indeed, most structs have only one field that gets reborrowed. Most reborrowable types probably only have a single field and a PhantomData. Some have multiple data fields and a PhantomData. Only very few have multiple reborrowable fields, though this is partially because it is inconvenient to define such types.



| Task | Owner(s) or team(s) | Notes |
|----------------------|------------------------------------|---------------------------------------------------------------------|
| Lang-team experiment | ![Team][] [lang] | allows coding pre-RFC; only for trusted contributors |
| Author RFC | *Goal point of contact, typically* | |
| Lang-team champion | ![Team][] [lang] | Username here |
| RFC decision | ![Team][] [lang] | |
| RFC secondary review | ![Team][] [types] | request bandwidth from a second team, most features don't need this |

### Implement non-recursive autoreborrowing

A basic autoreborrowing feature should not be too complicated: the `Pin<&mut T>` special-case in the
compiler already exists and could probably be reimagined to rely on a `Reborrow` trait.
Comment on lines +129 to +130
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be curious about any differences in behavior between the current implementation and the Reborrow one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I already wrote this to the above question but basically the Pin<&mut T> special case performs a check: "is T and ADT that matches Pin and contains &mut U?". If this is true, then a Pin reborrow adjustment is injected, which basically goes into the Pin, extracts the &mut U out of it, reborrows that (adding a "link" between the source and the result to bind their lifetimes), and then produces a new Pin<&mut U> with that. It relies on internal knowledge of the Pin type (including a field name) and obviously only works for Pin.

With Reborrow the check would be for "is T: Reborrow?" instead, and if true then a generic reborrow adjustment would be injected. This adjustment would then expand into a (cached if at all possible) piece of code that would be generated for the Reborrow trait that effectively would just perform a memory copy while adding the "lifetime links" between the source and the result.

(CoerceShared is a further complication here; I'm not exactly sure if the current Pin<&mut T> supports reborrow-as-shared and if it does then how does it check if it should perform it. There's something in the code that suggests it does have this support, but I'm not sure. Anyway, CoerceShared would need to effectively then check if its Target type matches the target type of the coercion site, and if so then inject a CoerceShared adjustment instead of a reborrow adjustment.)


| Task | Owner(s) or team(s) | Notes |
|-----------------------------------|------------------------------------|-------|
| Implementation | *Goal point of contact, typically* | |
| Standard reviews | ![Team][] [compiler] | |
| Lang-team champion | ![Team][] [lang] | |
| Design meeting | ![Team][] [lang] | |
| Author call for testing blog post | *Goal point of contact, typically* | |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd ideally like to see stabilization for pin reborrowing only in H2 or early H1. Do you think that would fit into this goal?

Copy link
Author

@aapoalas aapoalas Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. My personal opinion, off the cuff, is that stabilising Pin reborrowing alone would be a mistake. If there is some chance that we find the stabilised Pin reborrowing and a general autoreborrow implementation to be in conflict, we could end up in a situation where Pin needs special machinery for reborrowing until the next edition at least.

That being said, I don't see this as being very likely: assuming that the internal implementation of reborrowing can be agreed upon and Pin reborrowing can be implemented based on that, I could imagine stabilising it a bit ahead of time as plausible. Still sort of weird, but plausible.

As for stabilising reborrowing in general, I would be really happy if it could be one in H2 but I highly doubt it. H1, maybe?


## Frequently asked questions
Loading