Chapter 8

References and Slices

So far, references have appeared in passing: push returns one, .= binds one, and a parameter of type i64& is one. This chapter looks at them properly, together with their many-element cousin, the slice. It also explains the single rule the compiler uses to make sure neither can ever dangle.

#References8.1

A reference T& is the address of a T. It is eight bytes, it can never be null, and there is no arithmetic on it and no way to cast it to anything else.

References are transparent. Wherever a reference is used as a value, it behaves exactly like the thing it refers to: you read it, do arithmetic with it, compare it, print it and pass it to functions as if it were the target. Assigning to it writes the target. There is no * and no ->.

That raises an obvious question. If r = 5 writes the target, how do you change which thing r refers to? The answer is a separate operator, .=, which works on the reference itself:

fn main() {
    var pens: i64[>..] = [10, 20, 30, 40, 50];
    var r .= pens[1];          // r refers to pens[1]
    var c = pens[1];           // c is a copy of pens[1]
    r += 1;                    // writes pens[1]
    c += 100;                  // writes the copy
    print(pens, " ", r, " ", c);
    r .= pens[4];              // r now refers to pens[4]
    r = 0;
    print(pens);
}
[10, 21, 30, 40, 50] 21 120
[10, 21, 30, 40, 0]

The dot is the visual cue: x .= e and x .== e are about the reference x, while x = e and x == e are about whatever it refers to.

fn main() {
    var pens: i64[>..] = [40, 40];
    let a .= pens[0];
    let b .= pens[1];
    let c .= pens[0];
    print(a == b, " ", a .== b, " ", a .== c);
}
true false true

#Where references come from8.1.1

You will get most of your references from push, from parameters of reference type, and from .=. The explicit way to make one is &x, which takes a reference to a variable, a field or an element:

fn main() {
    var honks = 1;
    let a = &honks;            // a reference, because of the explicit &
    let b .= honks;            // also a reference
    var r .= honks;
    let copy_of = r;           // a copy: a reference to a fixed-size value decays
    a += 1;
    b += 1;
    print(honks, " ", copy_of);
}
3 1

The line let copy_of = r; is worth a second look. When a variable's type is inferred from a reference to a fixed-size value, it gets a copy of the value, not a second reference. You have to say .= or & to get a reference. Non-fixed values never copy implicitly, as you saw in Chapter 6, so for those a plain let gives a reference.

You can only take a reference to something that has a location. &5, or & of a function's temporary result, is an error.

#Slices8.2

A slice T[:] is a view of a run of elements in some array: a reference to the first one and a count. It is the universal "here are some elements" type. Every kind of array converts to a slice of itself, without copying, wherever a slice is expected:

fn total(xs: i64[:]) -> i64 {
    var t = 0;
    for x in xs { t += x; }
    return t;
}

fn main() {
    let fixed: i64[3] = [1, 2, 3];
    var grown: i64[>..] = [10, 20, 30, 40, 50];
    print(total(fixed), " ", total(grown), " ", total([4, 5]));
}
6 150 9

You can also take a slice of part of an array with a half-open range. Either bound can be left out, and ^k means "k from the end":

fn main() {
    var pens: i64[>..] = [10, 21, 60, 40, 0];
    let mid = pens[1..4];
    print(mid, " ", pens[..2], " ", pens[^2..], " ", mid.len);
}
[21, 60, 40] [10, 21] [40, 0] 3
pens: i64[>..] len 10 21 60 40 0 0 1 2 3 4 pens[1..4] ptr 3 a slice is a pointer and a count: 16 bytes, whatever it views
A slice holds a pointer to its first element and a count. It views the elements in place; nothing is copied.

A slice is bounds-checked against its own length, and a slice of a slice can never be larger than the original. A slice taken from a grow-only array stays valid when the array grows, because nothing moves. It keeps viewing the elements it was made from, and does not include the new ones:

fn main() {
    var pens: i64[>..] = [10, 21, 60, 40, 0];
    let mid = pens[1..4];
    pens.push(60);
    print(mid, " of ", pens.len);
}
[21, 60, 40] of 6

#The one rule8.3

Here is the whole of Goose's borrow checker:

A reference or slice must not outlive the variable that owns what it points at.

That is all. There are no lifetime parameters, no rules about how many references may exist at once, and no distinction between shared and exclusive borrows. Two references to the same element are fine, and you can write through both:

fn main() {
    var nest: i64[>..] = [3];
    let a .= nest[0];
    let b .= nest[0];
    a += 1;
    b += 1;
    print(nest);
}
[5]

Aliasing like this is harmless in Goose, because the things that make it dangerous elsewhere are absent. Nothing can be freed while a reference exists, a grow-only array cannot move, and there is no memory shared between threads.

#Roots8.3.1

To enforce the rule, the compiler gives every reference and slice a root: the variable whose lifetime bounds what the reference points at. &x is rooted at x. A slice of pens is rooted at pens. A reference returned by a function is rooted at whichever arguments it might have come from.

You never write roots down. The compiler works them out, and when a function is called with references that have different roots, it type-checks the function separately for each combination. That gives the precision of Rust's lifetime parameters without any of the syntax.

Most of the time you will only notice roots when you break the rule. There are three ways to do that.

Returning a reference to a local. A function's locals die when it returns, so nothing rooted in them can be returned:

