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

Fix the borrow error in this function:

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

Rotate the slice left by k positions and return its first element before and after the rotation. Wrap the rotation amount using k % xs.len(). For an empty slice, leave it empty and return (0, 0).

The tests check both the returned pair and the contents of the slice.

[10, 20, 30], k=0  -> (10, 10), slice [10, 20, 30]
[10, 20, 30], k=3  -> (10, 10), slice [10, 20, 30]
[10, 20, 30], k=7  -> (10, 20), slice [20, 30, 10]
[42],        k=99 -> (42, 42), slice [42]
[],          k=3  -> (0, 0),   slice []

The starter already handles the empty slice and reduces the rotation amount. Keep both checks: remainder by zero panics, and rotate_left panics when its argument exceeds the slice length.

Why the starter fails

The starter saves &xs[0] in old. This is a reference to an element, not a snapshot of its value. The return expression reads through that reference after rotate_left has requested mutable access to the slice. Rust rejects the overlapping shared and mutable borrows with E0502.

Here, the result needs the original number. Since i64 implements Copy, let old = xs[0]; saves an independent value. Return that value directly instead of dereferencing it. The saved number no longer borrows the slice.

A borrow can end before its block does

Rust uses non-lexical lifetimes (NLL) to determine where references must remain valid from how they are used in the program. In a simple sequence, a borrow can end after the reference's last use, even if the variable is still in scope:

fn main() {
    let mut text = String::from("hi");
    let first_view = &text;
    println!("{first_view}"); // Last use of this reference.

    text.push('!');
    assert_eq!(text, "hi!");
}

The mutation is accepted because first_view is no longer needed. If you print first_view again after push, the borrow must span the mutation, and compilation fails.

This is a useful way to investigate borrow errors: find the conflicting references and their later uses. Sometimes moving a use earlier is enough; sometimes you need an owned value, as in this exercise. Branches, returned references, and destructors can impose additional lifetime requirements, so the last textual mention of a variable is not a universal rule.

RFC 2094 explains the motivation for NLL and the limitations of the older checker. For a related collection example, see the entry API lesson.

Borrow lifetimes and dropping values are different

NLL does not automatically destroy a value after its last ordinary use. A lock guard, for example, normally keeps its lock until the guard is dropped. Use a smaller scope or drop(guard) when you need to release it earlier. Scopes remain useful for controlling resource lifetimes.

For this exercise, copying one i64 is enough. You do not need to clone the slice or introduce another scope.

A passing submission must compile, pass the tests, and pass clippy -D warnings.

Loading visualization…