Reborrow trait

The problem

This works:

fn foo<T>(param: &mut T) {}

fn foo_twice<T>(param: &mut T) {
    foo(param);
    foo(param);
}

This doesn't:

fn foo<T>(param: Pin<&mut T>) {}

fn foo_twice<T>(param: Pin<&mut T>) {
    foo(param);
    foo(param);
}

That's a problem.

Why this should be a priority

The language and library teams want to stabilize AsyncIterator within the next few months. Ideally, convenience methods like next() should soon follow. For maximum flexibility, these methods should take Pin<&mut Self>. But if Pin<&mut Self> is painful to use, that defeats the whole purpose of convenience methods!

Prior work

RFC 2364 (postponed) proposed to formalize a Reborrow trait. But the definition of the trait had a problem:

We introduce the following trait, as member of std::marker:

trait Reborrow { fn reborrow(&self) -> ???; }

Unfortunately the return type of fn reborrow isn't representable in general; the return type must be Self but with different lifetime(s) (and note that there will be multiple lifetimes if the type has multiple fields with lifetimes). It is not possible to use an associated type type Result: ??? because the return type includes a lifetime (or multiple) bound by the caller.

Let's try again.

Reborrow take 2

EDIT: see new design here

Signature

Here's the definition of Reborrow:

#[lang = "reborrow"]
#[marker]
pub trait Reborrow {}

impl<T: Copy + ?Sized> Reborrow for T { }

impl<'a, T: ?Sized> Reborrow for &'a mut T { }

And here's the new definition of Copy:

#[lang = "copy"]
#[marker]
pub trait Copy: Clone + Reborrow { }

Just as with Copy:

  • A type can implement Reborrow iff the types of all its fields implement it.
  • There is a built-in derive macro for implementing Reborrow.
  • Reborrow types cannot also be Drop.

Semantics

For any two types Super and Sub, such that Super is a supertype of Sub:

fn foo(arg: Sub) {
    let local: Super = arg;
    // HERE
}

At the point marked HERE:

  • If Sub implements Copy, arg remains fully accessible.
  • If Sub implements neither Copy nor Reborrow, arg is permanently inaccessible.
  • New: if Sub implements Reborrow but not Copy, arg is inaccessible only until Super's lifetime ends.

That is all.

Wait, how do you reborrow Wrapper<&mut T> as Wrapper<&T> with this proposal?

You don't :frowning_face: . At least, not until &mut T becomes a subtype of &T. But that's a problem for another day…

2 Likes

Nit: Copy has Sized as a supertrait (via Clone).


What else in std or the base language implements Reborrow? PhantomData<_> I guess. How about UnsafeCell?


Can you be more precise? For example, does this:

#[derive(Reborrow)]
struct S<'a> {
    r: &'a str,
    m: &'a mut str,
}

fn ex(s: S<'_>) {
    let s2 = s.reborrow();
}

Act like this?

impl<'a> S<'a> {
    fn reborrow(&mut self) -> S<'_> {
        S { r: self.r, m: self.m }
    }
}

fn ex(mut s: S<'_>) {
    let s2 = s.reborrow();
}

(If so, &mut _ reborrowing is something different.)

Or does it act like this?

    let s2 = S { r: s.r, s: s.m };

(If so, *s.r is not reborrowed, but *s.m is.)

1 Like

Probably like the latter? Since it's strictly more useful. So this would work:

#[derive(Reborrow, Debug)]
struct S<'a> {
    r: &'a str,
    m: &'a mut str,
}

fn ex(s: S<'_>) {
    let s2 = s;
    dbg!(s.r);
    drop(s2);
}

However, this wouldn't:

fn ex2(s: S<'_>) {
    let s2 = s;
    dbg!(s);
    drop(s2);
}

Nor would this:

// Could be `Copy`, but isn't
#[derive(Reborrow, Debug)]
struct S2<'a> {
    r: &'a str,
    m: &'a str,
}

fn ex3(s: S2<'_>) {
    let s2 = s;
    dbg!(s);
    drop(s2);
}

PhantomData<_> is already Copy, and therefore needs no separate Reborrow implementation.

UnsafeCell<T> is invariant in T, so an implementation of Reborrow would be pointless (as there is no supertype to reborrow it as).

Basically any generic container that implements Copy will want to implement Reborrow as well. (Poll, Option, Result, etc)

I'd love to see Rust have support for implicit struct reborrowing to make it easier to use structs that neatly wrap up nasty borrow splitting problems, or constrain how &mut can be used (Pin<&mut... being an example of that, of sorts).

