Skip to content

Lifetimes ​

A borrow is a look at a value that lives somewhere else. Talor writes neither the region nor a name for it: there is no 'a, no &, and nothing at a call site. The compiler infers how long a borrow lives from the control flow, and the borrow ends at its last use rather than at the end of the scope that holds it.

A language that makes lifetimes part of the type calls leaving them out elision: the annotation is omitted where a fixed rule over the signature determines it. Talor has only the elided form - nothing is written in any position - and the rule is not over the signature: the origin of a returned view is read from the body, so two functions with the same signature can hand back views of different places.

talor
fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let r = xs[0];              // `r` reads the element where it lies
    println(`${r.len()}`);
    xs = [];                    // accepted: `r` was used on the line above
    println(`${xs.len()}`);
    0
}

Views ​

A view is a shared, read-only look at a place. It is one machine word, it is Copy, it owns nothing and it releases nothing.

No word writes one. A place handed to a parameter the callee only reads is already a borrow of that place, a read of a place this function does not own keeps the borrow, and a value a body returns from a read derives its origin from the body. The compiler makes a view wherever a read can stay where it lies, and the program says nothing for it. view is the name of the concept, not a word of the language: a program may use it as a name.

talor
struct Point { x: i64, y: i64 }

fn norm2(p: Point): i64 { p.x * p.x + p.y * p.y }

let p = Point { x: 3, y: 4 };
println(`${norm2(p)}`);     // the parameter is only read: nothing is written at the call site

A view of a T answers what a place of type T answers: a field reads through it (r.x), a method call takes it as its receiver (r.len()), and an index reads through it (r[i]).

A Copy scalar read is a copy, because a copy of a scalar is cheaper than a pointer to it. For a value that owns anything, read a field or call a method through the view: *r on a view of an aggregate is refused, because it would take the whole value out of a place the function does not own.

The rules ​

  1. A view may not outlive the place it points at.
  2. A view may not live in a field.
  3. A view may not live in a container or a tuple.
  4. While a view of x is live, x may not be assigned, moved, or passed where it would be mutated. Live ends at the last use of the view.
  5. A view needs a place: a call or a literal has nowhere to live.
  6. A view is read-only; mutation goes through the owner.
  7. A function returns a view when every returned view comes from its parameters, and the call site ties the result to every argument it may come from.

Each rule has one message, and the message says what to write instead:

error: 'xs' is viewed on line 3 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()'
error: a view is read-only: write through 'x', which owns the place, or take an owned copy with '.clone()'

The one writable form of the same idea is Locked<T>, which only a lock makes and only for the length of one call: a view with the write permitted, and a region rule stricter than these seven.

A read of a place this function does not own is a look ​

An element of an array, a field of a borrowed parameter, a loop variable and a pattern binding on a borrowed value are places nothing may move out of, so reading one keeps the borrow rather than copying it:

talor
struct Row { cells: Array<i64> }

fn main(): i32 {
    let xs: Array<Row> = [Row { cells: [1, 2] }, Row { cells: [3] }];
    let r = xs[0];              // `r` reads the element
    let n = r.cells[1];         // and a view is indexed like the place it views
    println(`${n}`);
    for x in xs { println(`${x.cells.len()}`); }   // the loop variable is a borrow too
    0
}

Everything else is unchanged: a Copy value is copied, because a copy of a scalar is cheaper than a pointer to it; a place this function owns is read where it lies too, and moved when the new name is handed over or written through; a temporary has nowhere to be read from. Storing one of these reads, or passing it to a parameter the callee keeps, is a move out of storage the function does not own, and is refused. Returning one makes the function return a borrow, below.

A returned value borrows when its body reads in place ​

A return type is written as the type it is - Row - and the body decides whether what comes back is a look or a value:

talor
struct Row { cells: Array<i64> }
struct Table { rows: Array<Row> }

fn row(t: Table, i: i64): Row { t.rows[i] }   // a look at `t`, not a copy

let t = Table { rows: [Row { cells: [1, 2] }] };
let r = row(t, 0);
println(`${r.cells.len()}`);

A returned look may come from more than one parameter, and then the result is tied to every argument it may come from: none of them can be replaced while the result is live. Returned borrows shows each case.

The place a look is taken from has to outlive the statement that uses it, so a call whose origin is a temporary is refused:

error: a view returned by this call points into a temporary, and a temporary dies at the end of the statement; bind it to a name first

A look kept where its value is owned - pushed into a container, or assigned to a binding that outlives the argument - would outlive what it points at, so the compiler copies it at that point. Compiled with --borrow strict, it is refused instead, and .clone() is how a program asks for a value of its own.

A body that builds a value returns an owned one, and so does a body that writes a parameter and then hands it back. A body that returns a look on one path and a value it built on another is refused, because the caller could not tell whether the result is its to release, and so is a look wrapped in a value it builds, such as Some(t.rows[i]). Writing .clone() on the part makes it a value of the function's own:

error: 'pick' reads a place it does not own on line 4 and returns a value of its own on line 4, so the caller cannot tell whether the result is its to release; return a value of its own on both, with '.clone()' on line 4

A borrow in a closure ​

A closure may capture a view, because a view is Copy and read-only: the place it points at stays borrowed for as long as the closure is live, so that place cannot be moved or mutated while the closure exists.

talor
struct Row { cells: Array<i64> }
struct Table { rows: Array<Row> }

fn row(t: Table, i: i64): Row { t.rows[i] }

let t = Table { rows: [Row { cells: [1, 2] }] };
let r = row(t, 0);
let f = () => r.cells.len();
println(`${f()}`);

Such a closure cannot leave the function it was made in: a value that carries a view is not returned and not passed where the callee keeps it, since either would outlive the region the view was taken from, and fn(A): R has nowhere to say which region that is.

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