We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 15 of 24
Split borrows: the checker reasons about places, not variables
You are given a struct with three parallel fields and a method that has to read one while writing another. It does not compile.
pub struct Inventory {
pub items: Vec<String>,
pub counts: Vec<i64>,
pub log: Vec<String>,
}
pub fn run(items: Vec<String>, counts: Vec<i64>, ops: Vec<(String, i64)>)
-> (Vec<i64>, Vec<String>)
run builds an Inventory from items and counts with an empty log,
applies every (name, n) op through Inventory::restock, and returns
(counts, log).
restock(name, n) scans items from the front. For every position whose
item equals name, it adds n to the corresponding count and appends one log
line. If no position matched, it appends a miss line instead.
The log format is exact, character for character:
hit: "<name> <signed n> -> <new count>" e.g. "bolt +3 -> 8"
"bolt -2 -> 3"
"w +0 -> 3"
miss: "<name> miss" e.g. "gizmo miss"
The sign is always written, including +0. In Rust that is the {:+} format
specifier: format!("{name} {n:+} -> {total}").
items ["bolt","nut"], counts [5,1], ops [("bolt",3)]
-> counts [8,1], log ["bolt +3 -> 8"]
items ["a","a"], counts [1,2], ops [("a",1)]
-> counts [2,3], log ["a +1 -> 2", "a +1 -> 3"] <- every match, not the first
The wall
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
|
| for (i, item) in self.items.iter().enumerate() {
| ---------------- immutable borrow occurs here
| self.note(format!("{item} ..."));
| ^^^^^^^^^ mutable borrow occurs here
This is where intermediate learners stall — sometimes for weeks. “Why can’t I call my own helper method while I’m looking at one of my own fields?”
The crucial fact: the checker CAN do this. The signature cannot say it.
Delete the helper and inline its body, and the same code compiles:
for (i, item) in self.items.iter().enumerate() {
if item == name {
self.counts[i] += n; // fine
self.log.push(format!("{item} ...")); // fine
}
}
Nothing changed about which memory is touched. What changed is that the
borrow checker can now see the individual places. Within one function body
it tracks self.items, self.counts and self.log as three separate places
and happily lets a shared loan of the first coexist with exclusive loans of
the other two. This is the “places, not variables” rule from item 3.3, doing
exactly what it says.
The moment you call self.note(...), that visibility is gone. note‘s
signature says &mut self. Not “&mut self.log“ — there is no such type. So
at the call site the checker must assume the callee may touch any part of
self, and an exclusive loan of the whole struct collides with the shared
loan of self.items that the loop is holding.
So the limitation is interprocedural specifically. It is not that the analysis is weak; it is that the type system has no way to write down “this method only touches these two fields”. Knowing that turns an inexplicable rejection into a named gap with known workarounds — which is a completely different feeling.
The fix, which is not discoverable
Destructure self at the top of the method:
let Inventory { items, counts, log } = self;
self is a &mut Inventory, so match ergonomics (item 3.11) gives you three
independent locals of type &mut Vec<String>, &mut Vec<i64>, &mut Vec<String>. They are separate variables now, so the checker keeps them
apart with no difficulty at all, and you write log.push(...) instead of
self.log.push(...).
Nobody guesses this. It is taught or it is not known, which is why it is spelled out here rather than left as an exercise.
Two other standard routes, for your toolbox:
-
Move the helper off
self. A free functionfn note(log: &mut Vec<String>, line: String)takes exactly what it needs, and the call site borrows exactly one field. -
Two-phase compute-then-mutate. Collect the log lines into a local
Vecwhile scanning, then append them afterwards. Item 3.6, step 3.
The roadmap, clearly labelled as roadmap
There is an in-flight language experiment called view types that would let a signature name the fields it touches:
fn note(&mut self.{ log }, line: String) // rust#155938, nightly, 2026
That would close this gap properly. It is #![feature]-gated on nightly, it
is unusable in this course or in any stable code, and you should treat it as
news rather than as a plan. The workarounds above are how this is done today
and will remain valid regardless.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.