Borrowing and Slices · about 40 min

Inside the Borrow Checker

Why this matters

The borrow checker is the part of rustc that enforces the borrowing rules. Beginners often describe learning Rust as "fighting the borrow checker". The fight gets much shorter once you know how it thinks: what it tracks, which questions it asks, and which handful of restructurings resolve nearly every error. This lesson is that model.

What the checker tracks

For each reference, the checker computes a region: the set of points in the program where the reference may still be used. A region starts where the borrow is created and ends at the reference's last use. Then, at every point in the program, it asks one question:

Is any action here incompatible with a borrow whose region includes this point?

The incompatible actions, for a value x that is borrowed:

borrow live on xforbidden on x (except through that borrow)
shared &xmutating, mutably borrowing, moving, dropping
mutable &mut xeverything: reading, borrowing, mutating, moving, dropping

That is the whole model. Let us annotate a program with regions:

fn main() {
    let mut s = String::from("hi");
    let r = &s;                 // r's region starts
    println!("{r}");            // last use of r: region ends here
    s.push('!');                // mutation: fine, no borrow is live
    let w = &mut s;             // w's region starts
    w.push('?');                // last use of w: region ends
    println!("{s}");            // reading s: fine
}
hi
hi!?
line:        3      4      5      6      7      8
r:           [======]                                   shared borrow of s
w:                                [=====]               mutable borrow of s
s.push:                    X                            needs &mut s: no borrow live, ok
println s:                                      X       reads s: no borrow live, ok

Regions are not scopes

Before 2018, Rust used the variable's scope (to the closing brace) as the region, and many reasonable programs were rejected. Today's analysis, non-lexical lifetimes (NLL), uses actual uses. A reference variable may stay in scope long after its last use; that does not matter.

But a later use extends the region backwards over everything in between:

