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 4:26; `names` moved here at 3:18
$ echo $?
3

Two tools read the same six lines and said the same thing. 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. The diagnostic names the path names.len, finer than the variable. 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.” A move empties one subtree, not the whole struct; that 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.

It invents no 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.

What each construct does

E1001 fired because a List is not one of the types wolf copies on its own. Here is the whole default, one row for each way a program hands a value somewhere. Each row gets its section below; the table is up front so you can come back to it.

You writeThe value is CopyAny other value
let b = a, b = a, a field in a struct literalcopies; a stays livemoves; a is empty
f(a), with no modef reads it; you keep af reads it; you keep a
f(mut a)f writes it; you keep af writes it; you keep a
f(take a)moves; a is emptymoves; a is empty
move amoves; a is emptymoves; a is empty
copy aa second valuea second, independent value: deep for a struct or a List; a str shares its bytes
return v, or a body’s last valuecopies out to the callermoves out to the caller
(mut xs).push(a), xs[i] = acopies into the containercopies into the container; take a moves it there
a = … after a movea is live againa is live again

Two clauses generate every row. The first is [mem.tier0.move.1]: “Assignment, initialization, take arguments, and return move non-Copy values. The source place becomes uninitialized.” The second, [mem.tier0.move.3], makes the exception: “types opting into Copy copy implicitly on what would otherwise move.” The Copy types are built into the language, and each is a value whose copy costs what handing it over would: the integers, the floats, bool, char, byte, and str. The string is the one people miss, and the specification gives it a sentence of its own: “A str is Copy — its two-word view copies freely” ([mem.region.escape]). A str is a pointer and a length over bytes nobody can change, so a second view costs two words and shares everything else. A struct, a tuple or an enum is never Copy, whatever its fields.

So the line that ended chapter 3’s program has the opposite outcome on a str, and on every other type in that list:

fn main() -> !int {
    let lead = "ada"
    let done = true
    let mark = 'w'
    let words = 900
    let a = lead
    let b = done
    let c = mark
    let n = words
    print("{lead} {a} / {done} {b} / {mark} {c} / {words} {n}")
    0
}
$ lupin copyread.lu
ada ada / true true / w w / 900 900

Notice also what the table does not have: a row where an argument written without a mode moves. f(a) lends a for the call, whatever its type, and §7.3 says why. Only take gives a value away at a call.

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 and after the move and the shape is a tree with three leaves. The drawing below is not made by hand, and neither is any other in this chapter: the book runs the program under the interpreter’s own record of every place, statement by statement (the record that produces trap(use-after-move)), and draws the line it names from that record.

before line 5: let who = move d.meta.author / d             Doc / ├─ title      "regions" / └─ meta       Meta /    ├─ author  "ada" /    └─ words   900 /  / after line 5 / d             Doc / ├─ title      "regions" / └─ meta       Meta /    ├─ author  moved · move 5:15   (changed) /    └─ words   900 / who           "ada"   (new)

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

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: nothing to free, nothing to read, one place waiting for a value.

Nothing to read is a rule and not a manner of speaking. Add one line to the program above — the same move, and then a read of the leaf it emptied — and the compiler stops you at the read:

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.meta.author} wrote it")
    0
}
error[E1001]: `d.meta.author` is used here after its value moved away
  --> ./part-movedleaf.lu:10:13
   |
 8 |     let who = move d.meta.author
   |                    ------------- `d.meta.author` moved here
 9 |     print("{who} wrote it")
10 |     print("{d.meta.author} wrote it")
   |             ^^^^^^^^^^^^^ 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 who = move copy d.meta.author
   |

The caret is under the path, not under the move: the move is legal and the read is not. And the fix-it is the one worth remembering, because it is the whole choice the move expresses — move copy d.meta.author takes a copy and leaves the leaf where it is.

The interpreter reaches the same verdict from the other side, at run time, and it is worth watching it get there:

$ lupin moved.lu
ada wrote it
moved.lu: trap(use-after-move): `d.meta.author` was moved out and is uninitialized here [mem.tier0.move.2] at 7:13; `d.meta.author` moved here at 5:15
$ echo $?
3

