Skip to content

Returned borrows ​

A function that hands back one of its parameters, or a part of one, returns a borrow of the caller's argument. Nothing in the signature says so, and nothing has to: the compiler reads the body, works out which arguments the result may point into, and holds the caller to that.

A parameter handed back ​

A body that only reads a parameter and hands it back lends it. The caller keeps its value, and the result is a look at it:

talor
fn id(xs: Array<i64>): Array<i64> { xs }

fn main(): i32 {
    let xs: Array<i64> = [1, 2];
    let r = id(xs);
    (r.len() + xs.len()) as i32      // 4: xs is still the caller's
}

A part of a parameter is lent the same way. first hands back the element where it lies, and nothing is copied:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let r = first(xs);
    r.len() as i32                   // 2
}

A result that may come from several arguments ​

When the body may hand back one of several parameters, the result is tied to every argument it may come from:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a = [1, 2, 3];
    let b = [4];
    let r = longer(a, b);            // a look at a or at b
    (r.len() + a.len() + b.len()) as i32
}

The caller does not know which one it got, so while r is live neither a nor b may be replaced, moved or written:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a = [1, 2, 3];
    let b = [4];
    let r = longer(a, b);
    b = [];                          // refused: r may be a look at b
    (r.len() + b.len()) as i32
}
error: 'b' is viewed on line 8 and the view is still live here, so this assignment would leave it pointing at something else; end the view first, or take a copy with '.clone()'

A borrow lasts until its last use, not until the end of the block. Once r has been read for the last time, b is free again.

The contract is derived, and it is visible ​

The signature of longer is the one it was written with. What the compiler derived from the body is recorded by talor surface, which writes the module's contract and fails a build when it changes:

fn longer(x: Array<i64>, y: Array<i64>): Array<i64> from param 0 from param 1

from param 0 from param 1 is the contract a caller is held to. A change to the body that changes it - a result that may now come from a third argument, or that stops being a borrow - changes that line, so the change shows up where the function is defined rather than at a distant call.

Arguments that end at the call are moved ​

When every argument the result may come from is a temporary, or a local that is not used again, the call moves them in. The caller owns the result, the argument that was not handed back is released at the call, and nothing is copied:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn build(n: i64): Array<i64> {
    let a: Array<i64> = [1, 2, 3, 4];
    let b: Array<i64> = [n];
    longer(a, b)                     // a and b end here: the result is build's own
}

fn main(): i32 {
    let kept = build(7);
    kept.len() as i32                // 4
}

Where a borrow would outlive its argument, the compiler copies ​

A borrow cannot outlive what it points at. When a borrowed result is kept somewhere that owns its value - a binding with its type written, an assignment, a return, a field, an element, an argument the callee keeps - the compiler copies it at that one point, and nowhere else:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a: Array<i64> = [1, 2, 3];
    let result: Array<i64> = [];
    {
        let b: Array<i64> = [4];
        result = longer(a, b);       // copied here: result outlives b
    }
    (result.len() * 10 + a.len()) as i32   // 33
}

The copy is the one a program would otherwise write by hand as .clone(), at the same place. A call whose result stays inside the region of its arguments, which is the common case, copies nothing.

The same rule covers a borrowed result stored in a container:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let ys: Array<Array<i64>> = [];
    ys.push(first(xs));              // copied: ys keeps what it is given
    (ys.len() + xs.len()) as i32
}

--borrow strict: no copy the program did not write ​

Compiled with --borrow strict, the compiler inserts no copy of its own. A borrowed result kept past its arguments is refused, and the message names the argument:

talor build --borrow strict
error: this value is borrowed from 'a', and a borrowed value cannot be kept here; write '.clone()' to keep a copy

The same flag turns off the copy the compiler makes when one argument of a call is written and another is read out of the same place, such as g(h.k, h): that call is refused, and the message says where to write .clone().

What stays a move ​

Two shapes are not lent, and the caller gives up its value:

  • A parameter the body writes and then hands back. A borrow is read-only, so a body that changes its parameter hands the changed value over:

    talor
    fn add(x: Array<i64>, v: i64): Array<i64> {
        x.push(v);
        x
    }
    
    fn main(): i32 {
        let a: Array<i64> = [1];
        let r = add(a, 2);             // a is given to add
        r.len() as i32
    }
  • A parameter handed back on one path, and a new value on another. The caller could not tell whether it owns the result, so the parameter is handed over on every path:

    talor
    fn or_new(x: Array<i64>, k: i64): Array<i64> {
        if k > 0 { x } else { [9, 9] }
    }
    
    fn main(): i32 {
        let a: Array<i64> = [1, 2, 3];
        let r = or_new(a, 1);          // a is given to or_new
        r.len() as i32
    }

Where an owned value and a borrowed one meet ​

An if or a match whose branches disagree - one yields a borrowed call result, another a value of its own - joins into one value the program owns, so the compiler copies the borrowed side at the join:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs = [[1, 2], [3]];
    let k = 1;
    let r = if k > 0 { first(xs) } else { [7] };   // the borrowed side is copied here
    xs[0].push(9);                                 // so xs may change while r is live
    println(`${r.len()} ${xs[0].len()}`);
    0
}

Under --borrow strict the join is refused instead, and .clone() on the borrowed branch is the copy written by hand:

error: this value crosses the join and is kept there: it is borrowed from 'xs', and '.clone()' is what keeps a copy

The guarantees ​

  • A returned borrow never outlives what it points at: either the arguments outlive it, or the compiler copies it at the point where it would not, or, under --borrow strict, the program is refused there.
  • While a result is live, every argument it may come from is protected: none of them can be replaced, moved or written.
  • A borrow costs what a pointer costs. A copy happens only where a program keeps a borrowed result past its arguments, and it is the copy the program would have had to write.

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