fn main() {
    let mut s = String::from("hi");
    let r = &s;
    s.push('!');
    println!("{r}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> main.rs:4:5
  |
3 |     let r = &s;
  |             -- immutable borrow occurs here
4 |     s.push('!');
  |     ^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{r}");
  |               --- immutable borrow later used here

The fix is to move the use of r before the mutation, or to create r after it.

E0506: assigning to something borrowed

Assignment is mutation too:

fn main() {
    let mut scores = [10, 20, 30];
    let best = &scores[2];
    scores[2] = 0;
    println!("{best}");
}
error[E0506]: cannot assign to `scores[_]` because it is borrowed
 --> main.rs:4:5
  |
3 |     let best = &scores[2];
  |                ---------- `scores[_]` is borrowed here
4 |     scores[2] = 0;
  |     ^^^^^^^^^^^^^ `scores[_]` is assigned to here but it was already borrowed
5 |     println!("{best}");
  |               ------ borrow later used here

Note that the checker treats the whole array as borrowed when you borrow one element through indexing; it does not try to prove which index is which. Here the fix is simplest of all: i32 is Copy, so store the value, not a reference: let best = scores[2];.

What the checker can see precisely

The checker understands disjoint fields. Borrowing two different fields of a tuple (or, later, a struct) mutably at the same time is fine:

fn main() {
    let mut pair = (String::from("left"), String::from("right"));
    let a = &mut pair.0;
    let b = &mut pair.1;
    a.push('!');
    b.push('?');
    println!("{:?}", pair);
}
("left!", "right?")

It does not see through indexes, because index values are only known at run time:

fn main() {
    let mut arr = [1, 2, 3];
    let a = &mut arr[0];
    let b = &mut arr[1];
    *a += 10;
    *b += 10;
}
error[E0499]: cannot borrow `arr[_]` as mutable more than once at a time
 --> main.rs:4:13
  |
3 |     let a = &mut arr[0];
  |             ----------- first mutable borrow occurs here
4 |     let b = &mut arr[1];
  |             ^^^^^^^^^^^ second mutable borrow occurs here
5 |     *a += 10;
  |     -------- first borrow later used here
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

The help is accurate: the standard library has split_at_mut, which splits one mutable slice into two non-overlapping ones (lesson 5 and module 15 show how it works inside).

It also understands that method call arguments run before the call:

fn main() {
    let mut v = vec![1, 2, 3];
    v.push(v.len());
    println!("{:?}", v);
}
[1, 2, 3, 3]

push needs &mut v, and v.len() needs &v, in the same expression. This "two-phase borrow" is allowed because the mutable borrow only becomes active after the argument is computed.

A toolbox for borrow errors

When the checker says no, one of these almost always works. In rough order of preference:

  1. Reorder. Finish using the first borrow before starting the conflicting action. Often the whole fix is moving one line.
  2. Copy the value out. For Copy types, take the value (let x = v[i];) instead of a reference. You hold no borrow at all.
  3. Use indexes instead of references. Store "the position of the best element" (usize) rather than a reference to it, and index when needed. Indexes are plain numbers and borrow nothing.
  4. Shorten the borrow with a block. Put the code using the reference in { } so its last use is clearly before the conflict.
  5. Split the data. Borrow disjoint fields, or use split_at_mut for slices.
  6. Clone. When you really need an independent copy, clone removes the relationship entirely, at the cost of copying.

Later modules add tools for genuinely shared, mutable data (Rc<RefCell<T>>, Arc<Mutex<T>>) and lifetimes for references stored in structs. But for code within one function, the list above covers almost everything.

Here is strategy 3 in action, a classic "update while scanning" situation:

fn main() {
    let mut names = vec![String::from("bo"), String::from("alexandra"), String::from("cy")];
    // remember WHERE the longest is, not a reference to it
    let mut longest = 0;
    for i in 1..names.len() {
        if names[i].len() > names[longest].len() {
            longest = i;
        }
    }
    names[longest].push_str(" (longest)");
    names.push(String::from("dee"));
    println!("{:?}", names);
}
["bo", "alexandra (longest)", "cy", "dee"]

A &String to the longest name could not coexist with the push_str and push calls; an index can.

Why the checker is conservative

The checker sometimes rejects programs that would actually be fine at run time. It must decide at compile time, from the code alone, without running it, and it must never accept an unsafe program. So when in doubt it says no. In practice the rejections are usually telling you something real about your design: two parts of the code both think they can change the same data. Restructuring to make ownership clear tends to make the code better, not just accepted.

Common mistakes

Recap

Going further: take any E0502 you hit in the exercises and draw the regions as in the table above. The blog post "Non-lexical lifetimes: introduction" by Niko Matsakis explains the history of this analysis.

Exercises

Exercise 1 · quiz

Does it compile?

Does this compile?

fn main() {
    let mut total = 10;
    let r = &total;
    println!("{r}");
    total += 5;
    println!("{total}");
}
Answer
  • Yes: the shared borrow r ends at its last use, before total is modified

The borrow checker works with the region where a reference is actually used, from its creation to its last use, not with the variable's scope. r is last used in the first println!, so the borrow is over when total += 5 runs. This is called non-lexical lifetimes (NLL), and it is why reordering statements fixes many borrow errors.

Exercise 2 · write code

Copy the value out

This program keeps a reference to the current best score while it updates the scores array, and fails with E0506 ("cannot assign to ... because it is borrowed"). Fix it by storing a copy of the best value (an i32) instead of a reference. It must print:

best before: 30
scores: [10, 20, 0]

File: main.rs

Starting point

fn main() {
    let mut scores = [10, 20, 30];
    let best = &scores[2];
    scores[2] = 0;
    println!("best before: {best}");
    println!("scores: {:?}", scores);
}

Example

expected output

best before: 30
scores: [10, 20, 0]
Hint 1

i32 is Copy: let best = scores[2]; copies the number out of the array.

Reference solution
fn main() {
    let mut scores = [10, 20, 30];
    let best = scores[2];
    scores[2] = 0;
    println!("best before: {best}");
    println!("scores: {:?}", scores);
}

Exercise 3 · write code

Indexes instead of references

Read integers from stdin, one per line, into a vector (the starter does this). Then, for every element that is smaller than the element before it, replace it with that previous element (so the sequence never goes down), processing from left to right. Print the result with {:?}. For input 3 1 4 1 5 2 (one per line):

[3, 3, 4, 4, 5, 5]

Holding a reference to the previous element while assigning to the current one will not compile. Loop over indexes 1..v.len() and read v[i - 1] (a copy) before assigning v[i].

File: main.rs

Starting point

use std::io;

fn main() {
    let mut v: Vec<i32> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    println!("{:?}", v);
}

Example

input

3
1
4
1
5
2

expected output

[3, 3, 4, 4, 5, 5]

Example

expected output

[]
Hint 1

for i in 1..v.len() starts at the second element; with fewer than two elements the loop does not run.

Hint 2

let prev = v[i - 1]; copies the integer, so no borrow is held when you write v[i] = prev;.

Reference solution
use std::io;

fn main() {
    let mut v: Vec<i32> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    for i in 1..v.len() {
        let prev = v[i - 1];
        if v[i] < prev {
            v[i] = prev;
        }
    }
    println!("{:?}", v);
}

Exercise 4 · quiz

v.push(v.len())

v.push(v.len()); reads v (for len) inside the argument of a call that borrows v mutably (push). What happens?

Answer
  • It compiles: the mutable borrow for push only becomes active after the arguments are evaluated

Rust uses "two-phase borrows" for method calls: the &mut v for push is reserved first but only activated once the arguments are computed, and v.len() finishes before that. The compiler is precise about such cases; when it does reject code, there is usually a real ordering problem.