This would violate the non-aliasing (exclusivity) rule of mutable references in case of overlapping scopes, as you have shown in your first snippet. The r3 mutable reference to s is used in the print statement after the scope in which r4 exists, so under the current borrow checker rules it must be live from the point of declaration to the point where we use it in the print statement. This liveness scope includes the block in which we declare r4. Due to the exclusivity of the mutable borrow in r3, the borrow checker prevents us from creating a second mutable borrow to s while the first one is still live.
The nll RFC describes this a lot better than I could.
As a practical suggestion, reborrowing would work to avoid the two simultaneous mutable references.
In your snippet A, they are not in "different scopes". The scope of r4 overlaps with that of r3, because the former is a subset of the latter.
You need to understand that the rules of mutable exclusivity are not arbitrary. They are not designed to make you go mad; they are designed to ensure memory safety, which means you must not be able to mutate the same place through two distinct references in an interleaving manner.
If your code snippet "A" were allowed, then in the innermost block, you would have both r3 and r4 in scope, and both of them would be usable (for mutating s). This is what differentiates this example from reborrowing r3 (which is allowed).
Note that it can be allowed via reborrowing: let r4 = &mut *r3, but this explicitly prevents use of r3 for the duration of the reborrow. When references are taken independently, the borrow checker can't prove they won't break exclusivity rules.
Here's a modification of Code B where I remove the inner scope. It still compiles.
let mut s = String::from("hello");
let r4 = &mut s;
println!("r4 is {}", r4);
let r3 = &mut s;
println!("r3 is {}", r3);
The way that scopes interact with borrow checking is that when a variable goes out of scope
its destructor, if it has one, may require access to the value
the variable becomes uninitialized
The only thing that went out of scope in your example was r4. References do not have destructors so the first bullet doesn't apply. You didn't have a reference to r4 that would be invalid when r4 became uninitialized, so the second bullet didn't matter.
References to references are rare, so scopes rarely matter for references.
Both versions of Code B work because the borrow of r4 is not needed after the first println!, so it expires before r3 is created. Scope doesn't enter into this analysis at all.
Instead of writing up what I just did, I could have modified Code A and showed that it still does not compile, and talk about how the scope doesn't matter to that example at all either. The error is because r4 is created between r3 being created and r3 being used, so both exclusive references would have to exist at the same time.
Or more generally, you used s in an exclusive manner (taking a &mut) when another borrow of s (r3) was still alive. Moving s at that location would produce a similar error, for example, because moving is also an exclusive use.
In broad strokes, the borrow checker
Analyzes the liveness of Rust lifetimes (those '_ things) based on lifetime annotations[1] and how places[2] are used[3]
Looks at every use of every place to check for conflicts between the use and live borrows
The interaction with lexical scopes is that they add a use of everything that goes out of scope. This can be a deep use like a destructor (which may examine everything about the value going out of scope), or a shallow use like when a reference goes out of scope (a no-op besides making the reference uninitialized).
Many newcomers try to influence borrows/lifetimes by adding scopes, but it rarely works out since lifetimes are non-lexical, so they're typically just adding more uses. Probably the main case where it's beneficial is when you change where the desctructor of something like a mutex lock runs (which could also be accomplished with a drop(..)).
this is a refinement of the last step because a place borrowed by a reference with lifetime 'x, say, may be borrowed for less than 'x if the reference is overwritten, for example ↩︎