7. Who owns this?

The finale of Mahler's Sixth promises its hammer for eighty minutes, and keeps the promise.

In chapter 3 a value moved and the program died, and we told you to wait. This is the chapter where wolf stops apologizing.

One question runs through everything wolf does with memory: who owns this, and how big is the granule? Every rule in this part — moves, mut, the escape hatches — is that question asked at a different size. Hold onto it and the rest of this part is consequences.

7.1 The error we owed you

Here is chapter 3’s program, unchanged:

fn main() -> !int {
    var names = List[str]()
    let roster = names
    print("{roster.len} {names.len}")
    0
}
error[E1001]: `names.len` is used here after its value moved away
 --> ./s1.lu:7:26
  |
6 |     let roster = names
  |                  ----- `names` moved here
7 |     print("{roster.len} {names.len}")
  |                          ^^^^^^^^^ used after the move
  |
  = note: `names.len` is part of `names`; moving one empties the other. Disjoint fields stay
    usable.
  = note: re-initializing the place (assigning to it) also makes it usable again.
help: to keep the original, copy it at the move
  |
6 |     let roster = copy names
  |
$ lupin roster.lu
roster.lu: trap(use-after-move): `names` was moved out and is uninitialized here [mem.tier0.move.2] at 96..105; `names` moved here at 65..70
$ echo $?
3

Two tools read the same six lines and said the same thing, and that is not redundancy. wolf and lupin are separate implementations of one written specification, and both of them cite [mem.tier0.move.2] — the compiler by proving that no execution reaches that read with names empty, the interpreter by running until one does and stopping there. This chapter shows you both wherever both have something to say, because two implementations agreeing is how you know the rule lives in the language rather than in a tool.

Now read the compiler’s text as the primary source it is. Four things are named, and each of them is a piece of the model:

The place. Not the variable names — the path names.len. A place is any path the language can name and the compiler can prove distinct from another: a variable, a field of a variable, a field of a field. Ownership is tracked at that granularity, not per variable.

The move site. let roster = names is underlined as where the value left. A diagnostic that only says “this is invalid” makes you find the cause; this one has already found it, because the checker knows the program point where ownership changed hands.

The granularity note.names.len is part of names; moving one empties the other. Disjoint fields stay usable.” Moving a value does not poison a whole struct. It empties one subtree — which is §7.2’s subject and the reason §7.5 can exist at all.

The two repairs, and they are different claims about your program. copy names says: I want a second, independent list, and I am paying for it here. Assigning to names says: the old value is gone and this name is starting over. Wolf will do either; it declines to guess.

What it will not do is invent a third thing — a null, a zeroed list, a silent reference to the list roster now owns. The value went somewhere, the somewhere is written on the line above, and no name holds it twice.

Exercise 7-1 (comprehension · wolf + lupin) — Chapter 3’s broken Pack program, with one line added:

struct Pack { lead: str, tail: str }
fn adopt(take w: str) -> str { w }
fn main() -> !int {
    var p = Pack { lead: "ada", tail: "grace" }
    let a = adopt(take p.lead)
    p.lead = "lin"
    let c = p.lead
    print("{a} {c} {p.tail}")
    0
}

Exercise 3-2’s version was rejected with E1001 at let c = p.lead. Predict both tools’ behavior now, and name the sentence in the E1001 diagnostic you saw in chapter 3 that already told you the answer.

7.2 Values are trees

Every value in wolf has exactly one owner, and values contain values, so the picture is a tree. Here is one:

struct Meta { author: str, words: int }
struct Doc { title: str, meta: Meta }
fn main() -> !int {
    var d = Doc { title: "regions", meta: Meta { author: "ada", words: 900 } }
    let who = move d.meta.author
    print("{who} wrote it")
    print("{d.title}, {d.meta.words} words")
    0
}
$ lupin doc.lu
ada wrote it
regions, 900 words

Draw d before the move and the shape is a tree with three leaves:

