Conversation
This comment has been minimized.
This comment has been minimized.
e4c08f7 to
3b1957a
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Some changes occurred in cc @BoxyUwU Some changes occurred in match checking cc @Nadrieril This PR changes MIR cc @oli-obk, @RalfJung, @JakobDegen, @vakaras This PR changes rustc_public cc @oli-obk, @celinval, @ouz-a, @makai410 Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt Some changes occurred to constck cc @fee1-dead Some changes occurred to the CTFE / Miri interpreter cc @rust-lang/miri Some changes occurred in compiler/rustc_codegen_cranelift cc @bjorn3 Some changes occurred to the CTFE machinery Some changes occurred in src/tools/clippy cc @rust-lang/clippy |
|
r? compiler |
| /// Wraps a value in an unsafe binder. | ||
| WrapUnsafeBinder(Operand<'tcx>, Ty<'tcx>), | ||
|
|
||
| /// A reborrowable type being reborrowed |
There was a problem hiding this comment.
Please explain the runtime behavior of this operation.
The interpreter implementation seems to indicate that this is equivalent to a normal assignment of Copy($place). Why is it even a separate statement?
There was a problem hiding this comment.
Right, my apologies, I definitely should've added more information around this: I added a link to the experiment tracking issue into the opening comment which contains some more information and links but basically: in the trivial case (#[repr(transparent)]-compatible memory layout of types and a single lifetime) Reborrow is a Copy plus extra borrow checking analysis to perform the same reborrowing operation as happens with &'a mut T, but now on some user-defined ADT struct Custom<'a>(..). CoerceShared is then the &'a mut T -> &'a T equivalent of this operation and thus also requires a user-defined target type.
This PR does not implement any non-trivial cases so in both cases the runtime effect is exactly equivalent to a Copy, but future expansions should make CoerceShared possible even if the layouts of the source and target types do not match, including the possibility for the target type to drop out some fields from the source type. (This has a requested use-case where a custom exclusive reference type has some write-time metadata which is then not necessary in a read-only reference.)
There was a problem hiding this comment.
I added a link to the experiment tracking issue
That link is broken btw.
future expansions should make CoerceShared possible even if the layouts of the source and target types do not match, including the possibility for the target type to drop out some fields from the source type.
That sounds like an operation that is too complicated to be a single MIR statement, it should instead be encoded as a sequence of simpler operations. Otherwise every MIR consumer has to re-implement all that logic, partially defeating the point of having an IR like MIR in the first place.
There was a problem hiding this comment.
Agh, thank you: fixed the link in the comment.
That sounds like an operation that is too complicated to be a single MIR statement, it should instead be encoded as a sequence of simpler operations. Otherwise every MIR consumer has to re-implement all that logic, partially defeating the point of having an IR like MIR in the first place.
This sounds reasonable to me; the "non-trivial" reborrow case where multiple exclusive references participate in the reborrow at the very least would make sense as a combination of trivial reborrows. How to design a good decomposition of CoerceShared is less clear to me. For eg. a CustomMut<'a>(*mut T, PhantomData<&'a mut ()>) to CustomRef<'a>(*const T, PhantomData<&'a ()>) should it be a (field-wise?) copy plus then an entirely separate MIR action which relates the two lifetimes? Or should it be a single MIR action that performs the copy and the lifetime relation at the same time, like it is now?
The first one seems overly complicated for this particular case, but the second case does not really work if CustomMut instead looks like CustomMut<'a>(*mut T, usize, PhantomData<..>).
There was a problem hiding this comment.
Yes, someone will have to work out a design for that and present it in a structured form -- maybe not an RFC since it's largely an internal compiler design choice, but something similar. Probably an MCP, since it seems like a fairly big change.
There was a problem hiding this comment.
Definitely don't add two new operations. Please understand that any new MIR operation incurs a non-trivial cost on the rest of rustc and some of the surrounding ecosystem (e.g. Rust verification tools such as Verus or Kani). Be economical about adding new primitives.
There was a problem hiding this comment.
When you say "two new operations", do you specifically mean that there should either only be a reborrow/coerce shared MIR op XOR a Rvalue::Reborrow variant? As said, my understanding is shallow so I don't exactly know if both are strictly necessary or if we have simply made a judgement error in the implementation. A special MIR operation seems to make sense as we need to reborrow potentially through a PhantomData here, so that's definitely a new primitive sort of thing. Why the Rvalue was created I'm less clear on, though basically all of the actual operations are defined on it so it does seem relatively necessary in that sense, while the MIR operation is the one that actually produces the Rvalue.
But the aim absolutely is to produce something that is, as much as possible, trivial from a performance point of view, doesn't burden the ecosystem overmuch, and preferably is useful/powerful/restricted enough that it could be considered for replacing &mut at some later point in the future if so desired. But it definitely is also a new primitive (unfortunately) for now even if it were to replace the existing borrow one way down the line, since Rust currently has no way of reasoning about "reborrowing" a PhantomData<&mut ()> since of course that's just a Copy ZST and lifetime.
If you have any insight or suggestions on how to proceed to a more economical design here / what seems reasonable to you, I'd be really happy to hear absolutely any and all of them :) The possible paths that I see, including dumb ones, are:
- Current path with a MIR operation and Rvalue variant, working only on trivial treborrows (types with one lifetime, no changes in memory layout)
- Only MIR operation producing normal values (unclear where the current impl code moves but I assume it's possible?), only on trivial treborrows
- Only Rvalue (how do we produce this without a new MIR op?), only on trivial reborrows
- Special
PhantomExclusive(and its dualPhantomShared) type which is the only trivially reborrowable type (aside from&mut), possibly handled by the current Borrow Rvalue? - Complex reborrows: cram everything into the special MIR op, making it a very complex thing indeed
- Complex reborrows: produce these through effectively destructure+restructure, so a single complex Reborrow decomposes into a bunch of trivial ones and any associated data field copies as need be.
I'm absolutely trying to be all ears here: I want generic reborrowing in Rust, but I want it to be an unobtrusive expansion instead of an earth-shattering change and a horrible pain and annoyance on everyone working within the compiler and on the language. I don't want to muck this up in any way, shape, or form.
There was a problem hiding this comment.
Oh, this is a new Rvalue variant, not a new StatementKind. I had lost context some time since I started this thread. Sorry for the confusion.
I agree having that makes sense (but also see Oli's comment -- reusing an existing variant would be even better), but the variant should be documented a bit more. And if the op.sem of this variant becomes more complicated in the future, we may have to reconsider the best way to go about this.
There was a problem hiding this comment.
I added fairly extensive comments; do these seem good to you, or are there questions or pain-points left from your point of view?
We discussed reusing an existing variant but didn't really think it a good idea right now. Mostly for two reasons:
- Changing
Rvalue::Refwould probably be massively disruptive, and eg. removing the experiment from the compiler would become very hard. Rvalue::Refis probably one of the most important variants; adding extra data and an extra branch to check if it is this a generic Reborrow could cause measurable performance regressions.
Perhaps one day when Reborrow traits are stable and we consider implementing &mut reborrowing in terms of the traits...? But for now, I believe it is best to keep it fully separate.
There was a problem hiding this comment.
Perhaps one day when Reborrow traits are stable and we consider implementing
&mutreborrowing in terms of the traits...?
that is too late in the pipeline of the feature imo. I would like to see an attempt or two before being anywhere near stabilization. Preferrably right after this PR lands
| } | ||
|
|
||
| adjustment::Adjust::GenericReborrow(_reborrow) => { | ||
| // Do the magic! |
There was a problem hiding this comment.
Oh we should call delegates. See the ReborrowPin arm and that is basically what we want to do.
And eventually we should be able to subsume ReborrowPin case hopefully.
| // `&mut T: DerefMut` tho, so it's kinda moot. | ||
| } | ||
| Adjust::GenericReborrow(_) => { | ||
| // No effects to enforce here. |
There was a problem hiding this comment.
We should do something here. The type would change after a generic reborrow, ie the CoerceShared case.
|
Where can I read context on what CoerceShared is/why it exists, and what the design for reborrow traits is |
I added a link to the experiment tracking issue but put it very shortly: |
|
I think I'll just |
Thank you <3 my top secret plan (designed together with tmandry) was to get a compiler review and then reroll into types to that view as well so that works well :) |
Appreciated. |
|
given oli has more context on this, (though if you can't review this we should chat about it and figure out what to do here :>) |
|
|
| WrapUnsafeBinder(Operand<'tcx>, Ty<'tcx>), | ||
|
|
||
| /// Creates a bitwise copy of the indicated place with the same type (if Mut) or its | ||
| /// CoerceShared target type (if Not), and disables the place for writes (and reads, if Mut) for |
There was a problem hiding this comment.
What does "disables the place" mean?
The first half sounds like a description of the operational semantics, but the second half sounds like a description of what happens in the borrow checker. It's quite confusing to mix those together.
There was a problem hiding this comment.
Same thing as what b does for a here:
let a = &mut 0u32;
let b = &mut *a;a is "disabled" for reads and writes while b lives. So this is borrow checker stuff indeed; I assumed that'd be relevant information here. And I must admit I assumed it is fundamentally an opsem consideration; is &mut XOR & not part of opsem?
If I split this up, what sort of split would you expect? Would the first half explain simply that for a btiwse copy is produced (and that in the future Not reborrows may include reordering and dropping of fields), and then the second half explain that the borrow checker creates a covariant lifetime relation between the source and target's only lifetime (and that in the future multiple lifetimes may participate in the reborrowing, in which case all source lifetimes are covariantly related to their corresponding pair on the target)?
There was a problem hiding this comment.
It is relevant but it is not opsem. You implemented the opsem in this PR when you added support in interpret and you didn't do anything wrt disabling the source there did you? :)
You can just have two paragraphs, one for what it means for the borrow checker and one for what it means operationally.
There was a problem hiding this comment.
(this is still unresolved, so the PR is not fully ready yet)
There was a problem hiding this comment.
Sorry, mildly forgot. Fixed now.
Let me know if this is not still up to snuff in your view! <3
There was a problem hiding this comment.
Stuck somewhere in the middle of a review, but submitting at this point as I gotta run
I have come back to "this should just be a BorrowKind::Custom", which I don't think will affect perf or complexity (and def not more than this PR does), but I can finish reviewing if you want before doing such a major refactor of the PR
| self.sub_types(rv_ty, place_ty, location.to_locations(), category) | ||
| // Note: we've checked Reborrow/CoerceShared type matches | ||
| // separately in fn visit_assign. | ||
| if !matches!(rv, Rvalue::Reborrow(_, _)) |
There was a problem hiding this comment.
I think we should not have a visit_assign in this visitor, but instead do an if/else chain here to handle the generic reborrow case
There was a problem hiding this comment.
Didn't do this just yet.
There was a problem hiding this comment.
Actually, I'm not sure if we can do this: visit_assign is called in eg. call(a) for a whereas visit_statement I presume would only be called let b = a;
|
Reminder, once the PR becomes ready for a review, use |
I would very much appreciate that: I'm very afraid of doing such a major rewrite of the entire of the feature. This isn't exactly a great reason, but from a personal point of view it is quite discouraging to have something working after months of work and then hear that nah, this'd actually better be done an entire different way :D in that sense I would personally prefer landing this and then starting on the rewrite while concurrently gathering feedback from users. |
We discussed this a little: The main question we had was: what was your thinking about usage of the EDIT:
Turns out we weren't even using the |
This comment has been minimized.
This comment has been minimized.
3b3699f to
c10c973
Compare
This comment has been minimized.
This comment has been minimized.
|
@rustbot ready |
This comment has been minimized.
This comment has been minimized.
a739e9d to
b9d634a
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
View all comments
With this PR we now have basic functional Reborrow and CoerceShared traits. The current limitations are:
&mutwrappers is working (though I've not tested generic wrappers likeOption<&mut T>yet), but CoerceShared of&mutwrappers currently causes an ICE.The remaining tasks to complete before I'd consider this PR mergeable are:
Reborrow traits experiment: #145612
Co-authored by @dingxiangfei2009