Borrowing
A call does not take what it is given. Every parameter is borrowed unless the signature says otherwise, and the caller writes nothing: no &, no &mut, no marker at the call site.
struct Bag { items: Array<i64> }
fn total(b: Bag): i64 { b.items.len() }
let bag = Bag { items: [1, 2] };
let n = total(bag); // reading a parameter borrows it, so `bag` is still here
println(`${n} and ${bag.items.len()}`);What a call does with the value
Three things, and the compiler reads which one out of the callee's body. The same rule holds at a pub fn across a module and inside one, and nothing about it is written at the call site.
- Read - the parameter is a borrow, and the caller keeps its value.
- Write - the argument must be a place that may be written, and the caller's value changes.
- Keep - the callee stores the value, wraps it, or captures it in a closure, so the caller's variable is consumed. A parameter the callee only reads and hands back whole is lent instead: see returned borrows.
A use of a value the callee kept is refused where it is used, and the message names the call:
error: 'bag' was given to 'consume' on line 5, which keeps it; write 'bag.clone()' at the call if both uses are neededA parameter the body writes is the caller's variable
In C, a function that should change the caller's variable takes a pointer, the caller passes &x, and the function writes through *p. Talor has the same effect with nothing written in either place: when the body writes a parameter, the compiler passes it as a pointer to the caller's variable, and after the call the caller reads the value the function wrote.
struct Counter { n: i64 }
fn bump(c: Counter) { c.n = c.n + 1; }
fn fill(xs: Array<i64>, k: i64) {
let i: i64 = 0;
while i < k { xs.push(i); i += 1; }
}
fn main(): i32 {
let c = Counter { n: 40 };
bump(c);
bump(c);
let xs: Array<i64> = [];
fill(xs, 3);
(c.n + xs.len()) as i32 // 45: c and xs are the caller's, with the writes
}The program exits with 45. bump and fill receive the address of the caller's c and xs, not a copy, and the contract the compiler derived is what talor surface records:
fn bump(c: Counter edits): ()
fn fill(xs: Array<i64> edits, k: i64): ()Three things a C pointer does not promise, and this does:
There is no null. The argument is always a variable of the caller, so there is nothing to check before writing.
Two written arguments are never the same place. A call that hands the same variable to two parameters the body writes is refused, because each write would reach what the other one is changing:
talorstruct H { items: Array<i64> } fn swap_into(a: H, b: H) { a.items = [1]; b.items = [2]; } fn main(): i32 { let h = H { items: [] }; swap_into(h, h); // refused: both are written, and they are the same place 0 }error: argument 1 is edited and argument 2 is the same place 'h', so the write reaches what the other is read out of; pass two placesWhat is read out of the place being written is passed as a copy, so the write cannot reach it. Here
move_overreadskand writesh, and the call hands ith.k, a part ofhitself:talorstruct K { items: Array<i64> } struct H { k: K, total: i64 } // Reads `k`, writes `h`: `h.k.items` is emptied and replaced. fn move_over(k: K, h: H): i64 { h.k.items = []; h.k.items.push(99); let sum: i64 = 0; for x in k.items { sum += x; } // reads the k it was handed h.total = sum; sum } fn main(): i32 { let h = H { k: K { items: [10, 20, 30] }, total: 0 }; let s = move_over(h.k, h); // h.k is read out of the place h the call writes println(`sum=${s} total=${h.total} items=${h.k.items.len()} first=${h.k.items[0]}`); 0 }sum=60 total=60 items=1 first=99kis the array as it was at the call, because the compiler passed a copy ofh.k, and the write reached the caller'sh. Without the copy,kwould point into the array the first line of the body frees. Compiled with--borrow strict, the compiler makes no copy of its own and refuses the call instead, andmove_over(h.k.clone(), h)written by hand is the same program.
The clause: edits and takes
A clause after the signature states the effect at the signature, for whoever wants it written down rather than inferred:
pub fn append(dst: Array<i64>, src: Array<i64>) edits dst { dst.push(1); }
pub fn consume(b: Bag): i64 takes b { b.items.len() }
append(xs, extra); // the same call either way
let n = consume(bag); // `bag` is gone after this lineedits x- the function mutates the caller's value, so the argument has to be a place that may be written.takes x- the function keeps the value, and using it afterwards is an error that names the call.- Each verb names every parameter it applies to,
selfis one of the names, and the clause follows the return type.
The clause is optional everywhere, pub fn included, and it never reaches the caller: the compiler computes the mode of every parameter from the body and enforces the same rules either way. A written clause is checked against the body, so it cannot drift into a comment that lies:
error: 'size_of' edits 'h', and the body never writes it; drop the clauseA clause the body would derive anyway is information rather than a mistake, and the compiler can report it - as a warning, off by default. The one place the written word is the only source of the fact is a parameter whose type mentions a type parameter: such a value travels by copy whatever the instance is, so the body decides nothing and the clause says whether the callee keeps it.
Reaching a container is mutating
h.items.push(x) writes to the caller's value exactly as h.n = x does, and the two are one rule:
struct Holder { items: Array<i64> }
fn add(h: Holder, x: i64) edits h { h.items.push(x); }
let h = Holder { items: [1] };
add(h, 2);
println(`${h.items.len()}`); // 2With the clause written or inferred, the argument must be a place that may be written. Reading a parameter is not mutating, and needs no clause.
One write that reaches what another argument reads
Two arguments of one call, where the write reaches what the other is read out of, would be a place the callee writes while it reads. The one that is only read is passed as its copy, which the compiler writes itself:
struct H { items: Array<Array<i64>> }
fn g(dst: H, src: H): i64 {
let r = src.items[0]; // read out of `src`
dst.items = []; // the write, through the parameter the clause edits
r.len()
}
g(h, h); // accepted: `src` is a copy of `h`, taken before the call
g(h, k); // accepted: the write reaches neither, and nothing is copiedThe copy costs one clone and happens only at a call the write reaches. The receiver of a method is an argument like any other, in both directions: p.show(p.items), where show writes self.items, copies the argument, and h.look(h.k), where look writes its parameter and only reads self, copies the receiver.
Three cases are refused instead, because no copy resolves them:
- two written places that overlap, arguments or an argument and the receiver, since a copy of either one would take its write away from the caller;
- a read value that cannot be cloned, one that holds a closure or a resource;
- a place a lock lends, which is lent and never copied.
What a borrow is not
- A view is read-only, and no word writes one: the compiler infers it from how a value is used. See lifetimes.
- A Copy value is passed by value, and a read aggregate is not copied at all: it travels as a pointer to the caller's value.
- A function value's type carries no clause, so a function that writes, keeps or hands back a parameter cannot be used as a value.
- A closure's parameter is never taken, whatever the body does with it.
How long a borrow lasts
Until its last use, which is what lifetimes is about.