What exactly does “Super's lifetime” mean here? A lifetime parameter? What if Super has multiple lifetime parameters, and some of them are used in a way which requires them to be as long as the original, or are invariant — or both? For example,

pub(crate) struct Foo<'sh, 'mu> {
    buf: &'mu mut Vec<&'sh str>,
    cur: &'sh str,
}

Here the 'sh lifetime is used with references that do not need the reborrow treatment, and also cannot be shortened without unsoundness, since it is used in an invariant position. So, the reborrow feature should be able to shorten 'mu as needed while leaving 'sh alone.

Perhaps the variance of the lifetime parameters could imply what to do? Or perhaps Reborrow could explicitly specify what lifetime it affects:

impl<'sh, 'mu> Reborrow<'mu> for Foo<'sh, 'mu> {}

…no, that would give the lifetime parameter on Reborrow weird magic semantics. What if the reborrow output is specified as a GAT, while still not having any fn reborrow()?

impl<'sh, 'mu> Reborrow for Foo<'sh, 'mu> {
    type Output<'mu2> = Foo<'sh, 'mu2> where 'mu: 'mu2;
}

It would be a compile error for Reborrow::Output to be anything that is not a supertype of Self.

What I was originally going for was "the longest 'a such that Super: 'a holds". But I'm now realizing that this doesn't work. For struct Foo<'a>(&'a mut i32, &'static mut i32);, the lifetime of Foo<'a> is 'a, but it must remain borrowed for 'static.


Revised design

Here's what I think we have to do instead. First, we redefine Reborrow like so:

#[lang = "reborrow"]
#[marker]
pub trait Reborrow<'a> {}

impl<'a, T: Copy> Reborrow<'a> for T { }
impl<'a, T: ?Sized> Reborrow<'a> for &'a mut T { }
impl<'short, 'long: 'short, T: Reborrow<'short>> Reborrow<'long> for T { }
  • A type can implement Reborrow<'a> iff the types of all its fields implement Reborrow<'a>.
  • Other requirements are unchanged.

New semantics

For any two types Super and Sub, such that Super is a supertype of Sub:

fn foo(arg: Sub) {
    let local: Super = arg;
    // HERE
}

At the point marked HERE:

  • If Sub implements Copy, arg remains fully accessible.
  • If Sub implements neither Copy nor Reborrow, arg is permanently inaccessible.
  • New: if Sub implements Reborrow (lifetime does not matter) but not Copy, and Super: Reborrow<'a> (pick the shortest 'a for which this holds), arg is inaccessible for 'a.

Because Reborrow's semantics are defined in terms of subtyping, we get proper handling of invariance "for free".

Ralf at least has expressed an interest in interior mutability types which implement Copy. Anyway, the thing that tickled my mind was a dead-end, so nevermind for now I guess.

More generally, actual reborrowing is quite complicated. And while informed by lifetimes, lifetimes aren't the whole story, as (for one example) uses can make the (re-)borrow of a place shorter than the associated lifetime.[1] Not to mention lifetimes being flow-control sensitive more generally in Polonius.

I think a desugaring-based definition would be more tractable and explainable than some entirely new mechanism.


  1. Polonius version ↩︎

This must have been written before GATs were stabilized. Could it perhaps be done with GATs now? This could also take care of the "reborrowing to &T" scenario. GATs are a bit unwieldy, but you could do it like this [EDIT: wait, I think this isn't quite right, or maybe there is a simpler way to do it. But maybe this will give people ideas]:

trait ReborrowOutput {
  type Output<'a> where Self: 'a;
}
trait Reborrow<'a, Output: ReborrowOutput> {
  fn reborrow<'b>(&'a mut self) -> Output::Output<'b> where 'a: 'b;
}

(full code with impls in Rust Playground )

(Might be preferable if you could specify that Output should be covariant in 'a, but I'm not sure it would be a problem at use-sites because reborrow is explicitly able to shorten the lifetime anyway)

2 Likes

I tried your idea but putting it in one trait and using it on the original example of the first RFC: Rust Playground

I don't think any reborrowing actually happens in your code, because shared references are Copy. Indeed it's possible to remove the 'long and 'short lifetimes and the code (which is now essentially the identity function) still compiles!

To force reborrowing, you have to use exclusive references, and then the code won't compile :frowning:

oh. you are right. sorry :sweat:

don't see it mentioned here anywhere and just found this: reborrow - Rust (Am I allowed to post links?)

1 Like