What are the rules for scopes and rusts borrowing rules

From comprehensive rust

the following is a borrowing error:

fn main() {
    let mut a: i32 = 10;
    let b: &i32 = &a;

    {
        let c: &mut i32 = &mut a;
        *c = 20;
    }

    println!("a: {a}");
    println!("b: {b}");
}

Output:

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

But why, because I was under the impression that variable c lives only in the scope and no more afterwards, so why does rustc still complain on the last line, the mutable c is not even accessible anymore at this point? Is rustc just not able to asses the fact that c is no more available or this is not that easy for the borrow checker to assert??

The speaker notes do not even say a word on this, its kind of confusing :).

The issue is not that c lives too long, it's that you cannot even instantiate c = &mut a because a is already immutably borrowed (shared borrow).

3  |     let b: &i32 = &a;
   |                   -- immutable borrow occurs here

Like the slide says,

  • You can have one or more shared references to the value, OR
  • You can have exactly one exclusive reference to the value.

Since b is an immutable (shared) borrow &a, you cannot also have a mutable (exclusive) borrow &mut a.

2 Likes

Thanks, it took me on the wrong path of thinking, because moveing the print for b above the scope then suddenly works, because rustc sees that its the last use.
Thanks

I suspect your misunderstanding is about part that is before that example. It's related to ambiguity of English. This part:

The issue here with or. It maybe inteprereted as “inclusive or” or “exclusive or” and here we are talking about “exclusive or”.

You may only have many shared references or, alternatively, precisely one exclusive one.

But shared reference and exclusive one at the same time? That's not allowed.

Remember that &mut requires exclusie access to the referred var. It can't live simultaneously with an immutable ref, &.

So if an immutable ref is declared above the &mut initialization, and then used below it, there'll be access collisison, and the code won't compile. E.g.

let b = &a;        // immutable ref <--+
let c = &mut a;    // mutable ref <--+ |  < must be exclusive, all other refs must have ended here.
                   //                | |
println!("{b}");   //            <---|-+
println!("{c}");   //          <-----+

Even in a block, &mut a requires exclusive acces (because a may be referenced in another thread, doing its job).

It would have been ok if

  • both were immutable refs (&).
  • or if b weren't referenced below c = &mut a declaration.
// ok
let b = &a;        // immutable ref <----+
let c = &a;        // immutable ref <--+ | 
                   //                  | |
println!("{b}");   //              <---|-+
println!("{c}");   //            <-----+

or:

// ok
let b = &a;        // immutable ref <--+
                   //                  |
println!("{b}");   //            <-----+
let c = &mut a;    // mutable ref <--+
                   //                | 
println!("{c}");   //          <-----+
1 Like

Examples like this are typical, but also completely misleading. The lines involving c being within an inner block is totally irrelevant as far as borrow-checking goes for the example. Inline the block and nothing changes.

The only thing the inner block is doing is making c go out of scope earlier. But references going out of scope is almost never relevant. c going out of scope would conflict with some ref_c = &c being used afterwards,[1] but it has no effect on how long a is borrowed, for example.

As the other posts have explained: the actual problem is interleaving exclusive and shared borrowing. Or in other words: a is shared-borrowed from the creation of b through the last println! (due to the use of b), and that conflicts with taking a &mut a between the two. Inlining the inner block changes nothing about this analysis.

So ignore the inner block, it's a red herring for the example at hand.


This part is just more words about the role of scopes in borrow checking (which isn't relevant to the OP example, so perhaps skip it).

From a birds-eye view, the way the borrow checker works is to

  • calculate lifetimes based on annotations, how lifetime-carrying values[2] are created, and how they are used
  • calculate where in the code every borrowed place[3] is borrowed, and how,[4] based on how the borrow was created and the lifetimes[5]
  • Look at every use of every place, and see if it conflicts with an active borrow

Where lexical scopes come into play is that they introduce uses -- values going out of scope are used in some way. If they have a trivial destructor, the use is very shallow. If they have a non-trivial destructor, the use is generally fully exclusive, like taking a &mut _.

In particular, a reference going out of scope doesn't "use" the lifetime of its type or anything it points to. It doesn't even invalidate reborrows through the reference.[6] The only thing it conflicts with is the reference itself being borrowed, like a reference to a reference.

Going out of scope is much more relevant when there is a non-trivial destructor, as that introduces an exclusive use.

(Rust did have lexical lifetimes at one point -- but NLL got rid of that, 6 years ago.)


  1. because c becomes uninitialized ↩︎

  2. values of types that have a lifetime ↩︎

  3. a variable, a field, a dereference... ↩︎

  4. shared or exclusive ↩︎

  5. this extra step/layer allows some borrows to be shorter than the corresponding lifetime ↩︎

  6. Otherwise methods which return a reference to a field couldn't work! ↩︎

2 Likes