The first print ran. who holds the author now and printing it is ordinary. The second asks d for a path that left, and that is where the program stops — the same rule the compiler stated before anything ran. d.title and d.meta.words are untouched throughout, which is what the program at the top of this section prints: moving one leaf empties that leaf and nothing else.

move is the plain-expression spelling. At a call site the same act is written take, and the difference is grammatical, not 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` and hand back `copy t` (call sites drop theirs and keep their binding; a `read` parameter returned without the `copy` is E1002, #366)
  |
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 7:13; `d.title` moved here at 5:23
$ 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 granularity 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

Re-initialization and the Copy row of §7.1’s table meet in exercise 7-1’s program. Here it is with one more line, a second read of the field:

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}")
    print("{p.lead}")
    0
}
$ lupin pack.lu
ada lin grace
lin

The compiler runs it too, and adds the W1003 you met with publish: adopt hands back what it took. The program is about the call site, where three lines do three different things to the one place p.lead. Line 5 hands the field to adopt. take moves even a Copy value, because giving the value away is what the word says:

before line 5: let a = adopt(take p.lead) / p        Pack / ├─ lead  "ada" / └─ tail  "grace" /  / after line 5 / p        Pack / ├─ lead  moved · take 5:19   (changed) / └─ tail  "grace" / a        "ada"   (new)

Line 6 fills the emptied field again, and the place is live:

before line 6: p.lead = "lin" / p        Pack / ├─ lead  moved · take 5:19 / └─ tail  "grace" / a        "ada" /  / after line 6 / p        Pack / ├─ lead  "lin" · re-initialized 6:5   (changed) / └─ tail  "grace" / a        "ada"

Line 7 is the one the chapter had not shown you: a plain = that reads a field. p.lead is a str, and a str is Copy, so c gets a copy and p.lead stays where it was, which is why the last line can print it:

before line 7: let c = p.lead / p        Pack / ├─ lead  "lin" · re-initialized 6:5 / └─ tail  "grace" / a        "ada" /  / after line 7 / p        Pack / ├─ lead  "lin" · re-initialized 6:5 / └─ tail  "grace" / a        "ada" / c        "lin" · copy of p.lead   (new)

Had the field been a Meta, line 7 would have moved it out of p, and the last line would be E1001.

Copying is a decision

For a type you define, 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. Hand it to a new name and only the new name has it:

struct Meta { author: str, words: int }
fn main() -> !int {
    let a = Meta { author: "ada", words: 900 }
    let b = a
    print("{b.author}, {b.words} words")
    0
}
$ lupin handover.lu
ada, 900 words
before line 4: let b = a / a          Meta / ├─ author  "ada" / └─ words   900 /  / after line 4 / a          moved · plain = 4:13   (changed) / ├─ author  gone with a / └─ words   gone with a / b          Meta   (new) / ├─ author  "ada" / └─ words   900

The whole struct moved, its str and its int with it, though each alone would have copied: the rule is the type’s, and Meta is not Copy. Read the old name afterward and the compiler stops 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
before line 4: let b = copy a / a          Meta / ├─ author  "ada" / └─ words   900 /  / after line 4 / a          Meta / ├─ author  "ada" / └─ words   900 / b          Meta · copy of a   (new) / ├─ author  "ada" / └─ words   900

The rule worth extracting: for the types you define, 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 on a Meta knows a is finished. The Copy types of §7.1’s table are the exception the language admits, each a value whose copy costs what its handover would: on the integers, the floats, bool, char, byte and str, let b = a leaves a alone. Every struct, tuple and enum moves, whatever its fields; exercise 7-4 tests that on a struct of nothing but ints.

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 for every type you define 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 (break-it-on-purpose) · 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, and the list never changed hands or was duplicated. (The loop’s copy d.title says that best keeps a title of its own; a title is a str, so a plain = would have copied it too.) 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: best leaves longest for title, by the return row of §7.1’s table. A str is Copy, so that is two words copied; a Doc returned the same way would move. Either way longest’s own binding ends with the call, so nothing is left behind to read.

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.

The signature says as much by what it leaves out. longest takes a List[Doc], with no reference type and no annotation relating the argument’s lifetime to the return value’s; no such type exists 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 shows both sides of it. 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 the one place where wolf asks you to type something a compiler could have inferred. The argument for it: 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. That 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 7-6 (extension · wolf + lupin). A bounded buffer: struct Buf { items: List[int], cap: int }. Write push(mut b, x), which adds x and returns the new length, or the error row {full} when b already holds cap items; and grow(mut b, extra), which builds a larger Buf and moves the old list into it. Fill a buffer of two, watch the third push fail, grow it by two, and push again. The cap is yours because the list’s is not: a wolf List grows on its own and exposes no capacity to read or reserve. How many elements does grow copy?

Exercise 7-7 (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 9:23; `d.title` held here at 9:10
$ 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, not 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. That is the call-site mut principle, applied one level in.

Coming from Rust: Rust checks disjoint field paths too: its borrow checker sees through them inside a function body, passing &mut d.title and &mut d.subtitle to one call compiles there as well, and 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 7-8 (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-9 (comprehension + spelunking · wolf + lupin). Four call shapes against struct P { a: Q, b: Q }, struct Q { n: int }. In each, f takes both of its arguments mut and writes through both, so its signature is the two argument types with mut on each: shape 1 calls fn f(mut x: Q, mut y: Q), shape 2 fn f(mut u: int, mut v: 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)

Then check yourself against the compiler, and find the sentence in wolf --explain E1002 that decides all four.

7.6 Why there are no lifetimes

Wolf has no lifetime annotations, by design, and will not grow them. 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. No 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 measures 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 is not Copy (no struct is), but a copy of it is two words, 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 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-10 (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-11 (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.

None of these facts is 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-12 (fingers · lupin REPL). In the REPL, move a string out of one binding into another with move (a plain = would copy it), 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?

7.8 Deciding at run time

Every trait call in chapter 5 was settled before the program ran. The compiler knew the concrete type at each call (Row, Header, int) and wired the call straight to the impl, so r.text() costs what any call costs and carries nothing at run time to say which text it is. That holds for a primitive too, and on a primitive the call spells the trait’s name, the qualified form. The method sugar n.text() does not reach a primitive receiver; the compiler refuses it by name.

trait Text {
    fn text(self) -> str
}
impl Text for int {
    fn text(self) -> str { "int({self})" }
}
fn main() -> !int {
    let n: int = 7
    print(Text.text(n))
    0
}
$ lupin text.lu
int(7)

The other mode, deciding at run time which impl a call reaches, exists too, and wolf spells it the way it spells every deliberate cost, out loud: dyn. A dyn Draw is a pair of pointers, one to the value and one to a table of its trait’s methods, and a function that takes one serves every implementing type through the same two loads:

trait Draw {
    fn draw(self) -> str
}
struct Dot { x: int }
struct Ring { r: int }
impl Draw for Dot {
    fn draw(self) -> str { "dot at {self.x}" }
}
impl Draw for Ring {
    fn draw(self) -> str { "ring of {self.r}" }
}
fn render(o: dyn Draw) -> str { o.draw() }
fn main() -> !int {
    let d = Dot { x: 3 }
    let r = Ring { r: 9 }
    print(render(d as dyn Draw))
    print(render(r as dyn Draw))
    0
}
$ lupin draw.lu
dot at 3
ring of 9

render is compiled once, for every Draw there is or will be. At o.draw() the machine loads the table pointer out of the pair, loads the draw entry out of the table, and calls through it, with the pair’s other half as self. Two loads and an indirect call, per call, is the price of not knowing the type until the pair arrives, and it is paid at the one site that wrote dyn. d.draw() on the Dot binding in main would still be wired straight. Which is the whole of the trade in one sentence: a trait bound on a generic, chapter 5’s total[T: Add], is a separate copy of the function per type and no loads; a dyn is one copy of the function and two loads per call.

Now look at what feeds render: a binding, cast with as dyn Draw. That is a rule, and it is this chapter’s rule. The pair owns nothing. Its data half points at the operand, so the operand must be a place in §7.2’s sense (a binding, a field, an index), and the pair reads that place for the duration of the call, the way an unwritten parameter in §7.3 reads its argument and hands it back. The binding keeps ownership, the pair borrows, and the call is the duration. Hand the cast a temporary instead and there is nothing to point at once the expression ends:

trait Draw {
    fn draw(self) -> str
}
struct Dot { x: int }
impl Draw for Dot {
    fn draw(self) -> str { "dot at {self.x}" }
}
fn render(o: dyn Draw) -> str { o.draw() }
fn main() -> !int {
    print(render(Dot { x: 3 } as dyn Draw))
    0
}
error[E0810]: a temporary has no home — bind it first
  --> ./s19.lu:13:18
   |
13 |     print(render(Dot { x: 3 } as dyn Draw))
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^ this value would not outlive the cast
   |
   = note: the dyn pair's data half points AT the operand, so the operand must be a place: a
     binding, a field, or an index. Bind it first — `let home = …` — and cast the binding:
     `home as dyn Draw`.

a temporary has no home — bind it first. The fix is the one the note spells: let home = Dot { x: 3 }, then home as dyn Draw. Read it beside §7.6, because it is the same decision at one more site. A pair that outlived its operand would need a lifetime to say so, and wolf has none to give it; so the cast is refused where the operand would die, and the one visible line the reader writes is the language declining to allocate a home behind your back.

Exercise 7-13 (extension · lupin). Add a third shape to the Draw example, and then make render count its calls: what has to change, and what does not?

Exercise 7-14 (design). The cast-a-binding rule exists because the dyn pair points at its operand rather than owning it. What would the language have to invent for Dot { x: 3 } as dyn Draw to be legal, and who would pay for it?

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-15 (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-16 (comprehension + extension · lupin). Extend 7-15 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.

Exercise 7-17 (fingers · lupin). The plane-geometry kata: a Point, a Rect of two points (low corner in, high corner out), and an impl giving Rect three methods — contains(self, p), overlaps(self, o), area(self). Probe the edges: a point on the low edge, a point on the high edge, a rectangle that shares only a corner line. Every method here borrows. How do you know that from the signatures alone?

Exercise 7-18 (extension · lupin). One job, two ownership stories: uppercase every string in a list. Write it consuming — shouted(take xs) returns a new list and the argument is gone — and lending — shout(mut xs) rewrites in place and returns nothing. Run both. Count what each costs at the call site and in allocations, then answer: which one should a library export, and does the other need to exist at all?

Exercise 7-19 (fingers · lupin). The same plane geometry as 7-17, asked of the arms instead of the fields. Write corner(p), which names where a Point sits relative to the axes, and kind(r), which describes a Rect — both as a single match whose arms take the value apart by field name rather than reading p.x and self.lo.y through the value. Two questions when it runs: which arm of corner would become unreachable if you moved it to the top, and why does kind’s second arm need no _ beside it?

Exercise 7-20 (extension · wolf + lupin). Take 7-16’s walk one step further. Instead of printing, diff(a, b) returns the edits as a List[Edit], with struct Edit { mark: str, line: str }, and the caller prints them and counts the changed lines. Before writing it, decide whether diff lends its two lists or takes them, and say in one sentence what the lines in the result are under each choice. Run it on a three-line config file and a four-line successor of your own.

Exercise 7-21 (extension · wolf + lupin). The last three exercises share one shelf, whose documents now carry a list of their own: struct Doc { title: str, words: int, tags: List[str] } and struct Shelf { docs: List[Doc] }. First, remove(mut s, title): take every document with that title off the shelf and return how many went, or the error row {missing} when none did. Build the shelf’s new list by moving the survivors into it. How many documents does remove copy, and why is the shelf mut rather than take?

Exercise 7-22 (extension · wolf + lupin). Second, merge(mut into, take from): move every document of from onto the end of into. Why is from taken rather than mut, what may the caller do with its own binding after the call, and what would leaving take off the push inside the loop cost?

Exercise 7-23 (comprehension + spelunking · wolf + lupin). Third, longest(s): return the document with the most words, the Doc itself and not its title, from a shelf passed with no mode. Write it the obvious way, ending on s.docs[best], and build it. Read what the compiler says, then wolf --explain E1002, and pick one of the two repairs it offers: which one does a query on a shelf want? Then run the unrepaired program under lupin, and say why §7.3’s longest never met this, and why it would not have met it even returning a Doc, as long as that Doc had no tags.