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:
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:
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:
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:
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 1from 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:
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:
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:
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 stricterror: this value is borrowed from 'a', and a borrowed value cannot be kept here; write '.clone()' to keep a copyThe 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:
talorfn 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:
talorfn 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:
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 copyThe 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.