How does rust pass or bind to a mutable reference

fn dummy(s: &mut String) {
    println!("{}", s);
}

fn main() {
    let mut_ref_s = &mut String::from("this");
    let sss: &mut String;
    sss = mut_ref_s; //reborrow from mut_ref_s
    dummy(mut_ref_s); // moving newly reborrowed tmp from mut_ref_s
    let ss = mut_ref_s; // moving mut_ref_s;
}

According to this post, a mutable reference is either reborrowed or moved when passed around. But by what rules rustc chooses one or another? Take snippet above for example, let bindings gives different MIR based on whether &mut String is annotated or not just as commented. Can anyone helps to explain this, many thanks!
(Playground)

I don't know of a succinct set of rules and it's not that well documented. However, from the reference:

And as for the example:

    // 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
    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;
3 Likes

@quinedot thanks for your reply! I can get your points which explain almost everything. however sss and mut_ref_s share the same type. and for me, it has no necessity to do this kind of coercion at all.

So, generally, the lifetimes exist to enforce Rust's guarantees around borrows and memory safety. One core principle is "no two active &mut to the same memory", which requires &muts to move and not be copied. But under this system, you couldn't have a method like this without reborrowing:

fn bar(s: &mut String, one: &str, two: &str) {
    s.push_str(one); // if `s` gets moved
    s.push_str(two); // you can't use it anymore and this wouldn't work
}

And of course, that's way too restrictive; hence, reborrowing.

In terms of the example, if there wasn't a reborrow when you assigned to sss, you wouldn't be able to use mut_ref_s anymore.


Furthermore, lifetimes can be considered part of the definition of a type. For example, this won't compile, because the lifetimes may not be compatible:

fn foo<'a, 'b>(r: &'a str) -> &'b str {
    r
}

In this sense, &'long mut String can be considered a subtype of &'short mut String. The relationships become more complicated at the types become more complicated.

However, once a lifetime is determined (either by compiler inference or due to the labels you've used), it is not changed -- the type is static, just like other types in Rust. At that point, if you need to reborrow for some reason (like a method call or assignment), but still use the original variable, the reborrow will have to have a shorter lifetime than the original variable.

So let's look at your example again, but in a context where you can force the lifetimes by giving them names:

fn example<'a>(mut_ref_s: &'a mut String) {
    // Unlike before, these have the same type _including lifetime_
    let sss: &'a mut String;
    // Even though this is a reborrow, it has a lifetime that is exactly as
    // long as `mut_ref_s`
    sss = mut_ref_s;

    // That means we can't use `mut_ref_s` anymore -- only one `&mut` can be
    // usable at a time, and the most recent reborrow "wins".  So no more
    // reborrows for `mut_ref_s`.  (We could still use `sss` though.)
    dummy(mut_ref_s);
    
    // You can't move it while it's reborrowed in `sss` either.
    let ss = mut_ref_s;
}

When we've forced sss and mut_ref_s to have the same type including lifetime, the code no longer compiles.

Thus, if mut_ref_s wasn't coerced to a supertype (shorter lifetime) in your original example, it wouldn't have compiled.

5 Likes

Thanks again for you elaboration. It explains! Move does not make sense for call expr with mutable reference of receiver. So my final question is that would rust implicitly create a reborrow of a mutable reference with a shorter lifetime for any case just to let code compile, when there are no explicit annotation of lifetime markers?

It tries pretty hard to find a solution, but there are cases where it doesn't reborrow. If you have a let binding with no &mut specified or when a function argument is generic (and doesn't have &mut specified). Probably other cases I'm not thinking of. Perhaps some of these will implicitly reborrow in the future.

2 Likes

@quinedot Sorry for reopening this thread. It baffled me that sss which is the other mutable borrow actually lives within the scope of the first mutable borrow mut_ref_s, does not this violate "multiple mutable borrows are not allowed in the same scope" rule? Take the following code for example, the compiler actually allows to modify the referent thru sss:

fn dummy(s: &mut String) {
    println!("{}", s);
}

fn main() {
    let mut raw = String::from("this");
    let mut_ref_s = &mut raw;
    let sss: &mut String =  mut_ref_s; //reborrow from mut_ref_s with a shorter lifetime
    *sss = "that".to_string(); //mut_ref_s is alive in this scope as well
    dummy(mut_ref_s); // use of mut_ref_s 
    let ss = mut_ref_s; // 
}

Am I wrong about the scope desugaring by rules here? Hope to be enlightened!

BTW, I came up with this question when I tried to reason about how borrow rules work
for mutable "for range":

let mut v = [1,2,3];
for i in &mut v {
// here i and &mut v both seem to be mutable borrows of v
}

A reborrowed mutable borrow is temporarily frozen and can't be used until the reborrow itself ends.

2 Likes

Perhaps think of it like nesting, or like a stack. If the "no two [usable] mutable references at once" rule worked that strictly, most things just wouldn't be usable:

struct Foo {
    one: Bar,
    two: Bar,
}

impl Foo {
    fn quz(&mut self) {
        // Can't do anything with &mut self.one or &mut self.two
        // because they alias with &mut self
    }
}

But it doesn't work that way -- you can "split" your borrow into sub-borrows of Self's fields so long as the compiler can prove they don't overlap. However while you're actively using self.one or self.two, you couldn't replace the entirety of self, say:

    let one = &self.one;
    let *self = Foo { /* ... */ };
    println!("{}", one);

Because that would make one dangle. It works similarly for elements of a slice. And reborrowing is also related -- you can't use the original while the reborrow is in use, because they overlap. Reusing the original "kills" the reborrow.

Although it may not end up being the final form of Rust's borrowing system, there is a formal model for these ideas called stacked borrows. It is so-called because the sub-borrowing and re-borrowing acts like a stack: you push the reborrow onto the stack to make it active. But once you access an ancestor borrow, every descendant borrow that is currently active (that is on the current stack) gets popped off in order to expose the accessed ancestor.

By this mechanism, no two overlapping borrows can be used at the same time.

2 Likes

There is no such rule. As stated, it would mean that the guarantees of &mut are lexical, bound to the scopes within a function. This isn't true, the guarantees of &mut are global and semantic. There can be no two simultaneously active &mut borrows of the same memory region at any point in time during the entire execution of the program, including different functions and threads. That is what makes &mut so powerful: if you hold one, you know that nothing can ever change memory under you, as long as you don't pass it away or reborrow. Note that those guarantees cover not only references, but also any kind of unsafe access through pointers or any other means.

Now the tricky part is simultaneously active mentioned above. That one is actually hard to define precisely (at least in full generality including unsafe code), and the best current reference is the already mentioned Stacked Borrows paper. But at the most basic level, it means that there can be no interleaving accesses through different &mut's which point to aliasing memory. The accesses involve reading and writing through the reference, obviously, but it also includes creating a new &mut, including reborrowing.

This implies the "freeze" rule mentioned above: once you reborrow an x: &mut T, you cannot access x in any way until you're done with the reborrow. Essentially, as soon as you access x again, the lifetime of the reborrow ends and it can no longer be used. This all happens transparently if the reborrow happens in a method call or a passed function parameter, but local reborrows can surface the complexity.

The non-lexical lifetimes mostly manage to provide the semantics above, but there are a few important edge cases which they cannot handle, so that you can't access the original &mut even though the reborrow is no longer accessed.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.