-
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?
Conversation
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.
It's funny, I was just saying we should have a project goal for pin reborrowing, and then this appeared.
I think this is delightfully ambitious. I would say look for ways to make the RFC draft less cognitively demanding where you can (e.g. simplifying examples further, separating non-normative or detailed content into an appendix).
|
||
- 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. |
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
- 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. |
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.
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 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 :)
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. |
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.
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 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 i32
s 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.
### 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 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?
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.
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.
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. |
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.
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 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.)
| 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 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?
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.
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?
Happy to hear other people are thinking among similar lines :)
Absolutely: the draft has kind of stalled in large part because the process of writing it (and soliciting feedback over on https://internals.rust-lang.org/) caused it to grow and evolve, and I kept the evolution largely in the RFC which has made it rather unwieldy. A slimming/rewriting is definitely needed. |
Co-authored-by: Tyler Mandry <[email protected]>
Co-authored-by: Rémy Rakic <[email protected]>
Co-authored-by: Rémy Rakic <[email protected]>
Co-authored-by: Rémy Rakic <[email protected]>
Thank you @lqd <3 |
I want to propose autoreborrow traits as a project goal for the following 6 months. It is my personal goal to get the ball rolling on this, either way.
Rendered