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 write | The value is Copy | Any other value |
|---|---|---|
let b = a, b = a, a field in a struct literal | copies; a stays live | moves; a is empty |
f(a), with no mode | f reads it; you keep a | f reads it; you keep a |
f(mut a) | f writes it; you keep a | f writes it; you keep a |
f(take a) | moves; a is empty | moves; a is empty |
move a | moves; a is empty | moves; a is empty |
copy a | a second value | a second, independent value: deep for a struct or a List; a str shares its bytes |
return v, or a body’s last value | copies out to the caller | moves out to the caller |
(mut xs).push(a), xs[i] = a | copies into the container | copies into the container; take a moves it there |
a = … after a move | a is live again | a 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.
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:
Line 6 fills the emptied field again, and the place is live:
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:
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
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
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
= deleteor 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.titleand&mut d.subtitleto 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 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 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:
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)
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.