Skip to content

← Ownership II: Borrowing and the Borrow Checker step 5 of 24

Easy Primitives

Non-lexical lifetimes: a borrow ends at its last use

Another fix-it. The function is written; it does not compile.

pub fn rotate_and_report(xs: &mut [i64], k: usize) -> (i64, i64)

Rotate the slice left by k positions, and return (first element before, first element after). On an empty slice, do nothing and return (0, 0).

[10,20,30], k=0  -> (10,10), slice unchanged
[10,20,30], k=3  -> (10,10), slice unchanged   (k == len is a full turn)
[10,20,30], k=7  -> (10,20), slice becomes [20,30,10]
[42],       k=99 -> (42,42)
[],         k=3  -> (0,0)

slice::rotate_left panics if you hand it a number greater than the length, so k has to be reduced with k % len first — and % by zero is also a panic, which is why the empty case is handled before anything else. Both of those are already done for you in the starter. The compile error is the point.

The one fact this problem exists to install

Before 2019, Rust’s borrow checker was lexical: a borrow lived from the & until the end of the enclosing block, full stop. Under that model, let old = &xs[0]; would keep the vector frozen for the rest of the function, and the only cure was to wrap it in an artificial { ... } scope.

That model is gone. Since Rust 2018 (NLL — non-lexical lifetimes, RFC 2094) the rule is:

A borrow is live from where it is created to its last use, computed over the control-flow graph. After the last use, it is dead and conflicts with nothing.

This single fact resolves roughly half of every beginner E0502. It is also the fact most likely to be wrong in whatever tutorial or Stack Overflow answer you find, because the web is full of pre-2019 writing that is still confidently telling you to insert scopes.

So when you hit a borrow error, do not reach for a { } block. Ask instead: what is the last use of this loan, and can I make it earlier?

Applying it here

The starter writes:

let old = &xs[0];        // loan created
...
xs.rotate_left(k % n);   // needs exclusive access -> E0502
(*old, xs[0])            // <- the loan is still used HERE

old is used after the rotation, so the loan is still live at the rotation, so the rotation is rejected. The fix is not to shorten the code, add a scope, or copy the whole slice. It is to notice that you never wanted a reference at all:

let old = xs[0];   // i64 is Copy — this reads a value out and ends the loan

i64 implements Copy, so xs[0] in value position produces an independent i64. The momentary shared borrow needed to perform that read ends on the same line. Nothing is live afterwards, and rotate_left is free to run.

The general shape: when the thing you need is Copy, copy it out instead of holding a reference to where it lives. Item 3.6 is the same move again.

The three Problem Cases, and the one still open

RFC 2094 named three shapes that the old lexical checker rejected wrongly.

  • Problem Case #1 — a borrow used in only one branch of a match or if, but held for the whole block. Fixed by NLL.
  • Problem Case #2 — a borrow held across a loop back-edge that was actually finished with. Fixed by NLL.
  • Problem Case #3 — conditional control flow across functions: you take a &mut, use it in one branch, and want to take a fresh one in the other. Still rejected in 2026. The canonical instance is match map.get_mut(k) { Some(v) => v, None => { map.insert(...); ... } }. Item 3.20 is that exact case, with the idiomatic way around it and an honest account of the in-flight fix (Polonius).

So NLL made the checker much smarter, not omniscient. There is still one named hole, and knowing its name is worth a lot when you fall into it.

What NLL did not make obsolete

A caution, because the “just delete your scopes” message overshoots. There are still good reasons to end something early on purpose:

  • drop(guard) on a MutexGuard, RefMut or file handle. NLL ends the borrow, but the guard is a value with a Drop impl, and dropping it is a real side effect (unlocking, flushing) that happens at the end of its scope unless you say otherwise. Releasing a lock early is still drop(guard).
  • Types whose Drop runs at scope end and whose timing you care about.

NLL is about when loans die, not about when values die. Do not conflate them.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…