Skip to content

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.

talor
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 needed

A 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.

talor
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:

    talor
    struct 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 places
  • What is read out of the place being written is passed as a copy, so the write cannot reach it. Here move_over reads k and writes h, and the call hands it h.k, a part of h itself:

    talor
    struct 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=99

    k is the array as it was at the call, because the compiler passed a copy of h.k, and the write reached the caller's h. Without the copy, k would 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, and move_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:

talor
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 line
  • edits 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, self is 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 clause

A 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:

talor
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()}`);   // 2

With 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:

talor
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 copied

The 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.

Talor v0.1.0 - Released under the MIT OR Apache-2.0 license.