d --+-- title   "regions"
    +-- meta --+-- author  "ada"
               +-- words   900

move d.meta.author cuts one leaf off and hands it to who. Everything else is untouched, which the two prints check:

d --+-- title   "regions"
    +-- meta --+-- author  (moved away)
               +-- words   900

That is the whole data model. Ownership is a tree, a move transfers one subtree to a new owner, and the source path is empty afterward — empty, not invalid: nothing to free, nothing to read, one place waiting for a value.

move is the plain-expression spelling. At a call site the same act is written take, and the difference is grammatical rather than semantic:

struct Doc { title: str, body: str }
fn publish(take t: str) -> str { t }
fn main() -> !int {
    var d = Doc { title: "regions", body: "..." }
    let out = publish(take d.title)
    print("{out} {d.body}")
    print("{d.title}")
    0
}
warning[W1003]: `t` is taken, never touched, and returned
 --> ./s4.lu:5:12
  |
5 | fn publish(take t: str) -> str { t }
  |            ^^^^ consumption that consumes nothing
  |
  = note: the caller gives the value up only to receive it back; if callers could reasonably keep
    it, the signature is wrong.
help: drop the `take` (call sites drop theirs and keep their binding; owned payloads may then need a real transform)
  |
5 | fn publish(t: str) -> str { t }
  |

error[E1001]: `d.title` is used here after its value moved away
  --> ./s4.lu:10:13
   |
 8 |     let out = publish(take d.title)
   |                            ------- `d.title` moved here
 9 |     print("{out} {d.body}")
10 |     print("{d.title}")
   |             ^^^^^^^ used after the move
   |
   = note: re-initializing the place (assigning to it) also makes it usable again.
help: to keep the original, copy it at the move
   |
 8 |     let out = publish(take copy d.title)
   |

The interpreter agrees, and shows you something the compiler cannot: the program gets partway through before the rule catches it.

$ lupin publish.lu
regions ...
publish.lu: trap(use-after-move): `d.title` was moved out and is uninitialized here [mem.tier0.move.2] at 220..227; `d.title` moved here at 166..178
$ echo $?
3

The first print succeeded. d.body was never moved, so it is still d’s, and reading it is ordinary. The second print asks for the one path that left, and that is where the program stops. Field-granular is not a slogan here; it is the difference between the line that printed and the line that trapped.

An emptied place can be filled. Assign to it and it is a working name again:

struct Doc { title: str, body: str }
fn publish(take t: str) -> str { t }
fn main() -> !int {
    var d = Doc { title: "regions", body: "..." }
    let out = publish(take d.title)
    d.title = "regions, revised"
    print("{out} / {d.title}")
    0
}
$ lupin revise.lu
regions / regions, revised

Copying is a decision

Assignment moves, and it moves whatever the value contains. Meta is a string and an integer — small, and cheap enough that another language would duplicate it without telling you:

struct Meta { author: str, words: int }
fn main() -> !int {
    let a = Meta { author: "ada", words: 900 }
    let b = a
    print("{a.words} {b.words}")
    0
}
error[E1001]: `a.words` is used here after its value moved away
 --> ./s6.lu:8:13
  |
7 |     let b = a
  |             - `a` moved here
8 |     print("{a.words} {b.words}")
  |             ^^^^^^^ used after the move
  |
  = note: `a.words` is part of `a`; moving one empties the other. Disjoint fields stay usable.
  = note: re-initializing the place (assigning to it) also makes it usable again.
help: to keep the original, copy it at the move
  |
7 |     let b = copy a
  |

Change one word and the program runs:

struct Meta { author: str, words: int }
fn main() -> !int {
    let a = Meta { author: "ada", words: 900 }
    let b = copy a
    print("{a.words} {b.words}")
    0
}
$ lupin meta.lu
900 900

The rule worth extracting: duplication is a decision made at the site where it happens, not a property of the type looked up somewhere else. A reader of let b = copy a knows a second Meta now exists without knowing what Meta contains, and a reader of let b = a knows a is finished. Scalars — the integers and floats the machine copies in a register anyway — are the exception the machine forces and the language admits: let n = m on an int leaves m alone. Everything you define moves.

Coming from C++: this is the copy-constructor question answered the other way round. C++ made copying the default and asked authors to opt out with = delete or a move constructor; wolf makes handing over the default and asks callers to opt in with a word. The C++ ordering produces the copy nobody meant, at the call nobody reads; this ordering produces a compile error, at the line where the decision belongs.

Exercise 7-2 (fingers · lupin) — Given struct Wolf { name: str, call: str } and struct Den { alpha: Wolf, beta: Wolf }, draw the ownership tree of a Den before running anything: one box per value, one arrow per field. Then move the deepest leaf out with move and verify, by printing them, that the leaf’s sibling and its cousins are all still usable.

Exercise 7-3 (extension · wolf + lupin) — Using one struct, one function taking take, and nothing else, write the smallest program that traps use-after-move through a field. Predict the compiler’s E-code and the interpreter’s trap kind before checking both. Why does the exercise say “through a field” — what would be different, and what the same, with a bare local?

Exercise 7-4 (comprehension · lupin) — Every field of P is an int. Predict what the second line of main does to a:

struct P { x: int, y: int }
fn main() -> !int {
    let a = P { x: 1, y: 2 }
    let b = a
    print("{a.x} {b.y}")
    0
}

Then change one word so it prints 1 2, and say what the changed program costs that the original did not.

7.3 Borrowing without the word

Chapter 4 stated the parameter rule and moved on. Here it is again, with the reason attached:

struct Doc { title: str, words: int }
fn longest(docs: List[Doc]) -> str {
    var best = ""
    var most = 0
    for d in docs {
        if d.words > most {
            most = d.words
            best = copy d.title
        }
    }
    best
}
fn main() -> !int {
    var docs = List[Doc]()
    (mut docs).push(Doc { title: "regions", words: 900 })
    (mut docs).push(Doc { title: "moves", words: 640 })
    let title = longest(docs)
    print("{title}, longest of {docs.len}")
    0
}
$ lupin longest.lu
regions, longest of 2

longest reads a list of two documents and main uses the list afterward. No annotation was written at either end, the list never changed hands, and the only duplication in the program is the one the loop asks for by name. That is the mode an unwritten parameter has: the callee reads the caller’s value for the duration of the call, and the caller keeps it. Returns are the other direction and the other verb — best moves out to title, which is §7.2’s rule applied at a function boundary.

The rule fits in one sentence: a parameter with no mode is the caller’s, for reading, until the call returns. Absence is the syntax, and absence is the right syntax for the common case.

What deserves your attention is what is not in that signature. longest takes a List[Doc]. There is no reference type in it, no sigil, no annotation relating the argument’s lifetime to the return value’s — and there is no such type anywhere in the language for a signature to mention. Wolf’s cross-function story is entirely modes: read, mut, take. A function says what it does with your value, not how long it may keep a pointer to it, because it may not keep a pointer to it at all.

That is a real restriction and §7.6 pays for it honestly. It is also what buys the silence: there is nothing to annotate here because there is nothing that could escape.

7.4 mut at both ends

The shelf, a document store we will keep for the rest of this part, starts as two structs and one function that changes something:

struct Doc { title: str, words: int }
struct Shelf { docs: List[Doc] }
fn add(mut s: Shelf, d: Doc) {
    (mut s.docs).push(d)
}
fn main() -> !int {
    var shelf = Shelf { docs: List[Doc]() }
    add(mut shelf, Doc { title: "regions", words: 900 })
    add(mut shelf, Doc { title: "moves", words: 640 })
    print("{shelf.docs.len} docs")
    0
}
$ lupin shelf.lu
2 docs

