Skip to content

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

Medium Primitives

Split borrows: the checker reasons about places, not variables

This exercise gives you a struct with three fields and a method that reads one field while writing the others. As written it does not compile. Your job is to make it compile without changing what it does.

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>)

The contract

run builds an Inventory from items and counts with an empty log, applies each (name, n) op in order through Inventory::restock, and returns (counts, log).

restock(name, n) scans items from index 0 upward. For every index i where items[i] == name, it adds n to counts[i] and appends one log line that shows the new count. If no index matched, it appends one miss line instead. Names are compared exactly: the comparison is case sensitive and byte for byte, with no trimming.

Inputs you can rely on:

  • items and counts have the same length. Either may be empty.
  • items may contain the same name more than once.
  • ops may be empty, in which case counts comes back unchanged and log is empty.
  • Every running total stays within the range of i64, so you do not need to handle overflow.

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 of n is always written, including +0 for zero. The {:+} format specifier does this: format!("{name} {n:+} -> {total}"). The new count is printed without a forced sign, so a negative total prints as, for example, -4.

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 just the first

The error

The starter code routes every log line through a small helper, fn note(&mut self, line: String). Compiling it gives:

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
   |                          immutable borrow later used here
...
   |                 self.note(format!("{item} {n:+} -> {}", self.counts[i]));
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

The loop holds a shared borrow of self.items for as long as it runs, because item points into that vector. Calling self.note(...) inside the loop needs an exclusive (&mut) borrow, and the error says that exclusive borrow conflicts with the shared one.

Why inlining the helper compiles

Replace the call with the helper's body and the same logic compiles:

for (i, item) in self.items.iter().enumerate() {
    if item == name {
        self.counts[i] += n;
        self.log.push(format!("{item} {n:+} -> {}", self.counts[i]));
    }
}

The program touches the same memory as before. The difference is what the borrow checker can see. The borrow checker tracks borrows of places, not just of whole variables. A place is a path to a piece of memory, such as self.items or self.log. Inside one function body, the checker knows that self.items, self.counts and self.log are different fields, so a shared borrow of self.items can exist at the same time as exclusive borrows of self.counts and self.log. Borrowing different fields of one struct at the same time like this is called a split borrow. The article on aliasing and mutability covers places in more detail.

A method call hides that detail. The borrow checker checks each function on its own and, at a call site, looks only at the callee's signature, not its body. note's signature says &mut self, which means exclusive access to the whole Inventory. Rust has no stable syntax for a parameter type that means "exclusive access to only the log field of self". So at the call site the checker must assume note may touch any field, including items, and that conflicts with the loop's borrow of self.items.

In this example, then, the rejection comes from what a method signature can express, not from the code touching overlapping data.

Fix: destructure self

At the top of restock, split self into its fields:

let Inventory { items, counts, log } = self;

self has type &mut Inventory. When a pattern for a struct is matched against a reference to that struct, each field is bound by reference (this is called a binding mode; see binding modes). Here every field is bound by ref mut, giving three local variables of types &mut Vec<String>, &mut Vec<i64> and &mut Vec<String>. Each one borrows a different field, so the checker lets you read items while you write to counts and log. In the loop you then write log.push(...) instead of calling the helper.

Two other common fixes:

  • Make the helper take only what it needs. A free function fn note(log: &mut Vec<String>, line: String) called as note(&mut self.log, line) borrows only the log field.
  • Compute first, then mutate. Collect the new log lines in a local Vec while scanning, and append them to self.log after the loop has finished with self.items. The copy out of the loan exercise uses the same idea.

If you stop calling note, delete it. An unused private method produces a dead_code warning, and the grade treats warnings as errors.

Possible future language support

Rust language designers have discussed extensions, sometimes called view types, that would let a signature name the fields a method uses. None of them is part of stable Rust, and this exercise does not depend on them. The fixes above work on stable Rust today.

Your submission must compile, pass the tests and pass cargo clippy -- -D warnings.