Simple resources about reborrowing for beginners?

I have collected some basic links to start learning about reborrowing which apparently isn't very well documented, and I haven't found it in the book. (I'm at Ch 9-10)

I wonder whether there are some other links you would add, and what does the addition of that information to the book depend upon (or what's blocking it?) ?

I just thought it might be useful for others as well which is why I'm sharing them. (I didn't searched much on this forum yet, although I should have.)

This (loosely related) doesn't seem explained in the book either but maybe it is. My understanding is that mutability of variable and of a reference very different things. It's somewhat clear though that the ownership is transferred and so the function gets to decide, but this is quite different with references since reborrow may happen in that case, instead of a move.

My point is not that this doesn't make sense but it could be better documented/Explained maybe?

===========

Edits to add some extra links

Some posts on this topic and forum that I have found quite interesting are

2 Likes

I don't see it mentioned in The Rust Programming Language, either, but it's mentioned several times in Programming Rust by Blandy, Orendorff, Tindall.

There are some information about it in RFC 2094, which was mentioned by @quinedot not too long ago here: Hidden Details of Rust's Reborrow, Double References, and Lifetimes.

2 Likes
2 Likes

There are also some straightforward rules:

    // Reborrows can happen as a form of coercion at coercion sites
    // A binding with an type annotation is a coercion site:
    let sss: &mut String;
    sss = mut_ref_s; //reborrow from mut_ref_s
    // Function parameters annotated to be `&mut _` reborrow too
    // Context: fn signature is `fn dummy(a: &mut String)`
    dummy(mut_ref_s); // moving newly reborrowed tmp from mut_ref_s
    // Bindings without explicit types do not coerce or reborrow
    let ss = mut_ref_s; // moving mut_ref_s;

In general my opinion is that all of chapter 4 in the book needs a rewrite, and that the community as a whole needs better introductory material about borrowing that does not

  • conflate value liveness with Rust lifetimes
  • say or imply that Rust lifetimes are intrinsically scope based
  • say things like "the lifetimes are chosen at the call site"

One can explain reborrows without this background material, and an example or two is often enough to let someone learning Rust get the gist and continue on with their learning. So maybe it's not a hard blocker. But I feel it's somewhat incomplete or feels more magic that it needs to if you have some misconceptions about borrows more fundamentally.

Some comments on some of the links follow. Some of the comments may of limited usefulness unless you already have a mental model of borrowing close to mine...


Haibane Tenshi

The problem with their "emulating reborrows" is that the reference itself is borrowed, instead of reborrowing the referent. It's the same thing I'm getting at here. Incidentally, the only time a reference going out of scope matters to borrow checking is when the reference itself is borrowed. Which is one of those things most existing learning material gets wrong. (I.e. this is related to why I think we need better introductory material on borrows more generally.)

They are correct that we can't express reborrows with traits or existing function APIs so far.

Their "liveness scope" isn't really how the compiler works. Value liveness scopes have no associated Rust lifetime and are not inherited by borrow lifetimes. Their stacked borrows citation should probably be an NLL (or Polonius) citation. I didn't put in enough work to understand their mental model enough to grasp their 'l+'r1 suggestion, but it might make sense if you could designate a place with 'l instead of some "liveness". I skipped the "scope expansion" section since they'd already lost me.


Reference #788

Not a learning resource, but I'll just note that subtypes and coercions do not explain reborrowing. Those don't let you "duplicate" a non-Copy value like &mut _ reborrowing does. Or to create some lifetime relationships but not others based on the presence of a nested &_ somewhere.


SO 62960584

Good answer :slight_smile:.


SO 30535529

Their second example compiles with NLL (since Rust 1.36 edition 2015 / 1.31 edition 2018). Learning pre-NLL borrow checking isn't very productive, probably.


Questions about &mut T and move semantics

I mostly skimmed it.

Stacked borrows -- or an alternative, Tree borrows -- or some other future alternative -- are about the operational semantics.[1] What the borrow checker does in safe code, like reborrows, will always be a compile-time checkable subset of what the opsem model allows. They can still be useful in terms of providing a mental model of why something is accepted, e.g. a stack of reborrows.

Someone mentions Rust by Example, but that needs a rewrite too IMNSHO.

This is inaccurate in a couple ways,

  • the reference itself isn't borrowed with reborrowing, as covered above
  • reborrows definitely can have the same lifetime as the original borrow,[2] that's how &mut _ getters work

  1. Which model Rust will actually adapt is undecided. ↩︎

  2. AFAIK there is no "strictly shorter" lifetime relationship in the compiler ↩︎

3 Likes

I wonder whether this interpretation of reborrowing is something you would subscribe to?

Answer by steffahn

Especially the bit where it says:

let mut a = 42;
let b: &mut i32 = &mut a;
let c: &i32 = &*b;
println!("{} {}", a, c);

The reference c borrows the target of the reference b , which seems like it’s a , but the borrow checker still considers this operation to go through b . So it’s more like c is borrowing from b . The way to think about this in terms of lifetimes then is: Since c borrows from b , as long as the immutable borrow c is alive, the mutable borrow b of a still needs to stay alive, too. This is what the compiler complains about. If rustc talks about “borrow later used here”, it actually just means that there’s some kind of dependency between the lifetime of the borrow and the value it’s pointing at. So c us “using” the mutable borrow in the sense that accessing c keeps the mutable borrow alive . Perhaps, some useful further reading about this kind of example would be this post here (already linked the relevant section, but it’s probably worth reading the whole thing at some point).

Also, on the forum posts you skimmed through, this question's answers seemed useful to me as a beginner (just to extract those that seem the most useful):

Answer by trentj:

I agree, this is "just" an implicit reborrow, not a Deref(Mut) coercion.

However, I don't think Copy is relevant either; implicit reborrowing is unique to &mut references.

When you put a &mut reference at a coercion site (usually a function parameter) where a &mut reference of the same type is expected, the reference is implicitly reborrowed with a shorter lifetime, rather than moved. Reborrowing is different from copying or subtype coercion, and you can't enable it for any other type; it is built in to the compiler for &mut references.

Implicit reborrowing also does not work for functions that expect a generic type R where R is inferred to be &mut T — the function parameter must be of the form &mut _. You can, of course, still reborrow an argument explicitly, like f(&mut *y).

Same question by peerreynders: (suggesting "stack")

The output makes it look like mutable references can "stack" where the more recent exclusive access "suspends" the previous one. Once the more recent exclusive access "goes out of use" the previous exclusive access becomes active again.

And by ExpHP

The compiler inserts a reborrow in any possible location where a &mut T is "moved" into a place that already has its type known to be &mut _ (where the _ is anything; it could be T, it could be some other U (in which case coercions may also be inserted), or it could even be left up to type inference with _ (in which case it is always inferred to be T)).

So:

let x = 0;
let y = &mut x;

let z: &mut i32 = y;  // This is an implicit reborrow (equiv. to &mut *y)
let z: &mut _ = y;  // This is an implicit reborrow
let z: _ = y;  // This is a move
let z = y;  // This is a move

Similar examples using function calls

fn takes_mut_i32(_arg: &mut i32) {}
fn takes_mut_t<T>(_arg: &mut T) {}
fn takes_any_t<T>(_arg: T) {}

takes_mut_i32(y);  // this is an implicit reborrow
takes_mut_t(y);  // this is an implicit reborrow
takes_any_t(y);  // this is a move; sometimes this surprises people and there
                 //     have been countless "bugs" filed about it

=============
But none of those is a complete explanation of reborrows at all

Yes. Reborrows keep the original borrow active. There are some nuances but that's the gist.

The annotations are more or less accurate for this simple example, but I'll note that

  • lifetimes aren't lexical scopes and lexical scopes don't correspond to lifetimes either
  • the presence of 'a { .. } implies there's a lifetime associated with a, but there is not; value liveness and Rust lifetimes ('_ things) are not the same thing

So do take away the concept of "the reborrow duration 'c is a subset of the original borrow duration 'b", but don't buy in too hard to the example's "desugaring".

&T can be reborrowed, like for a field access, but don't sweat it. It's much simpler than &mut _ due to &T being shared (you can have many of them) and being Copy.

Yeah, successive &mut _ reborrows are a stack, or a sequence of subsets.

Known to be &mut _ or &_. I'm not sure that _ is always inferred to be T in this scenario (but it doesn't matter much either way).

It's been cited before, but anyway the closest thing I know of to a spec is the NLL RFC (which I summarized here).

When 'a: 'b, uses of 'b keep 'a alive. (For reborrows, 'orig: 'reborrow.)

It's hard to give a truly complete explanation as many errors (or non-errors) that happen in the exploration of reborrows are due to borrow checking in general (a complex topic), and there are some other special rules for references that may also come up.

1 Like