Skip to content

Experiment: Reborrow traits#151753

Open
aapoalas wants to merge 32 commits intorust-lang:mainfrom
aapoalas:aapoalas/reborrow-and-coerce-shared-first-version
Open

Experiment: Reborrow traits#151753
aapoalas wants to merge 32 commits intorust-lang:mainfrom
aapoalas:aapoalas/reborrow-and-coerce-shared-first-version

Conversation

@aapoalas
Copy link
Contributor

@aapoalas aapoalas commented Jan 27, 2026

With this PR we now have basic functional Reborrow and CoerceShared traits. The current limitations are:

  1. Reborrowable types can only have one lifetime parameter, so as to avoid having to figure out and store in rmeta the information of which lifetimes weaken during reborrowing.
  2. Reborrowing of &mut wrappers is working (though I've not tested generic wrappers like Option<&mut T> yet), but CoerceShared of &mut wrappers currently causes an ICE.

The remaining tasks to complete before I'd consider this PR mergeable are:

  • Fix ICE on CoerceShared. Unfortunately this might require dipping into rmeta.
  • Expand the tests to give a more complete view of the current state of the experiment.

Reborrow traits experiment: #145612

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jan 27, 2026
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rust-bors

This comment has been minimized.

@aapoalas aapoalas marked this pull request as ready for review February 2, 2026 21:16
@rustbot
Copy link
Collaborator

rustbot commented Feb 2, 2026

Some changes occurred in rustc_ty_utils::consts.rs

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

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Feb 2, 2026
@aapoalas
Copy link
Contributor Author

aapoalas commented Feb 2, 2026

r? compiler

@rustbot rustbot assigned jackh726 and unassigned tmandry Feb 2, 2026
/// Wraps a value in an unsafe binder.
WrapUnsafeBinder(Operand<'tcx>, Ty<'tcx>),

/// A reborrowable type being reborrowed
Copy link
Member

Choose a reason for hiding this comment

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

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?

Copy link
Contributor Author

@aapoalas aapoalas Feb 3, 2026

Choose a reason for hiding this comment

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

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.)

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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<..>).

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member

Choose a reason for hiding this comment

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

Why does it need to be distinguished in MIR from a regular copy? To be special-cased in the borrow checker? Would making it an Rvalue also serve that goal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly, it's for borrow checker special casing. It does also appear as an Rvalue::Reborrow but my understanding here is absolutely too shallow to really comment on whether that or the MIR operation or both serve the goal, or if one or the other is actually redundant and unnecessary.

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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 dual PhantomShared) 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.

Copy link
Member

@RalfJung RalfJung Feb 8, 2026

Choose a reason for hiding this comment

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

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.

}

adjustment::Adjust::GenericReborrow(_reborrow) => {
// Do the magic!
Copy link
Contributor

@dingxiangfei2009 dingxiangfei2009 Feb 3, 2026

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

@dingxiangfei2009 dingxiangfei2009 Feb 3, 2026

Choose a reason for hiding this comment

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

We should do something here. The type would change after a generic reborrow, ie the CoerceShared case.

@BoxyUwU
Copy link
Member

BoxyUwU commented Feb 3, 2026

Where can I read context on what CoerceShared is/why it exists, and what the design for reborrow traits is

block.and(PlaceBuilder::from(temp))
}
ExprKind::Reborrow { source: _, mutability: _, ty: _ } => {
todo!();
Copy link
Contributor

Choose a reason for hiding this comment

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

We should generate a temporary. Therefore, we can merge this case one above, in other words build a temporary place, so that the reborrowing receives its own region "token."

@aapoalas
Copy link
Contributor Author

aapoalas commented Feb 3, 2026

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: Reborrow is to struct X<'a>(..) what &mut *x is for &mut T, and CoerceShared is the equivalent operation as &*x. CoerceShared takes a type argument instead of having an associated type in expectation of it being possible to reborrow types like struct Double(&mut A, &mut B) which would then have three different "shared reference" variants: struct DR1(&A, &mut B), struct DR2(&mut A, &B), and struct DR3(&A, &B), though I am currently doubting the wisdom of this choice and it may return to using an associated type.

@BoxyUwU
Copy link
Member

BoxyUwU commented Feb 3, 2026

I think I'll just
r? me
on this since I know jack is quite busy with coercions work right now and this should probably have a types reviewer 🤔

@rustbot rustbot assigned BoxyUwU and unassigned jackh726 Feb 3, 2026
@aapoalas
Copy link
Contributor Author

aapoalas commented Feb 3, 2026

I think I'll just r? me on this since I know jack is quite busy with coercions work right now and this should probably have a types reviewer 🤔

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 :)

@jackh726
Copy link
Member

jackh726 commented Feb 3, 2026

I think I'll just r? me on this since I know jack is quite busy with coercions work right now and this should probably have a types reviewer 🤔

Appreciated.

Copy link
Contributor

@oli-obk oli-obk left a comment

Choose a reason for hiding this comment

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

Not sure this would be better, but if there are also plans to generalize field projections later, it would probably be good to try to keep references and custom ref types in sync

View changes since this review

if let Err(terr) =
self.sub_types(rv_ty, place_ty, location.to_locations(), category)
// Note: we've checked Reborrow/CoerceShared type matches
// separately in fn visit_assign.
Copy link
Contributor

Choose a reason for hiding this comment

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

This being requires is another sign that this may not be the best representation.

From other use sites it seems to me like you'd want to generalize Rvalue::Ref to support more types than references

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants