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
= deleteor 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.titleand&mut d.subtitleto 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 selfor 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 ofself.wordscannot calld.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:
f(mut p.a, mut p.b)f(mut p.a.n, mut p.b.n)f(mut p.a, mut p.a.n)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.