fn first_three(s: u8[:]) -> u8[:] {
    var buf: u8[>..] = [];
    buf.append(s);
    return buf[..3];
}

fn main() { print(first_three("goose")); }
error: returning a reference rooted in buf, which dies with this function (§9.2)

The fix is to return the array itself, which is built in the caller's destination, or a slice of the parameter s, which belongs to the caller.

Storing a reference somewhere that lives longer. Here view outlives the block that owns inner:

fn main() {
    var outer: u8[>..] = [];
    outer.append("outside");
    var view = outer[..];
    {
        var inner: u8[>..] = [];
        inner.append("inside");
        view = inner[..];
    }
    print(view);
}
error: storing a reference rooted at inner, which does not outlive the destination (§9.2)
main outer view { inner block } inner inner dies at the closing brace, but view lives on fine
view lives in main's scope, but inner dies at the end of the block, so view may not refer to it.

Keeping a view of a temporary. A function's result, a literal or a copy is a temporary that lasts until the end of its statement. You can pass a view of it to a function, but you cannot keep one:

fn make() -> u8[>..] { str("honk honk") }

fn main() {
    let v = make()[..4];
    print(v);
}
error: binding v to a slice rooted at a temporary, which does not outlive it (§9.2): a temporary lasts until the end of its statement, so bind it to a variable of its own first

The message says what to do: let t = make(); let v = t[..4];.

#A function that returns one of its arguments8.3.2

A function may return a reference that came from one of its parameters, because the arguments belong to the caller. When it could return any of several, the result is rooted at all of them:

fn heavier(a: i64[:], b: i64[:]) -> i64[:] {
    if total(a) >= total(b) { a } else { b }
}

fn total(xs: i64[:]) -> i64 { var t = 0; for x in xs { t += x; } t }

fn main() {
    var morning: i64[>..] = [4200, 3900];
    var evening: i64[>..] = [4600, 4100];
    let w = heavier(morning, evening);
    print(w);
}
[4600, 4100]

Here w is rooted at both morning and evening, and it must satisfy the rule for both: it may not outlive either one. In Rust you would have written fn heavier<'a>(a: &'a [i64], b: &'a [i64]) -> &'a [i64]. Goose works the same thing out from the body.

#Liveness, not scope8.4

The compiler cares about when a reference is last used, not about where its scope ends. A slice that is never used again is dead, even if its variable is still in scope, and a dead slice does not stop its array from shrinking:

fn main() {
    var line: u8[>..] = [];
    line.append("honk honk");
    let first = line[..4];
    print(first);              // first's last use
    line.clear();              // fine
    line.append("hiss");
    print(line);
}
honk
hiss

Swap the last two uses around, so that first is printed after the clear, and the compiler objects, naming both the array and the slice that is still in use:

fn main() {
    var line: u8[>..] = [];
    line.append("honk honk");
    let first = line[..4];
    line.clear();
    print(first);
}
error: cannot clear line while first is still used: it may hold a reference or slice into it (§5.1)

This is why a scratch buffer can be refilled in a loop and still hand out slices of itself on every iteration. Each iteration's slices are dead by the time the next clear comes around.

#Writability8.5

Everything is writable unless its type says otherwise. const T& is a reference through which the target cannot be written, and const T[:] is a read-only slice. Some things are read-only by nature: a string literal is static data, so it is a const u8[:].

Parameters are generic over constness. A parameter declared u8[:] accepts both a writable slice and a read-only one, and the function is checked separately for each. Writing through the parameter is fine for a writable argument, and an error, reported at the call, for a read-only one:

fn shout(s: u8[:]) {
    for &c in s { if c >= 'a' && c <= 'z' { c -= 32; } }
}

fn main() {
    var buf: u8[>..] = [];
    buf.append("honk");
    shout(buf);                 // fine: buf is writable
    shout("hiss");              // a string literal is not
}
error: cannot assign through this path (const, or a read-only instantiation, §9.5)
in fn shout(u8[:]) instantiated from main.goose:9

The second line of the message points at the call that caused the problem. Remove that call and the program runs, turning honk into HONK.

So you rarely write const on a parameter. You write it when you want to promise that a function only reads, which also lets the compiler catch an accidental write inside the function itself. The places where const is required are slots that store references: a struct field, an array element or a global that is meant to hold read-only data, such as a string literal, must say so in its type. Chapter 9 has examples.

#Rebinding8.6

A reference variable that you rebind with .= stays tied to the root it started with, or to another root at the same scope depth. In practice that means you can walk around inside a structure, or move between sibling variables, which is what rebinding is for:

fn main() {
    var a: i64[>..] = [1, 2];
    var b: i64[>..] = [3, 4];
    var cursor .= a[0];
    cursor .= a[1];
    cursor .= b[0];             // another local at the same depth: fine
    print(cursor);
}
3

Anything more adventurous, such as pointing an outer variable at something from an inner scope, is the "storing a reference" error from before.

#Exercises8.7

  1. Write a function largest(xs: i64[:]) -> i64& that returns a reference to the largest element, and use it to double the largest element of [3, 9, 4] in place.

  2. Write fn middle(s: u8[:]) -> u8[:] that returns its argument without its first and last bytes (or an empty slice if it is shorter than 2). Call it on a string literal and on a builder. Why is it fine to return a slice here, when first_three above was rejected?

  3. Change the line example above so that it prints first after the clear, but compiles. (There is more than one way.)