Count the muts: one in the declaration, one at each call, and one on each receiver. mut is written wherever a value is about to be written through, including on the left of a method call — (mut s.docs).push(d) says out loud that push takes its receiver exclusively.

Drop the one at the call site and the program does not build:

struct Doc { title: str, words: int }
struct Shelf { docs: List[Doc] }
fn add(mut s: Shelf, d: Doc) {
    (mut s.docs).push(d)
}
fn main() -> !int {
    var shelf = Shelf { docs: List[Doc]() }
    add(shelf, Doc { title: "regions", words: 900 })
    print("{shelf.docs.len}")
    0
}
error[E1007]: `add` declares `s` as `mut`, but the call site does not say so
  --> ./s11.lu:11:9
   |
 6 | fn add(mut s: Shelf, d: Doc) {
   |            - the parameter is declared here
...
11 |     add(shelf, Doc { title: "regions", words: 900 })
   |         ^^^^^ this argument
   |
help: write the argument's mode: `mut …`
   |
11 |     add(mut shelf, Doc { title: "regions", words: 900 })
   |

This is decision X1, and it is the one place where wolf asks you to type something a compiler could have inferred. The argument for it is not about the compiler. It is this: open a file of wolf you have never seen, run one search, and you have that file’s complete mutation surface.

$ grep -n '(mut ' shelf.lu
3:fn add(mut s: Shelf, d: Doc) {
4:    (mut s.docs).push(d)
8:    add(mut shelf, Doc { title: "regions", words: 900 })
9:    add(mut shelf, Doc { title: "moves", words: 640 })

Four lines out of twelve: one declaration that claims the right to write, and three places that exercise it. Nothing else in the file can change anything, and you learned that without reading the body of add or the body of push. Add grep 'var ' for locals and the audit is complete, with no false negatives — which is the property a required annotation buys and an inferred one cannot. In a language where mutation is invisible at the call site, the same question takes a call graph.

Swift got here first: inout parameters are marked at the call with & for exactly this reason, and wolf’s debt is direct. The difference is scope — Swift’s & marks one parameter kind, while wolf spells every non-default mode at both ends, so take reads the same way mut does.

Exercise 4-3 (extension · lupin) — Give a List[int] a grow and a shrink, both taking the list mut, and call each of them. Then, without running anything, state how you would find every mutation in your program with one search.

Exercise 7-6 (fingers + spelunking · lupin) — Write swap for two ints using mut at both ends, and verify it. Then state the single search you would run over a strange codebase to find every line that can mutate anything — and what property of the language makes the search complete.

7.5 Field-granular exclusivity

mut means exclusive: for the duration of the call, that argument is the only way to reach the place. Two mut arguments in one call are therefore a question, and wolf answers it by looking at the paths.

fn swap(mut a: str, mut b: str) {
    let t = move a
    a = move b
    b = t
}
struct Doc { title: str, subtitle: str }
fn main() -> !int {
    var d = Doc { title: "draft", subtitle: "regions" }
    swap(mut d.title, mut d.subtitle)
    print("{d.title} / {d.subtitle}")
    0
}
$ lupin swap.lu
regions / draft

d.title and d.subtitle are different places, so two exclusive claims on them coexist, and one swap works on two fields of one struct. Point both arguments at the same field and the claims collide:

fn swap(mut a: str, mut b: str) {
    let t = move a
    a = move b
    b = t
}
struct Doc { title: str, subtitle: str }
fn main() -> !int {
    var d = Doc { title: "draft", subtitle: "regions" }
    swap(mut d.title, mut d.title)
    print("{d.title}")
    0
}
error[E1002]: `d.title` cannot go `mut` here: it overlaps `d.title`, already passed `mut` in this call
  --> ./s13.lu:12:27
   |
12 |     swap(mut d.title, mut d.title)
   |              ------- `d.title` is passed `mut` here
   |                           ^^^^^^^ second exclusive claim on the same place
   |
   = note: the same place twice is never disjoint.
$ lupin swap.lu
swap.lu: trap(exclusivity): `d.title` is accessed as `mut` while `d.title` is held as `mut`; the paths conflict [mem.model.path.disjoint] at 219..230; `d.title` held here at 206..217
$ echo $?
3

The rule the two tools share is one line, and wolf --explain E1002 states it in general: two paths conflict when one is a prefix of the other. d.title conflicts with itself and with d; it does not conflict with d.subtitle. Exclusivity is that test, run at every call site, over paths rather than variables.

A method that touches part of its receiver can say so, and then callers keep the rest:

struct Doc { title: str, subtitle: str, words: int }
impl Doc {
    fn retitle(mut self.{title, subtitle}, t: str) {
        self.subtitle = move self.title
        self.title = t
    }
}
fn main() -> !int {
    var d = Doc { title: "draft", subtitle: "", words: 900 }
    (mut d).retitle("regions")
    print("{d.title} / {d.subtitle} / {d.words}")
    0
}
$ lupin retitle.lu
regions / draft / 900

mut self.{title, subtitle} is a view set: a promise, in the signature, about which paths the method touches. It is a promise the compiler holds you to:

struct Doc { title: str, subtitle: str, words: int }
impl Doc {
    fn retitle(mut self.{title, subtitle}, t: str) {
        self.title = t
        self.words += 1
    }
}
fn main() -> !int {
    var d = Doc { title: "draft", subtitle: "", words: 900 }
    (mut d).retitle("regions")
    0
}
error[E1008]: this method declares a view of `self.{title, subtitle}`, but touches `self.words`
 --> ./s15.lu:8:9
  |
6 |     fn retitle(mut self.{title, subtitle}, t: str) {
  |                -------------------------- the view set is declared here
7 |         self.title = t
8 |         self.words += 1
  |         ^^^^^^^^^^ outside the declared view
  |
  = note: callers rely on the view: they may use the other fields while this method runs. Add the
    field to the view set, or take plain `mut self`.

The footprint is part of the signature, so widening it is a visible change to your callers rather than a quiet one — which is the same principle as the call-site mut, applied one level in.

Coming from Rust: the disjoint-fields half of this section is not the contrast. Rust’s borrow checker sees through field paths inside a function body, and passing &mut d.title and &mut d.subtitle to one call compiles there too; the 2021 edition extended the same path-precision to closure captures. The contrast is the view set. A Rust method is &mut self or nothing — there is no way to write in a signature that a method touches only two of six fields — so a caller holding a borrow of self.words cannot call d.retitle(), and the advice is to refactor: split the struct, or make the method a free function taking the fields it wants. Rust has been circling a language answer to this under the name “view types” since 2021. Wolf put the footprint in the signature, which costs a signature that says more and buys callers that need no refactor.

Exercise 4-4 (comprehension + spelunking · wolf) — Given struct Inner { n: int } and struct P { a: Inner, b: Inner }, one of bump(mut p.a.n, mut p.b.n) and wide(mut p.a, mut p.a.n) is legal and one is not. Say which and why, then check yourself against the compiler and against wolf --explain E1002.

Exercise 7-7 (comprehension · wolf + lupin) — The simplest possible exclusivity violation is one place claimed twice: bump2(mut n, mut n) where bump2 takes two mut ints. Predict what each tool says, then answer the design question hiding under it: if the call were allowed, what would n be afterward — and why is “it depends on the body” the real reason for the rule?

Exercise 7-8 (comprehension + fingers · lupin) — Four call shapes against struct P { a: Q, b: Q }, struct Q { n: int }. Verdict for each, before checking any:

  1. f(mut p.a, mut p.b)
  2. f(mut p.a.n, mut p.b.n)
  3. f(mut p.a, mut p.a.n)
  4. f(mut p, mut p.b)

7.6 Why there are no lifetimes

Wolf has no lifetime annotations, and will not grow them (D10). That is a trade, not an absence, and this section shows both sides so you can judge it.

What Rust buys

Here is the program shape where Rust’s precision earns its keep — a tokenizer that hands back borrowed slices of an input buffer it does not own:

pub struct Token<'a> {
    pub text: &'a str,
}

pub fn tokens(input: &str) -> Vec<Token<'_>> {
    input
        .split_whitespace()
        .map(|text| Token { text })
        .collect()
}

No byte of the input is copied, each token is a pointer and a length aimed into it, and 'a is what makes the whole thing sound: a Token<'a> cannot outlive the &'a str it points into, the compiler proves it, and the caller can hold the tokens in a struct, return them, and store them in a collection. That is a genuinely excellent piece of language design, and no honest account of wolf’s model can skip it.

What it costs

Three costs, all of them measured in the same currency — how much of the memory model shows up in signatures that have nothing to do with memory.

The first is virality. A lifetime in one struct is a lifetime in every struct that holds it and every function that touches one, so 'a propagates outward through code whose author has no opinion about borrows.

The second is variance: a second type system, mostly invisible, that decides when &'long T may stand in for &'short T. It is sound, it is necessary, and most working Rust programmers meet it only as an error message.

The third is the sound programs the checker rejects. Non-lexical lifetimes fixed most of them in 2018; NLL “case #3” — a borrow that is live on one path and dead on the other, which the region-based checker cannot see — was the known remainder, and the Polonius work that addresses it has been under way since 2018. That is not an indictment of anyone. It is a measurement of how hard the last mile of this design is, and it is the mile wolf declined to walk.

Wolf’s position

Wolf takes the coarser deal deliberately. Borrows exist, they are inferred, they are last-use rather than scope, and they are invisible — because they never cross a signature. A function’s relationship to your memory is a mode, and modes have no duration to name: the call is the duration.

So the Rust program above has no wolf transcription, and this is what wolf writes instead:

struct Tok { start: int, end: int }
fn tokens(line: str) -> List[Tok] {
    var out = List[Tok]()
    var i = 0
    var start = 0
    while i <= line.len {
        if i == line.len || line[i..i + 1] == " " {
            if i > start { (mut out).push(Tok { start: start, end: i }) }
            start = i + 1
        }
        i += 1
    }
    out
}
fn main() -> !int {
    let line = "the wolf runs"
    for t in tokens(line) {
        print("{line[t.start..t.end]}")
    }
    0
}
$ lupin tokens.lu
the
wolf
runs

A Tok is two integers. It copies freely, it stores anywhere, it outlives anything, and it means nothing without the string it indexes — which the caller holds, and which line[t.start..t.end] re-derives at each use, checked. The input is still not copied. What changed is who holds the proof: Rust’s compiler proves the slice is valid, and wolf’s caller keeps the string in scope and pays a bounds check.

Count the trade honestly. Wolf loses the API that hands out borrowed slices to arbitrary callers, and it loses the compile-time proof that the range is in bounds. It keeps the input un-copied, and it spends no character of any signature on memory. For the third option — memory that outlives the frame that made it, shared without copying — you name a granule instead of a lifetime, and that is the next chapter.

Coming from Rust: the honest summary is that wolf is betting on the distribution. Rust’s annotations are the price of a guarantee you need on the hot path of a parser and nowhere in the request handler; wolf charges nothing in the handler and charges a bounds check or a region in the parser. If your programs are mostly parsers, that is a worse bet. Say so — that is the shape of the argument, and it is settled by measurement, not by taste.

Exercise 7-9 (spelunking · wolf) — Run wolf --explain E1001 and read all of it. Quote the sentence that licenses re-initialization, the phrase that states field granularity, and the one word in the first paragraph that makes let b = a and f(take a) the same subject.

Exercise 7-10 (design) — Rust’s zero-copy parser hands out &str slices of an input buffer it does not own, with lifetimes proving the buffer outlives every slice. Sketch the wolf alternatives — copying the token text, returning byte ranges into a caller-held string, or parsing into memory the caller names — and argue which one a tokenizer library should ship. What does each cost, and who pays it?

7.7 What the machine does

A mode is a promise, and a promise a compiler can rely on is an optimization it can perform. That is the whole of this section: the facts you write for the reader are the facts the machine gets.

Take swap(mut d.title, mut d.subtitle). The compiler passes each mut argument as an address, and attaches two facts to each: this address is readable and writable for the width of the value, and no other address in this call reaches the same bytes. C programmers have met the second fact before — it is restrict, and the difference is who is asserting it. In C the programmer asserts it and the compiler believes; a wrong restrict is undefined behavior discovered years later by a customer. In wolf the compiler derives it from [mem.tier0.excl.1] and rejects the program that would make it false. Same fact, and the two languages differ only on the question of who could be lying.

An unwritten parameter carries the complementary fact: for the duration of the call, nothing writes this place. A value that cannot change during a call is a value whose loads can be hoisted out of a loop and whose fields can be kept in registers across a call the optimizer cannot see into.

A move is the cheapest of the three. Handing a value over is a copy of its top-level words — for a List, the pointer, the length, and the capacity — plus a promise that the source will never be read again. The promise is what E1001 enforces, and it is why the copy is enough: with no second reader, there is nothing to keep consistent, and no destructor runs at the source. Moving a Doc between two owners costs the same as moving an int between two owners, whatever the Doc contains.

The shape to keep is that none of these facts are separate from the teaching. The optimizer’s model of your program and the model in your head are the same model, written once, in the signature.

Exercise 7-11 (fingers · lupin REPL) — In the REPL, move a string out of one binding into another, then read both — the corpse first. What does the session do that a compiled program cannot, and which clause tag names the reason the trap did not end your session?

The shelf, and what it cannot do

The value rules are complete. Every one of them has been a way of answering “who owns this” with the same answer: exactly one place, and the language will tell you which.

That answer has a ceiling, and the shelf hits it as soon as two parts of the program want the same document:

struct Doc { title: str, words: int }
struct Shelf { docs: List[Doc], recent: List[Doc] }
fn read(mut s: Shelf, i: int) {
    (mut s.recent).push(copy s.docs[i])
}
fn revise(mut s: Shelf, i: int, w: int) {
    s.docs[i].words = w
}
fn main() -> !int {
    var shelf = Shelf { docs: List[Doc](), recent: List[Doc]() }
    (mut shelf.docs).push(Doc { title: "regions", words: 900 })
    read(mut shelf, 0)
    revise(mut shelf, 0, 1200)
    print("shelf: {shelf.docs[0].words}, recent: {shelf.recent[0].words}")
    0
}
$ lupin shelf.lu
shelf: 1200, recent: 900

The recent list holds a document that stopped being true. It was accurate when it was copied and it has been drifting ever since, and the two tools this chapter has for the problem are both worse than the problem: copy on every read and go stale, or store indices and hand-maintain them against every insert and removal. A tree of single owners has no way to say “these two lists are looking at one document”, and no way at all to say “these five thousand objects live and die together.”

You can feel the shape of the missing feature. It is not a smarter borrow. It is a bigger granule.

Exercise 7-12 (extension · lupin) — The longest common subsequence of two line lists is the skeleton every diff tool hangs on. Build the DP table as a List[List[int]] and return its corner. For two three-line “files” of your choosing, compute the answer on paper first, then check it. Note what the signature says about ownership: which of your parameters were moved, and how many annotations did it take to say so?

Exercise 7-13 (comprehension + extension · lupin) — Extend 7-12 into a printing diff: walk the finished table backward from the corner, emitting two spaces for common lines, - for deletions, + for additions. Before running, predict the full output for old = wolf / moon / elk and new = wolf / elk / river. Then explain why the walk must go backward.