-
Notifications
You must be signed in to change notification settings - Fork 65
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
base: main
Are you sure you want to change the base?
Changes from all commits
460b3ab
7844728
3653c00
0c11c18
659e2a9
79f41fb
a177b10
d271144
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
- 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... I'd first assume that coercion would only happen with 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 |
||
- 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure an attribute would be sound: a 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 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I already wrote this to the above question but basically the With ( |
||
|
||
| 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* | | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 That being said, I don't see this as being very likely: assuming that the internal implementation of reborrowing can be agreed upon and 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 |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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