Rust borrow rules, confused about some examples

Hi, I'm trying to figure out all the different cases where I cannot take references of some variable due to the borrow rules. I'm trying to figure it out because I'm making a Rust program generator - so it'll need to generate programs that respects the borrow checker. Would appreciate any in depth guides or documents that talk explicitly about the borrow rules (e.g. how the compiler checks it or any formal documentation).

It looks like the borrow checker only really barks when it sees that the immutable borrow has actually performed some mutation, rather than complaining about there being a mutable and immutable reference at the same time.

let mut t: i32 = 5;
let s = &t;
let v = &mut t;  
*v = 20;  // Compiler is OK until s is read in some way... I think...

// This line is what causes compiler to be unhappy    
println!("{}", s);

This is something related that I'm confused about - why is the second takes_ref considered a mutable borrow? I thought by specifying as function parameter that I'm taking by reference, it wouldn't be considered as a mutable borrow.

struct A {
    a: u32,
}

struct B<'a> {
    a: &'a mut A,
}

#[allow(unused_variables)]
fn main() {
    let mut a: A = A{a: 5};
    
    let b = B{a: &mut a};
    
    takes_ref(&a);
    
    takes_ref(&b.a);
}

fn takes_ref(a: &A) -> i32 {
    5
}


   Compiling playground v0.0.1 (/playground)

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/main.rs:16:15
   |
14 |     let b = B{a: &mut a};
   |                  ------ mutable borrow occurs here
15 |     
16 |     takes_ref(&a);
   |               ^^ immutable borrow occurs here
17 |     
18 |     takes_ref(&b.a);
   |               ---- mutable borrow later used here

