There is a difference between syntactical scope of a variable and whether you can actually still use that variable in Rust. Syntactically, r1 and r2 stay “in scope” until the end of the block. This notion of scope is the one you need to have when determining what which name refers to. There’s for example rules at play that prioritize the innermost variable with the same name when resolving names, a concept also known as “shadowing”.
On the other hand there’s the borrow checker which can restrict certain variable usages further. You could try to put these rules in words as: when you create a new shared reference &s all previous murable references to s may no longer be used. And when you create a new mutable reference &mut s all previous references to s, shared or mutable, may no longer be used.
The “timeframe” (or syntactically roughtly “the part of your code”) in which a reference can be used is called its “lifetime”. Then the rule becomes that the lifetimes of mutable references may not overlap the lifetime of any other reference to the same variable. The compiler is able to figure out the lifetime of a variable automatically, so in your example it determines that the lifetime of r1 and r2 end before r3 is created and its lifetime starts. If the compiler is not able to find any appropriate way to assign lifetimes to your references, you will get a compile error.
For example
if we consider
/* 1 */fn main() {
/* 2 */ let mut s = String::from("hello");
/* 3 */
/* 4 */ let r1 = &s; // no problem
/* 5 */ let r2 = &s; // no problem
/* 6 */ println!("{} and {}", r1, r2);
/* 7 */
/* 8 */ let r3 = &mut s; // no problem
/* 9 */ println!("{}", r3);
/*10 */
/*11 */ println!("{}", r1);
/*12 */}
Then r1 is created in line 4 and used in line 6 and 11. So its lifetime (the code section in which it can be used) must be at least lines 4 to 11. But this includes line 8 where r3 is created, a mutable reference to the same variable. The lifetimes of r1 and r3 may not overlap, but there’s no way to avoid any overlap, hence the compiler error:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:8:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
...
8 | let r3 = &mut s; // no problem
| ^^^^^^ mutable borrow occurs here
...
11 | println!("{}", r1);
| -- immutable borrow later used here
error: aborting due to previous error
The compiler points out line 11 to tell you “see, r1 must live at least until line 11”. And it points to line 8 since that’s where r3 is created.
Usually there’s a rule in Rust that variables are only dropped at the end of their scope. This might seem to contradict the way how references are more short-lived than their full scope. The thing to keep in mind here is that references don’t have any destructors, so even though the references may still be on the stack, it can be declared dead (as in, no longer alive) before the end of its scope. Some more advanced example of this kind of phenomenon might me this:
fn main() {
let v: Vec<i32> = vec![1,2,3];
let x: &i32 = &v[1];
let v2: Vec<&i32> = vec![x];
// the reference at v2[0] points into v, to v[1]
drop(v);
// v2 implicitly dropped only at the end of the scope (as indicated below)
// right here, v2 is not dropped yet, while v is dropped
// v2 actually still contains the - now dangling - pointer
// into v. This is safe since the destructor of `Vec<&i32>` does not
// actually dereference the references it containts (some thing that
// the borrow checker knows about because of some special unsafe annotations
// in the standard library)
// long story short, accessing
v2[0];
// here is a compile error, but without this line, this code compiles
// v2 dropped here
}
Finally, there’s other ways next to borrowing how variables can become unusable in certain ways. For example if you move out of a variable, that variable is still in scope, but you cannot access the (non-existent) value in it anymore. In a more complicated setting, it might actually be the case that you only conditionally moved out of a variable. The variable is still in scope though, and in both cases you can still assign to it.
E.g.
fn main() {
let mut s = String::from("hi");
if false { // The type checker or borrow checker don’t care what
// the value of the condition here is, the code compiles.
// with `if false` if and only if it compiles with `if true`
drop(s); // move out of `s`, conditionally
}
// s still in scope, i.e.
// if I type the name "s" here, the compiler will know I mean
// the s above. Yet i cannot do
// println!("{}", s); // uncomment to get compiler error
// the reason being: if the condition in the `if` above was true,
// s would not contain any (valid) value anymore at this point
// I can still do
s = String::from("hello");
// though. The old string was still in `s` (since the `if false` statement
// does nothing in practice) and the string "hi" was only dropped
// the moment the new one was assigned to `s`.
// now we can
println!("{}", s);
}