error: aborting due to previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground`

1 Like

I’m not sure what the best kind of general documentation is to learn more about the borrow checker in-depth. Maybe others know some resources. I can answer your question here though:

It’s often important to think about borrowing as some kind of chain or stack of borrows. One thing can borrow another, and someone can borrow the first thing which transitively also kind-of borrows the second thing, too, then.

In your example there’s

  • a variable a of type A,
  • a variable b of type B<'_> that contains a mutable reference to a
  • a temporary reference &b.a of type &A that borrows … well … we’ll get to that, but it goes through b, so on a first approximation it’s borrowing b in some way

Now, the more simple kind of example that presents a similar error is this one

let mut a = 42;
let b: &mut i32 = &mut a;
let c: &i32 = &*b;
println!("{} {}", a, c);
   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --> src/main.rs:5:19
  |
3 | let b: &mut i32 = &mut a;
  |                   ------ mutable borrow occurs here
4 | let c: &i32 = &*b;
5 | println!("{} {}", a, c);
  |                   ^  - mutable borrow later used here
  |                   |
  |                   immutable borrow occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

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

Now, your example uses a struct B<'_>, instead of a mutable reference directly, but the principle is similar, especially since one of the struct’s fields is a mutable reference. In fact, it’s the only field, so all things considered, the type B<'a> can pretty much be considered same as the type &'a mut A in most ways Writing something like &b.a is mostly similar to borrowing b, and then transforming the borrow of b into a borrow of the field. You can e.g. write a function like this:

fn transform_the_borrow<'a, 'b>(reference: &'a B<'b>) -> &'a A {
    reference.a
}

and then think about

takes_ref(&b.a);

as something similar to

takes_ref(transform_the_borrow(&b));

But in this version of the code, it becomes obvious that b itself was borrowed and the borrow of the target of the field a is only derived from that borrow of b.

By the way, you can also write your original example with

takes_ref(b.a);

so: without the &. Your code would’ve created an & &mut A and then implicitly converted it into a &A which is what takes_ref expects; without the extra &, you just have a &mut A which is implicitly converted to a &A as well. To get rid of any implicit conversion, we’d need to explicitly write that we want to “immutably borrow the target of b.a, i.e. &*b.a (which is parathesized as &(*(b.a)) in case you’re unsure of what exactly gets dereferenced by the dereference operator. In particular, it isn’t parenthesized as “&((*b).a)” or as “(&(*b)).a).


Now, to go into a bit more detail, the situation is slightly more involved: In general something like &*b.a doesn’t borrow “the whole” value b. There’s special rules such that e.g. structs that individual fields can be borrowed independently, even if b was only a reference to the struct. This allows for code like

fn split_that_borrow(x: &mut (i32, i32)) -> (&mut i32, &mut i32) {
    (&mut x.0, &mut x.1)
    //    ^^^ there’s an implicit dereferencing here, too, it’s 
    // equivalent to (&mut (*x).0, &mut (*x).1)
}

to compile. But note that these kinds of borrows that borrow a struct partially aren’t “fist class” in Rust in the sense that you can’t write a type signature saying “this is a reference to a value of type B but it only accesses this certain (set of) field(s)”.

Anyways, now the more correct way to think about &*b.a is that it’s borrowing b partially in a way that only its field a is borrowed. Or maybe more specifically only the target of where the reference that’s the value of the field b.a is pointing to is actually borrowed, but since every reference only has one target, so you can still do something like

let mut a: A = A{a: 5};

let mut b = B{a: &mut a};

let r: &A = b.a;

let mut new_a = A { a: 42 };

b.a = &mut new_a;

takes_ref(r);

where r borrows the target of b.a, but then before it’s used the field b.a itself is modified (but this is not problematic because such a modification won’t change the previous target of the value in b.a. Note, as I already mentioned above, that this reasoning will not go as far as to realize that the target of b.a is the variable a, which is the kind of analysis that would be needed for your code example not to be rejected.

2 Likes

And to also address the first question:

In Rust the lifetime of a borrow (i.e. a reference) can be shorter than the scope of the variable holding that reference. The code above but without the last line containing the println, acts a lot as if it was written like this:

let mut t: i32 = 5;
let s = &t;
drop(s); // <- this is more symbolic to convey intent, “dropping” a shared reference
         //    like this doesn’t actually do anything (because &T: Copy)
let v = &mut t;  
*v = 20;

or like this:

let mut t: i32 = 5;
{
    let s = &t;
    // scope of `s` ends here
}
let v = &mut t;  
*v = 20;

i.e. it acts as if the variable s doesn’t really exist anymore after the it’s last used. Well… it isn’t used at all, so then the last use is kind-of just the point of definition. If you’re going into this a bit further you might bump into the question of “how does this all work when variables are always dropped at the end of their scope only? Doesn’t dropping them consitute another, final, point of usage of the variable?” And the answer would be: There’s special rules that allow the reference in s to still exist in-memory without being “dropped” but without being considered used anymore (even by the drop), and these rules aren’t problematic because the type &i32 doesn’t have any destructors at all that need to be called, so dropping really doesn’t use the reference anymore. Hence the reference in s is, technically, being dropped at the end of scope only – no earlier – but it’s containing a reference that’s long dead at this point and the compiler is totally fine with that. The special rules even go somewhat further to the point that you can even write

fn foo() {
    let mut t: i32 = 5;
    let s = vec![&t];
    let v = &mut t;  
    *v = 20;
    // s dropped here! Note that `Vec` *does* have a destructor.
}

but you can’t write

fn foo() {
    let mut t: i32 = 5;
    let s = vec![&t];
    let v = &mut t;  
    *v = 20;
    drop(s); // s dropped here, but we’re trying to be explicit
}

These nuances are further discussed here. I guess the whole Ownership chapter of the Rustonomicon is quite a valuable resource to learn bore about the borrow checker.

2 Likes

Rust is very strict language and you need to follow certain rules that you normally wouldnt for other languages.

I follow certain rules too. In above two lines, since b takes a mutable reference of a .. I always make sure I get to a from b only.
Following two lines break that rule:

There can only be one mutable reference of a (which b has). If you want many immutable references of a ask b. It all depends on binding. For eg. you can have another method takes_ref_mut(a: &mut A). so you can do:

fn main() {
    let mut a: A = A{a: 5};
    
    let b = B{a: &mut a};
    
    takes_ref(b.a);
    
    assert!((*b.a).a == 5);
    
    takes_ref_mut(b.a); //b.a is same for above call but this has mutable binding
    
    assert!((*b.a).a == 6);
}

fn takes_ref(a: &A) -> i32 {
    5
}

fn takes_ref_mut(a: &mut A) -> i32 {
    a.a = 6;
    todo!()
}

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.