Goose

The Goose Language Reference

This is the reference for the Goose language: every feature, topic by topic, with examples. It sits between the two other descriptions of the language. The tutorial teaches by example and spends its time on what is different about Goose; the specification states the exact rules in the language an implementor needs. This document is for the programmer who has read the tutorial and wants the whole language laid out: exhaustive like the specification, but written to be read, and with a program for every rule.

Each section ends in, or carries, a pointer of the form "spec §3.3" to the specification section that is the final word on it. Features that exist nowhere else get the most room. Features that work as they do in C or Rust, if and while among them, get a paragraph.

Every example in this document compiles with the current compiler, and scripts/check_doc_examples.py checks that they still do. A complete program is shown with its output after it; a shorter example is the body of a main, or a set of declarations, and is written so that it can be dropped into one. Run any of them with

goose example.goose

which compiles the program into the compiler's own process and runs it there, with no C compiler and no build step. goose check typechecks without running, and goose build makes an executable (packages.md has the rest of the command line).


1. Lexical structure

A Goose source file is UTF-8 text. A byte-order mark at its start is ignored, and line endings are read as \n whichever way the file writes them.

Comments are // to the end of the line and /* … */, which nests: /* a /* b */ c */ is one comment.

Identifiers are [A-Za-z_][A-Za-z0-9_]*. The keywords are listed in Appendix A. A few words are keywords only in one position and names everywhere else: from after a return value, upto after an unsigned type in a type, inline and checkfree before fn, and fields, variants and types in a const for header. in is a keyword throughout.

Integer literals are decimal (42) or hexadecimal (0xFF). The whole unsigned 64-bit range is accepted: a value above i64.max is a u64 constant. A character literal 'a' is an integer constant, the byte's value; it takes the escapes a string does.

Float literals have a ., an exponent, or both: 1.5, 2.5e-3, 1e9. A . begins a fraction only when a digit follows it, so 1..2 is a range and 1. is not a number. C99 hexadecimal floats with a binary exponent are accepted too: 0x1.8p3 is 12.0.

String literals are "…" with the escapes \n \t \r \0 \\ \" \' and \xNN (two hexadecimal digits). A literal is static read-only data, of type const u8[:] (section 6).

Raw strings are """…""": no escapes, and the next """ ends the text. On one line the text is whatever lies between the delimiters, backslashes and quotes included. A multi-line raw string starts with its opening """ at the end of a line and ends with the closing """ on a line of its own; the closing delimiter's indentation is removed from every line, so the text can sit at the code's own indentation. The line breaks just after the opening and just before the closing delimiter are not part of the text; an empty line before the closing delimiter adds a final newline.

fn main() {
    let usage = """
        usage: wc [-l] file
          -l   count lines only
        """;
    print(usage);
    print("""C:\temp\new "quoted" \n""");
}
usage: wc [-l] file
  -l   count lines only
C:\temp\new "quoted" \n

Other literals: true and false; null, the empty value of any optional type (section 10); and self, which is legal only inside a struct or variant literal and denotes the value that literal is constructing (section 11).

Statements and semicolons

An expression statement ends in ;. A statement that is a block-ended construct, meaning if, match, block, a loop, guard … else { }, a nested fn, or a call with a trailing function block, needs none: it ends at its closing }. An operator never continues such a statement, so to use one of them as an operand it has to be parenthesized:

let a = 3;
let v = (if a > 2 { 1 } else { 2 }) + 3;
if a > 2 { print("big"); }                 // no `;` needed
let w = if a > 2 { 10 } else { 20 };       // as an initializer: fine as it is
print(v, " ", w);

A stray ; after a block is harmless. Every bracketed, comma-separated list accepts a trailing comma: struct and array literals, call arguments, parameter and generic lists, field lists and match arms.

Evaluation order

Everything evaluates left to right: operands, arguments, and the fields of a literal. A by-value operand is read before the operands after it run; a reference or slice operand keeps its address or view, not a snapshot of the contents. The destination of an assignment is resolved first, and a compound assignment reads its old value before its right-hand side runs. Spec §2.


2. Declarations, variables and scope

let, var and const

let answer = 42;              // a binding that cannot be reassigned
var count: i32 = 0;           // a variable: reassignable
const limits = [10, 20, 30];  // neither reassigned nor written into
count += 1;
count++;                      // a statement, not an expression

The type follows the name and can usually be left out: an un-annotated declaration takes the type of its initializer. let forbids assigning the name as a whole and nothing else: the contents of a let array or struct are as writable as their type says, so let xs = [1, 2, 3]; xs[0] = 9; is fine. Read-only contents come from the type, const T (section 12), and const x = e; declares a let of type const T. A var of a const type is the converse: it can be reassigned but never written into.

let xs = [1, 2, 3];
xs[0] = 9;                    // fine: let forbids `xs = …`, not element writes
const ys = [1, 2, 3];
// ys[0] = 9;                 // error: ys is const
var s: const u8[:] = "text";  // a read-only view that can be re-pointed
s = s[1..];
print(xs, " ", ys, " ", s);

A let without an initializer gets its value exactly once, and the compiler checks that it does: no path may read it before it is assigned, and no path may assign it twice, which includes assigning it in a loop it is declared outside of, or in a function called more than once.

fn main() {
    let n: i64;
    let ready = true;
    if ready { n = 1; } else { n = 2; }
    print(n);
}
1

Every local must be initialized before it is read, on every path. Fixed arrays must be fully initialized; the fill literal [v; n] makes a large one cheap. An inner scope may redeclare a name, which shadows the outer one. Spec §4.4.

Globals

Globals are declared like locals, at the top level of a file, with any type: scalars, structs, and growable arrays too. They initialize before main runs, in declaration order within a file and imported files first, and their initializers may call functions. A var global given only a type starts out as that type's default value: var log: u8[>..]; is an empty array. A let or const global must be given its value. Section 21 has the rest.

Named constants

A let or const global with no written type and a constant initializer is a named constant. An integer one is used exactly as the literal would be: it adapts to any integer type its value fits, serves as an array size or a fill count, and names a value in a match arm.

let SIZE = 16;
let HALF = SIZE / 2;
var grid: u8[SIZE] = [0; SIZE];
fn main() {
    let b: u8 = SIZE;            // adapts, as the literal 16 would
    let wide = SIZE * 1000000000000;
    grid[HALF] = 1;
    print(b, " ", wide, " ", grid.len, " ", match 8 { HALF => "half", _ => "other" });
}
16 16000000000000 16 half

A global with a written type (let SIZE: i64 = 16;), a local let, and any var are values of their type and adapt to nothing. Spec §3.1, §11.1.

Type aliases

type Name = T; names a type. An alias is the same type, not a new one:

type Id = u32;
type Names = u8[..16][>..];
fn main() {
    let a: Id = 7;
    let b: u32 = a;
    var ns: Names = [];
    ns.push("goose");
    print(a + b, " ", ns);
}
14 ["goose"]

A nominal type comes from a struct or enum declaration (section 8, section 9).

Type syntax at a glance

Type syntax is postfix. Every form below is described in its own section; this table is the map.

Written Meaning Section
i8 … i64, u8 … u64, f32, f64, bool, varint the scalars 3
T[4], T[], T[varint], T[..8], T[..], T[>..], T[>..<] the array family 5
T[:] a slice: a view of a run of Ts 7
Point, Pair<i64> a struct, a generic struct instance 8
Shape, Shape.., Shape.Circle an enum in fixed mode, in variable mode, one variant 9
T&, T? a reference, an optional (nullable) reference 10
T&<u32>, T&<u32 in pool>, T&<u32>? relative references 11
const T, const T&, const T[:] read-only contents, a read-only reference or view 12
u32 in a, u32 upto a an index of the global array a, a bound of it 15
fn(i64) -> bool a function value's shape, as documentation 18

const qualifies the first reference or slice built on the base type: const u8[:] is a read-only view of bytes. To reach an outer one, parenthesize: const (u8[:])& is a read-only reference to a slice variable. An index type takes no postfix of its own, so an array of them is written (u32 in a)[>..].


3. Scalars and arithmetic

Integers

The integer types are i8 i16 i32 i64 and u8 u16 u32 u64, two's complement, usable everywhere: fields, elements, locals, parameters, results. i64 is the conventional type for counts, sizes and indices; it is what .len, integer literals and the builtins produce. The narrower types are for data that should be compact and for kernels that want a particular width.

Arithmetic happens at the operands' own width. u8 + u8 is an 8-bit add, i32 * i32 a 32-bit multiply. There is no promotion to int: the declared types decide the operation.

A literal has no type of its own. It adapts to any integer type whose range holds it, and is an i64 where nothing constrains it. let x: u8 = 255; is fine and let x: u8 = 256; is a compile error.

let byte: u8 = 255;
let answer = 42;              // an i64
let wrapped: u8 = byte + 1;   // an 8-bit add: 0, since unsigned math wraps
let widened = byte + answer;  // the u8 widens into the i64: 297
print(wrapped, " ", widened);

Mixing widths. A binary operator's operands must reach one common type, which is also the result's type. The rules, in order:

  1. Equal types stand.
  2. A literal adapts to the other operand's type (an error if it does not fit).
  3. Otherwise, if exactly one operand widens implicitly into the other's type, the wider type wins: u8 + i64 is an i64 add.
  4. An integer meeting a float converts to the float's type: i32 * f32 is an f32 multiply.
  5. Anything else is a compile error asking for a cast: same-width signed and unsigned (u32 + i32), or u64 with anything signed. Nothing is silently promoted to 64 bits.
let a: u32 = 7;
let b: i32 = 3;
// let c = a + b;             // error: u32 and i32 do not unify; cast one
let c = a as i64 + b;         // an i64 add
let d: u8 = 200;
let e = d + 100;              // 100 adapts to u8: an 8-bit add, 44
print(c, " ", e);

Shifts are the exception: x << k has x's type whatever the count's type, and the count is masked to the width. Comparisons unify the same way but produce bool.

% is Euclidean. The result lies in [0, |b|) and is never negative, at every integer type, whatever the dividend's sign. So x % n is always a valid index into an array of length n, and the compiler drops the bounds check that a C-style remainder would need a guard for.

print(-7 % 3, " ", 7 % -3, " ", -7 / 3);
2 1 -2

Integer / truncates toward zero, so / and % do not form a matching quotient/remainder pair for negative dividends. std supplies both missing partners, at every integer width: rem(a, b) pairs with /, and div_euclid(a, b) pairs with %.

import std;
fn main() {
    print(-7 / 3, " ", rem(-7, 3));
    print(div_euclid(-7, 3), " ", -7 % 3);
    print(div_euclid(-1, 10), " ", -1 % 10);  // tile and offset
}
-2 -1
-3 2
-1 9

div_euclid rounds down for a positive divisor and up for a negative one; its quotient overflows exactly when / does. rem is zero or has the dividend's sign, and rem(MIN, -1) is zero at every signed width. Both functions abort on a zero divisor. Float % uses C fmod, so -7.0 % 3.0 is -1.0; these std functions take integers only.

Overflow. Unsigned arithmetic wraps modulo 2^width in every build, by definition: that is what hashes, random number generators and bit tricks mean by unsigned math. Signed overflow aborts in a debug build (--debug) and wraps in a release build. Division or remainder by zero aborts always, and so does i64.min / -1. A debug build checks each operation as it executes after optimization, so it is a bug-finding tool, not a promise about every intermediate written in the source.

var h: u64 = 14695981039346656037;
for c in "goose" { h = (h ^ c) * 1099511628211; }   // FNV-1a, wrapping by design
print(h);

A u64 literal above i64.max, as above, is accepted; negating one is an error except for -9223372036854775808, which is exactly i64.min. Spec §6.2.

Floats

f32 and f64 are IEEE floats. f32 arithmetic stays 32-bit; an f32 widens to f64 implicitly, never the reverse. Every integer converts to either float type implicitly, so 2 works wherever 2.0 would.

A float literal adapts to whichever float type its destination or other operand has, and is an f64 where nothing gives it a type. So does an expression whose float-ness comes only from literals and integers: n * 0.5 is computed in 32 bits when it is passed to an f32 parameter and is an f64 in let h = n * 0.5;. A let with no written type whose initializer is such an expression adapts the same way at each use, local or global, which is how PI in the standard library serves both widths.

let PI = 3.141592653589793;
fn area32(r: f32) -> f32 { PI * r * r }     // PI adapts to f32 here
fn main() {
    let n = 3;
    let h = n * 0.5;                        // an f64
    let r: f32 = 2;                         // an integer becomes a float implicitly
    print(h, " ", area32(r), " ", PI);
}
1.5 12.566371 3.141592653589793

A float prints in the shortest form that reads back as the same value of its own type, with .0 after a whole number: 1.0, 0.1, -0.0; inf, -inf and nan. On floats % is C's fmod. Spec §3.1, §6.3.

bool

bool holds true or false in one byte. Comparisons produce it; if, while and guard require it: there is no integer-to-bool coercion, so if n { … } with an integer n is an error (if n != 0). && and || short-circuit.

varint

varint is a variable-length integer storage type for fields and array elements, one to ten bytes long depending on the value. Reading one gives an i64; it can only be written when its containing value is constructed. Section 8 covers it with the other field types.

Conversions

One principle: within a kind, a conversion the compiler can prove value-preserving is implicit; anything that could lose a bit or flip a sign takes a cast. Between the kinds, an integer becomes a float implicitly and a float becomes an integer only through a cast.

Conversion How
to a wider integer of the same signedness (i8 → i32, u16 → u64) implicit
unsigned to a strictly wider signed type (u8 → i16, u32 → i64) implicit
f32 → f64 implicit
any integer, u64 included, to f32 or f64 implicit, rounding to the nearest float
a literal or named constant into any type its value fits implicit
any integer but u64 into a varint store implicit
narrowing, same-width signedness change, signed to unsigned, u64 to signed, f64 → f32, float to integer x as T or x as! T

x as T converts between any two numeric types. It is range-checked in a debug build, which aborts if the value changes, and truncates, wraps or rounds toward zero silently in a release build. x as! T never checks: it is for when losing bits is the intent, in a debug build too. Conversions to a float are never checked, in either form. Float to integer is defined exactly: truncate toward zero, then wrap into the target's width; NaN gives 0.

let big = 300;
let n: u8 = big as! u8;       // 44, by design
let f = 2.75;
let i = f as i64;             // 2
let back = i as f64 / 4;      // 0.5
let neg = -1;
let u = neg as! u32;          // 4294967295
print(n, " ", i, " ", back, " ", u);

A cast that changes nothing is a warning, so the compiler points out the ones the rules above make unnecessary: b as i64 of a u8 stored in an i64 says redundant `as i64`: a u8 converts to i64 implicitly here. A cast that decides something stays silent: a type nothing else gives (let k = 5 as u32;), an operation's width ((a as i64) * b with i32 operands), or a float's rounding.

Comparing u64 with signed values. u64 is the one unsigned type with no signed type to widen into. A comparison between a u64 and a signed operand is allowed when the signed operand is syntactically known to be non-negative: a non-negative literal, a .len or .cap, or a let bound to one of those (and not also bound to a writable reference). The comparison is then a single unsigned one. Any other case needs a cast.

let h: u64 = 18446744073709551615;
var xs: i64[>..] = [1, 2, 3];
let n = xs.len;
print(h > 5, " ", h > xs.len, " ", h > n);   // all allowed: 5, .len and a let of it

Spec §6.1, §6.3.

Arithmetic on structs and fixed arrays

The arithmetic operators apply memberwise to two values of the same struct or fixed-array type whose scalar leaves are all integers or all floats, and so does unary - where no leaf is unsigned. * and / also take one scalar operand, in either order, when every leaf has exactly the same type; the scalar converts to that type. This is how the library's vector types work, with no operator overloading in the language.

struct V3 { x: f32, y: f32, z: f32 }
fn main() {
    let a = V3 { 1, 2, 3 };
    let b = V3 { 0.5, 0.5, 0.5 };
    var c = a + b;
    c *= 2;
    print(c, " ", -a, " ", a / 2.0, " ", 2.0 / a);
    let sum = [1, 2, 3] + [10, 20, 30];
    print(sum);
}
V3 { 3.0, 5.0, 7.0 } V3 { -1.0, -2.0, -3.0 } V3 { 0.5, 1.0, 1.5 } V3 { 2.0, 1.0, 0.6666667 }
[11, 22, 33]

+, - and % need two matching aggregates. Both operands are evaluated once, before any member is written. Spec §6.1.

Bitwise operators

~ & | ^ << >> work on integers and keep the operand's type. >> shifts in zeroes on unsigned types and copies the sign bit on signed ones. The bitwise operators bind tighter than comparisons, so x & mask != 0 is (x & mask) != 0, and shifts bind tighter than &. ~c of a non-negative constant adapts like a constant, so let a: u8 = ~4; is 251 and flags & ~F_ROAD clears a bit of a u8. A shift of a constant by a variable count keeps the constant's freedom: 1 << k takes the type its destination has.

var flags: u8 = 0;
let F_ROAD: u8 = 4;
flags |= F_ROAD;
flags |= 1 << 1;              // the constant 2 adapts to u8
flags &= ~F_ROAD;
let k = 40;
let m: u64 = 1 << k;          // a u64 shift, since m is one
print(flags, " ", m, " ", flags & 2 != 0);

There are no binary literals; write 0x hexadecimal or a shift. The complete precedence table is in Appendix A.


4. Where values live

This section is short, but everything after it rests on it.

Size classes

Every type is in one of three size classes:

A compound type's class is the largest of its parts' classes. A type is flat when it holds no reference, slice or relative reference at any depth; only flat values may cross between threads (section 22). Spec §1.1.

Data stacks

A Goose program has the native call stack, some static data (string literals and compile-time constants), and N data stacks, where N is a number the compiler computes for the program. A data stack is a large reservation of address space with a bump pointer. That is the whole memory system: no malloc, no free list, no garbage collector, no reference counts, no destructors.

Fixed-size values live on the native stack or in registers. Every variable or resizable value lives on a data stack, owned by the variable that declares it (a local, a global, a by-value parameter, or a field or element of one of those), and the rules that make this work are:

  1. Values are pushed in lifetime order and freed all at once when the owning scope ends. The free is one store: the stack's top goes back to where it was.
  2. At most one resizable value is live per stack, and it is the topmost thing on that stack for its whole life. So growth is a pointer bump with no capacity check, and nothing ever moves.

Three growable arrays alive at once get three stacks; a stack is reused the moment a scope frees it. The compiler proves all of this statically, and there is no syntax for a stack anywhere in the language. Spec §1.2, §1.3, §10.3.

fn main() {
    for round in 3 {
        var scratch: u8[>..] = [];                   // a fresh buffer, each round
        for i in 100000 { scratch.push((i % 256) as! u8); }
        print("round ", round, ": ", scratch.len, " bytes");
    }                                                // freed here: one store
}
round 0: 100000 bytes
round 1: 100000 bytes
round 2: 100000 bytes

Nothing moves

Because a growable array's elements never move, a reference into it stays valid however much is pushed afterwards:

struct Item { id: i32, weight: f32 }
fn main() {
    var items: Item[>..] = [];
    let first .= items.push(Item { 1, 0.5 });         // a reference to element 0
    for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
    first.weight = 99.5;                              // still valid
    print(items.len, " items, first is ", items[0]);
}
1000000 items, first is Item { 1, 99.5 }

A std::vector or a Vec reallocates and cannot promise this. It is why Goose code holds typed references where other languages hold integer indices into an arena.

Copies are visible

Fixed-size values copy silently on assignment, as in C, whatever their size. Non-fixed values are never copied implicitly: that copy is O(size), and the source should show it. A variable, field or element of variable or resizable class reaching a destination binds by reference when the destination is a reference, a slice, an untyped parameter, an un-annotated let/var, or a for binding, and is an error at a destination that wants a value of its own. A value-typed destination takes a fresh value (a literal, a call's result) or an explicit copy(x).

fn main() {
    var a: i64[>..] = [1, 2, 3];
    var b = copy(a);          // an explicit, visible O(n) copy
    b.push(4);
    let c = a;                // no copy: c names the same array as a
    c.push(5);
    var p = Item { 1, 0.5 };
    var q = p;                // fixed-size: a silent copy
    q.id = 2;
    print(a, " ", b, " ", p.id, " ", q.id);
}
struct Item { id: i32, weight: f32 }
[1, 2, 3, 5] [1, 2, 3, 4] 1 2

The same rule governs calls: f(xs) hands a growable array to a slice parameter, to a T[>..]& parameter and to an untyped parameter alike, by reference. Spec §4.1.

Built in place

A constructed value that is not fixed-size is built directly in its final home, through any depth of calls and through the branch an if or match takes: a function returning a growable array by value writes its elements straight into the caller's variable, or into the element being pushed, or into the field of a record being built inside another array. There is never a temporary that is then copied into place. So returning a built-up value costs what filling an out-parameter would, and words.push(str("word", i)) formats the text straight into the new element. Spec §4.3, §7.3.


5. The array family

There is no single array type. There is a family whose members differ in one respect only: what happens to the size. In all of them the representation is [metadata][elements…], inline and packed, never a pointer to a separate block.

Type Class What it is Stored metadata
T[k] fixed k elements, k a compile-time constant none
T[] variable sized at construction, frozen after a u32 length
T[u8], T[u16], T[u32], T[u64] variable the same, with a length of that width a length of that type
T[varint] variable the same, with a variable-length length 1 byte for short arrays
T[..k] fixed capacity k inline; grows and shrinks within it a length, in the smallest type holding k
T[..] variable capacity chosen at construction; grows and shrinks within it a u32 capacity and length
T[>..] resizable grow-only: the workhorse a length
T[>..<] resizable grow-shrink: stacks, queues, heaps a length
fn main() {
    var fixed: i32[4] = [1, 2, 3, 4];
    let zeros: f64[8] = [0.0; 8];           // the fill form
    let frozen: i64[] = [7, 8, 9];          // sized once, never changes
    let packed: u8[varint] = "hi";          // a 1-byte length before the bytes
    var small: u8[..8] = [];                // capacity 8, inline wherever it sits
    small.push('h'); small.push('i');
    var sized: i64[..] = [..16];            // capacity 16 chosen now, length 0
    sized.push(1);
    var grow: i64[>..] = [];                // the default growable array
    for i in 5 { grow.push(i * i); }
    var stack: i64[>..<] = [];              // the one that pops
    stack.push(1); stack.push(2); stack.push(3);
    let top = stack.pop();
    print(fixed, " ", zeros.len, " ", frozen, " ", packed, " ", small, " ",
          small.len, "/", small.cap, " ", sized.cap, " ", grow, " ", stack, " ", top);
}
[1, 2, 3, 4] 8 [7, 8, 9] hi hi 2/8 16 [0, 1, 4, 9, 16] [1, 2] 3

Which one to use

Only T[>..] and T[>..<] are resizable, and only those are subject to the one-per-stack rule. Everything else can go anywhere a value of its class can.

Compile-time sizes

The k of T[k] and T[..k], and the count of a fill literal [v; n], is a constant expression: literals, named constants, arithmetic on them, and the .len of a global whose length its declaration fixes (const names = ["a", "bc"]; var counts: i64[names.len];). A type parameter is a type, never a size: fn f<N>(a: i64[N]) is an error, since there are no size parameters. A local that hides a global's name is not a constant either. Spec §3.3.

Element restrictions

fn main() {
    var words: u8[][>..] = [];
    for i in 3 { words.push(str("w", i)); }
    for w in words { print(w); }         // fine
    // print(words[0]);                  // error: arrays of variable-size elements
                                         //        cannot be indexed, only iterated
}
w0
w1
w2

A stored length must hold the actual count: T[u8] holds at most 255 elements, and a construction that would exceed its length type's range is rejected when it is known statically, or aborts when discovered at run time. Spec §3.3.

Placement: what may contain what

No type contains itself by value, directly or through other types. It may refer to itself through a reference or a slice, which are fixed-size whatever they point at: enum List { Nil, Cons { v: i64, next: List? } } is fine. Spec §3.4.

Array literals

fn take(c: u8[4]) -> i64 { c[3] }
fn main() {
    var out = [];                    // a T[>..]; T is decided by the first push
    out.push(2.5);                   // an f64[>..]
    let tbl: i64[4] = [7; 4];
    let names: u8[..8][3] = ["a", "bb", "ccc"];
    print(out, " ", tbl, " ", names, " ", take([0, 0, 0, 7]), " ", [1, 2, 3].len);
}
[2.5] [7, 7, 7, 7] ["a", "bb", "ccc"] 7 3

A struct literal and a variant literal are described in section 8 and section 9. Spec §4.2.

Members

These are the built-in operations. Per UFCS (section 17) a.push(v) is push(a, v), so the standard library's functions on arrays read the same way.

Member On Meaning
.len -> i64 every array kind and slices the element count
.cap -> i64 T[..k], T[..] the capacity
.push(v) -> T& limited, grow-only, grow-shrink one element, constructed in place; returns a reference to it
.append(src) limited, grow-only, grow-shrink the elements of an array, slice or array literal, copied in
.pop() -> T limited, grow-shrink; grow-only where a shrink is allowed removes and returns the last element
.pop_n(n) the same drops the last n elements
.resize(n, v), .resize(n) the same grows with copies of v, or shrinks
.clear() the same empties the array
.index_of(r) -> i64 arrays of fixed-size elements the index of the element r refers to

Growth always supplies element values: there is no operation that exposes an uninitialized slot. Exceeding a limited array's capacity aborts; so does shrinking below empty. index_of needs a reference the compiler knows points into that very array (section 12). The reusable pool operations are in section 14. Spec §3.3, §12.

fn main() {
    var xs: i64[>..<] = [];
    xs.push(1);
    xs.append([2, 3, 4]);
    let last .= xs.push(5);
    print(xs.index_of(last), " ", xs.pop(), " ", xs);
    xs.resize(6, 0);
    xs.pop_n(2);
    print(xs, " ", xs.len);
    xs.clear();
    print(xs.len);
}
4 5 [1, 2, 3, 4]
[1, 2, 3, 4] 4
0

Equality

== and != compare arrays structurally: length first, then elements. Two operands of different array kinds compare too, as slices, whenever their slices would have one type: a u8[>..] against a string literal, a u8[..32] field against a u8[:]. Spec §4.5.


6. Strings and text

There is no string type. A string is any array-family type with u8 elements, and which one you pick says what you want:

No encoding is enforced; UTF-8 is a library convention, and the library's each_utf8 and push_utf8 understand it.

fn main() {
    var s: u8[>..] = [];
    s.append("hello");
    s.push(',');
    format(s, " world! ", 6 * 7);            // format appends the text of anything
    let t: u8[] = "fixed";                   // a copy of the literal, 5 bytes and a length
    let v: const u8[:] = s[..5];             // a view of s's first five bytes
    print(s, " [", s.len, " bytes] ", t, " ", v);
}
hello, world! 42 [16 bytes] fixed hello

A string literal is stored only in a slot declared const, since it is read-only; copying it into any u8 array (let t: u8[] = "fixed";) gives a writable value of its own. u8[..16] and the other limited arrays are builders too, up to their capacity: format(p.name, "linus").

Rendering text

Three builtins turn values into text, and each takes any number of arguments of any type, inserting nothing between them:

Every scalar has a text form: integers in decimal, floats in the shortest form that reads back to the same value, true and false, and a u8 array as its bytes. Every other value renders structurally: an array as [1, 2, 3], a struct or variant as its positional literal with the type name (Point { 1, 2 }, Circle { 1.5 }), a reference as what it points at, and a null optional as null. A character literal is an integer, so str('x') is "120"; a single byte is appended with push. A u8 array nested inside an aggregate is quoted and escaped.

A format overload fn format(out: u8[>..]&, v: T), taking T by value or by reference, renders a T through itself wherever one occurs in an argument of any of the three builtins, so a type chooses its text once. It is looked up in the namespace T is declared in, then globally: the type decides, not the code that prints.

struct Person { name: u8[..16], age: i32 }
fn format(out: u8[>..]&, p: Person) { format(out, p.name, " (", p.age, ")"); }

fn main() {
    var people: Person[>..] = [];
    people.push(Person { name: "ada", age: 36 });
    people.push(Person { name: "grace", age: 45 });
    print(people, " ", str(people[0]).len);
    print(['a', 'b'], " ", "text"[0], " ", str('x'));
}
[ada (36), grace (45)] 8
[97, 98] 116 120

Spec §3.7. The standard library's std module adds the usual operations, all over u8[:]: find, trim, split, words, join, parse_int, parse_flt, format_int, compare, to_lower, and more (stdlib.md). split returns an array of slices into its input: a few bytes per part, with no text copied.

fn main() {
    let line = "  key = some value  ";
    let body = trim(line);
    let eq = find(body, "=");
    print("'", trim(body[..eq]), "' -> '", trim(body[eq + 1..]), "'");
    let parts = split("alice,30,paris", ',');
    print(parts.len, " parts: ", parts, " ", words("  go   north "));
}
'key' -> 'some value'
3 parts: ["alice", "30", "paris"] ["go", "north"]

7. Slices

T[:] is a reference plus an element count: a view of a contiguous run of Ts inside some array. It is the universal "process a range" parameter type, since every array kind becomes one without copying its elements.

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

fn main() {
    let fixed: i64[4] = [1, 2, 3, 4];
    var grow: i64[>..] = [10, 20, 30, 40, 50];
    let view: const i64[:] = fixed;       // fixed itself, not a copy
    print(total(fixed), " ", total(grow), " ", total(grow[1..4]), " ",
          total(grow[^2..]), " ", view[0], " ", grow[..2].len);
    let s = grow[1..3];
    grow.push(60);                        // s still views elements 1 and 2
    print(s, " ", s == [20, 30]);
}
10 150 90 90 1 2
[20, 30] true

A slice is checked. It carries a static root, the variable that bounds the lifetime of what it views, and it can neither outlive that variable nor survive a shrink that would free the elements. The checker tracks liveness, not scope: a slice whose last use is before a clear is dead, and the clear is fine. Section 12 has the rules, and section 13 the shrink side.

fn main() {
    var line: u8[>..] = [];
    line.append("hello world");
    let w = line[..5];
    print(w);
    line.clear();                         // fine: w's last use is above
    line.append("again");
    let v = line[..];
    // line.clear();                      // error: cannot clear while v is still used
    print(v);
}
hello
again

A slice is a reference, so it follows the same writability rule: a slice of a var array is writable through, a string literal and a slice of a const value are not, and a parameter declared T[:] takes either and is checked with what it is given. A function that only reads documents that with const T[:]. Spec §3.10, §9.5.

Because a slice never copies, it is the natural key for a table. The library's dictionary keyed by const u8[:] stores views into text the program already holds:

import dictionary;

fn main() {
    let text = "the quick brown fox jumps over the lazy dog the fox";
    var counts = dictionary<const u8[:], i32> {};
    each_split(text, ' ') { counts.update(it, 0) { it += 1; }; };
    let n = counts.get("the");
    if n { print(counts.count, " distinct words; 'the' x", n); }
}
8 distinct words; 'the' x3

8. Structs

struct Unit { kind: i32, hp: i32 = 100, x: f32, y: f32, name: u8[] }

A struct is its fields in declaration order, packed: no padding is inserted, unaligned access is assumed cheap, and a struct of an i8[3] and an i32 is 7 bytes. Struct declarations introduce nominal types; two structs with the same fields are different types.

Literals

A literal names its fields, or lists them positionally in declaration order; the two forms do not mix. Fields with a declared default may be left out: trailing ones in the positional form, any of them in the named form. An optional field with no default is null when left out. A literal that ends in .. may leave out any field, which then takes its type's default value: zero, false, null, an empty array or string. A field whose type has no default value (a non-optional reference) must still be given.

struct Unit { kind: i32, hp: i32 = 100, x: f32, y: f32, name: u8[] }
var selected: Unit;                                             // a global with a type only:
                                                                // every field its default
fn main() {
    let a = Unit { kind: 1, x: 1.5, y: 2.5, name: "scout" };   // hp 100
    let b = Unit { 2, 50, 0, 0, "" };                           // positional, all given
    let c = Unit { kind: 3, .. };                               // hp 100, x 0, y 0, name ""
    print(a, " ", b.hp, " ", c, " ", selected.hp);
}
Unit { 1, 100, 1.5, 2.5, "scout" } 50 Unit { 3, 100, 0.0, 0.0, "" } 100

A global var given only a type starts as its type's default value. A local must be initialized at its declaration when its type is not fixed-size, since the value's storage is laid out when it is constructed; a fixed-size local may be declared first and assigned on every path before it is read (section 2).

Named initializers may be written out of declaration order only when swapping them back changes nothing the compiler can see: reads and arithmetic that cannot trap are fine, calls with effects, indexing and loops are not. Construction always proceeds in declaration order, straight into each field. If effects must happen in another order, do them before the literal. Spec §4.2.

Field defaults

A default is an expression evaluated at each construction that leaves the field out, in field order among the explicit initializers. It may name globals and call functions, and it never sees the constructor's locals or sibling fields. default<T>() (Builtins) builds a fixed-size T from its declared defaults, so an invariant a declaration encodes as a default survives generic code that fills in values.

var serial = 0;
fn next() -> i64 { serial += 1; serial }
struct Rec { id: i64 = next(), tag: u8[..4] = "none", score: f64 = 1.0 }

fn main() {
    let a = Rec { .. };
    let b = Rec { score: 2.0 };
    let c = default<Rec>();
    print(a, " ", b, " ", c.id);
}
Rec { 1, "none", 1.0 } Rec { 2, "none", 2.0 } 3

A default may not lead back to a construction that takes it again, which would never end. Spec §3.2.

Field qualifiers and padding

struct Hdr { magic: u8[4], pad, size: u32, let kind: u8, const name: u8[..8] }

fn main() {
    var h = Hdr { magic: "GOOS", size: 4, kind: 1, name: "x" };
    h.size = 8;
    // h.kind = 2;                    // error: kind is a let field
    // h.name.push('y');              // error: name is const
    print(h, " ", h.name.cap);
}
Hdr { "GOOS", 8, 1, "x" } 8

Spec §3.2, §4.4.

Variable-size fields and varint

A struct may hold variable-size parts, and they sit inline, in declaration order. varint is a LEB128 integer field that takes as many bytes as its value needs (one byte up to 63, two up to 8191, and so on; small negatives are as short as small positives). A struct with a varint or a u8[] field is variable-class, and its fields after such a field are found by walking. A varint field is written only at construction; reads give an i64.

struct Item  { sku: u8[varint], qty: varint, cents: varint }
struct Order { id: varint, customer: u8[varint], items: Item[varint] }

fn main() {
    var book: Order[>..] = [];
    book.push(Order { id: 1001, customer: "alice",
                      items: [Item { sku: "SKU-441", qty: 2, cents: 1999 },
                              Item { sku: "SKU-7", qty: 1, cents: 500 }] });
    for o in book {
        var t = 0;
        for it in o.items { t += it.qty * it.cents; }
        print(o.customer, " owes ", t, " cents over ", o.items.len, " items, ",
              bytes_of(book).len, " bytes in all");
    }
}
alice owes 4498 cents over 2 items, 29 bytes in all

That order is 29 bytes with no pointer in it. The price is that book is sequential: iterate it, or keep an index beside it. A variable-class field or element is frozen once built: to change one, rebuild its container. Spec §3.4, §3.6.

A resizable tail

A struct's last field may be a resizable array, which makes the struct resizable: it lives where a growable array can (a local, a global, a by-value parameter, a result), never as an element or a middle field. The tail grows through the struct like any growable array.

struct Stack { name: u8[..8], items: i64[>..<] }

fn push_twice(s: Stack&, v: i64) { s.items.push(v); s.items.push(v); }

fn main() {
    var s = Stack { name: "main", items: [] };
    push_twice(s, 7);
    s.items.pop();
    print(s.name, " ", s.items, " ", s.items.len);
}
main [7] 1

Spec §3.4, Appendix C.2.

Generic structs

struct Pair<T> { a: T, b: T }
struct Node<T> { v: T, next: Node<T>? }

fn main() {
    let p = Pair<i64> { 1, 2 };
    let q = Pair { a: 0.5, b: 1.5 };           // T inferred from the fields
    var n = Node<u8> { v: 1, next: null };
    print(p, " ", q, " ", n.next == null);
}
Pair<i64> { 1, 2 } Pair<f64> { 0.5, 1.5 } true

A generic struct is monomorphized like a function, and its instantiations must be finite: a field naming the struct again with a larger type argument each round (struct P<T> { v: T, next: P<T[]>? }) is rejected. Spec §3.2.

Equality and arithmetic

== and != compare structs memberwise, pads excluded; a reference or slice field compares by identity (address and length), since recursing through it could loop. The arithmetic operators apply memberwise (section 3). Ordering operators exist on numbers only. Spec §4.5.


9. Enums (algebraic data types)

enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }

An enum is a tag followed by the payload of one of its variants. It is the only dynamic polymorphism in the language: no inheritance, no traits, no vtables. A variant's payload is declared like a struct's fields, with the same defaults and .. rules, and a variant with no payload is a plain constant. An enum whose variants all have no payload is a C-like enum, one byte. A variant literal is Shape.Circle { r: 1.0 }, and Shape.Dot for a payload-less one.

Two modes

Every enum type can be stored two ways, chosen at the point of use by the type written there:

Fixed mode (Shape) is what a Rust enum or a C++ variant is: a tag plus room for the largest payload, 17 bytes for every Shape, Dot included. It is fixed-size, so an array of them is indexable, and a value may be overwritten in place with a different variant. In exchange, nothing may point into its payload: match binds the payload by value only. It is usable only when every payload is fixed-size.

Variable mode (Shape.., with the trailing dots) gives each value exactly its variant's size: 1 byte for a Dot, 9 for a Circle, 17 for a Rect. An array of them is sequential, and a value can never change variant, since its neighbours are packed right behind it. In exchange you may take references into a payload and edit it in place. A variant with a variable-size payload (a string, say) can only ever be used in this mode.

enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }

fn main() {
    var shapes: Shape[>..] = [];                 // fixed mode: indexable
    shapes.push(Shape.Circle { r: 1.0 });
    shapes.push(Shape.Dot);
    shapes[1] = Shape.Rect { w: 1.0, h: 1.0 };   // a different variant, in place
    print(shapes);

    var packed: Shape..[>..] = [];               // variable mode: packed
    packed.push(Shape.Circle { r: 1.0 });
    packed.push(Shape.Rect { w: 2.0, h: 3.0 });
    packed.push(Shape.Dot);
    for s in packed {
        match s {
            Rect &r => { r.w += 1.0; },          // edits the payload inside the array
            _ => {},
        }
    }
    print(packed, " ", bytes_of(packed).len, " bytes");
}
[Circle { 1.0 }, Rect { 1.0, 1.0 }]
[Circle { 1.0 }, Rect { 3.0, 3.0 }, Shape.Dot] 27 bytes

Allowing either variant replacement or interior references, but never both, is what keeps the type system sound. Spec §3.5.

match

enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }

fn name(s: Shape) -> u8[:] {
    match s {
        Circle c => if c.r > 1.0 { "big circle" } else { "circle" },
        Rect r => if r.w == r.h { "square" } else { "rectangle" },
        Dot => "dot",
    }
}

fn round(s: Shape) -> bool {
    match s { Circle, Dot => true, Rect => false }
}

fn main() {
    print(name(Shape.Circle { r: 2.0 }), " ", name(Shape.Rect { w: 1.0, h: 1.0 }), " ",
          round(Shape.Dot));
}
big circle square true

Over integers, the arms are literals, named constants, half-open ranges, or a comma-separated list of those, and _ is required. The first arm that matches is taken; an arm that an earlier arm covers entirely is an error. A name is a variant when the scrutinee is an enum and a constant when it is an integer.

let GROUND = 0;
let SHALLOW = 1;
let DEEP = 4;
let LAVA = 7;

fn terrain(t: i64) -> u8[:] {
    match t {
        GROUND => "ground",
        SHALLOW..DEEP => "water",
        LAVA, 10..12, -3 => "hot",
        _ => "rock",
    }
}

fn main() {
    for t in [0, 2, 7, 11, -3, 20] { print(t, " ", terrain(t)); }
}
0 ground
2 water
7 hot
11 hot
-3 hot
20 rock

Spec §8.1.

Case functions

The arms of a match may instead be written as separate functions, one per variant type, and called with the enum value. The call dispatches on the tag through a jump table, and exhaustiveness is checked exactly as for a match: every variant needs exactly one applicable overload, and the result types must agree. This is the virtual-call idiom, without a vtable, and a new variant is a compile error until it gets its case.

enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }

fn area(s: Shape.Circle) -> f64 { 3.14159 * s.r * s.r }
fn area(s: Shape.Rect) -> f64   { s.w * s.h }
fn area(s: Shape.Dot) -> f64    { 0.0 }

fn main() {
    var shapes: Shape[>..] = [];
    shapes.push(Shape.Circle { r: 1.0 });
    shapes.push(Shape.Rect { w: 2.0, h: 3.0 });
    shapes.push(Shape.Dot);
    var total = 0.0;
    for s in shapes { total += area(s); }
    let c = Shape.Circle { r: 2.0 };          // a variant type is an ordinary type
    print(total, " ", area(c));
}
9.14159 12.56636

Variant types (Shape.Circle) are first-class: a value, a parameter, a field, or a reference Shape.Circle&. An overload taking Shape.Rect& binds the payload by reference and needs a variable-mode scrutinee, as a &-binder does. Dispatch is on one parameter position; the other arguments pass through, and every argument is evaluated once before the tag picks the case. Tag dispatch is tried only when no ordinary overload matches the call. Spec §8.2.

Enums with references

An enum may refer to its own type through a reference, which is what makes linked and tree-shaped data in a pool possible. Combined with variable mode and the narrow links of section 11, a tree of mostly-small nodes stops paying for its largest variant everywhere:

enum Expr {
    Num { v: f64 },
    Add { l: Expr..&<u16>, r: Expr..&<u16> },
}

fn eval(e: Expr.Num&) -> f64 { e.v }
recursive fn eval(e: Expr.Add&) -> f64 { eval(e.l) + eval(e.r) }

fn main() {
    var pool: Expr..[>..] = [];
    let a = pool.push(Expr.Num { v: 1.5 });
    let b = pool.push(Expr.Num { v: 2.0 });
    let sum = pool.push(Expr.Add { l: a, r: b });
    print(eval(sum), " in ", bytes_of(pool).len, " bytes");
}
3.5 in 23 bytes

Each Num is 9 bytes and the Add is 5: a tag and two 2-byte offsets. The recursive keyword is explained in section 19.


10. References and optionals

T& is a reference to a T: one machine address, with no pointer arithmetic, no casts, and no way to make it dangle. Spec §3.8.

References are transparent

An expression that denotes a reference behaves as its target in every value context: reads, arithmetic, comparisons (r1 == r2 compares the targets), passing to a by-value parameter, print(r). There is no dereference operator and no ->. A write through a reference, r = v or r += v, writes the target.

Since the reference itself is invisible in ordinary expressions, there is a separate spelling for "the reference, not the target":

fn main() {
    var xs: i64[>..] = [10, 20, 30];
    var r .= xs[0];              // r refers to element 0
    var c = xs[0];               // c is a copy of element 0
    r = 11;                      // writes xs[0]
    c = 99;                      // writes the copy
    r .= xs[2];                  // r now refers to element 2
    r += 1;
    print(xs, " ", c, " ", r, " ", r .== xs[2], " ", r == xs[2]);
}
[11, 20, 31] 99 31 true true

Making and binding references

&x makes a reference to an lvalue: a variable, a field, an element, or what another reference points at. & of a temporary is an error. & of a location that holds a reference yields the stored reference: there are no references to references.

Where the destination's declared type is a reference, an lvalue of the pointee type binds without &, and writing the & is a warning:

fn inc(n: i64&) { n += 1; }

struct Cursor { line: i64& }

fn main() {
    var count = 0;
    inc(count);                  // binds count by reference
    inc(count);
    let r: i64& = count;         // an annotated reference variable
    r += 10;
    let cur = Cursor { line: count };
    cur.line += 100;
    print(count);
}
112

Where a variable's type is inferred, a reference to a fixed-size value decays to a copy of the target: let x = r; copies. The exceptions are an explicit &lvalue initializer, which infers the reference type, and the .= form. A non-fixed lvalue binds by reference whatever the form, since non-fixed values never copy implicitly (section 4):

fn main() {
    var n = 1;
    var xs: i64[>..] = [1];
    let a = &n;                  // a reference: explicit &
    let b .= n;                  // a reference: the .= form
    let c = xs;                  // a reference: xs is not fixed-size
    var r .= n;
    let d = r;                   // a copy of n: r decays
    a += 1; b += 1; c.push(2);
    print(n, " ", d, " ", xs);
}
3 1 [1, 2]

A function whose result type is written T& returns a reference; one whose result type is inferred returns a reference where it returns a non-fixed lvalue or an explicit &x, and a copy where it returns a reference to a fixed-size value. To bind a reference-returning call to a fixed-size element, use .= or annotate: let e .= pool.push(v);.

A let reference variable does not rebind; it says nothing about the target, which is as writable as it was: let r .= xs[i]; r = 5; writes the element.

Optionals

References are never null. T? is an optional T: a nullable reference, represented as a reference whose null is address 0, at no space cost. It composes with any type (i64? is an optional integer, by reference), and applied to a reference type it just makes that reference nullable: T&? is T?. The literal null is its empty value, and an optional struct field with no declared default is null when left out.

An optional is not transparent. It must be narrowed first, by if, while, guard, assert, or a comparison with null, and inside the narrowed region it simply is a T&: transparent, writable through, and bindable with .=. Rebinding the variable ends the narrowing, except in a while whose condition is the test, which runs again before each iteration.

fn first_at_least(xs: i64[:], min: i64) -> i64? {
    for &x in xs { if x >= min { return x; } }
    return null;
}

struct Node { v: i64, next: Node? }

fn main() {
    let xs: i64[4] = [1, 5, 8, 2];
    let big = first_at_least(xs, 5);
    if big { print("first >= 5 is ", big); } else { print("none"); }
    let none = first_at_least(xs, 100);
    print(none == null);

    var c = Node { v: 3, next: null };
    var b = Node { v: 2, next: c };
    var a = Node { v: 1, next: b };
    var cur: Node? = a;
    var sum = 0;
    while cur {                      // narrowed for the body, re-tested each time
        sum += cur.v;
        cur .= cur.next;
    }
    print(sum);
}
first >= 5 is 5
true
6

An optional rebinds with .= to a reference, which narrows it, or to null, which does not. A narrowed optional writes through like any reference; there is no way to write through one without narrowing it. assert(o) narrows for the rest of the block. Spec §3.8.

What references may point to

Anything, with two restrictions that the sections on enums and shrinking explain: nothing may point into the payload of a fixed-mode enum (section 9), and a reference into a grow-shrink array T[>..<] may live in a variable only and must be done with before the array next shrinks (section 13). A reference into a grow-only array stays valid for as long as it can be named, and references into limited arrays survive pops and pushes, reading whatever value the slot holds now.


11. Relative references

A T& is eight bytes, and an address means nothing outside the process that made it. A relative reference is the same link stored as a narrow offset: T&<u8>, T&<u16>, T&<u32>, T&<u64> or T&<varint>. Loading one yields an ordinary T&, storing one re-encodes it, and nothing else about using it changes. There are two forms, differing in what the offset is measured from:

T&<u32>? is the optional form, with offset 0 as null, so a link costs four bytes including its null. Spec §3.9.

A tree in one array

struct Node { key: i32, count: i32 = 1, left: Node&<u32>?, right: Node&<u32>? }

fn insert(pool: Node[>..]&, key: i32) {
    if pool.len == 0 { pool.push(Node { key: key }); return; }
    var cur .= pool[0];
    loop {
        if key == cur.key { cur.count += 1; return; }
        let next = if key < cur.key { cur.left } else { cur.right };
        if next { cur .= next; continue; }
        if key < cur.key { cur.left .= pool.push(Node { key: key }); }
        else { cur.right .= pool.push(Node { key: key }); }
        return;
    }
}

recursive fn in_order(n: Node&, out: i32[>..]&) {
    let l = n.left;
    if l { in_order(l, out); }
    out.push(n.key);
    let r = n.right;
    if r { in_order(r, out); }
}

fn main() {
    var pool: Node[>..] = [];
    for k in [50, 30, 70, 20, 40, 60, 80, 30] { insert(pool, k as i32); }
    var keys: i32[>..] = [];
    in_order(pool[0], keys);
    print(keys, " ", pool.len, " nodes, ", bytes_of(pool).len, " bytes, ", pool[1].count);
}
[20, 30, 40, 50, 60, 70, 80] 7 nodes, 112 bytes, 2

A node is 16 bytes: two i32s and two 4-byte links. cur.left .= pool.push(…) pushes the node and stores the offset, and the push cannot invalidate cur, because nothing moves. Storing a self-relative link requires the compiler to see that the reference and the destination derive from the same root array, exactly: here both are pool. The offset is range-checked at the store only where the array could span more than the width holds.

A self-relative offset is only meaningful where it was stored, so a value that contains one cannot be copied: not by assignment from a variable, not as a by-value argument or match binder, not as an element copy, not by copy(x). Such values are constructed in place (a literal pushed into the array), bound by reference (let n .= f(); rather than let n = f();, which the error suggests), matched with &-binders, and taken by reference in case functions and format overloads. A value whose links are all in pool copies like any other, since its offsets do not depend on where it sits. The one copy that is always allowed is a default value: a literal's .. gives a relative field null. Spec §3.9.

A function or trailing block can return a fresh value with relative links. It is constructed at the receiver, including across function calls and with optimization disabled:

struct Node { value: i64, next: Node&<u32>? }
fn linked(first: Node&, value: i64) -> Node {
    Node { value: value, next: first }
}
fn add<F>(nodes: Node[>..]&) { nodes.push(F()); }
fn main() {
    var nodes: Node[>..] = [Node { value: 1 }];
    nodes.push(linked(nodes[0], 2));
    let first .= nodes[0];
    add(nodes) { Node { value: 3, next: first } };
    let next = nodes[2].next;
    if next { print(next.value); }  // 1
}

Each call must place the result in the same root as the linked element. For example, let n = linked(nodes[0], 2) is an error: the new local is separate storage. Returning an existing node by value is still a prohibited copy. A result that needs its receiver's root also cannot be used as a resize fill or converted through a temporary array representation.

pool in T&<u32 in pool> names a global var or reusable var of a grow-only type whose elements hold a T; the pool is part of the type's identity. The stored value is the offset from the pool's base plus one, so 0 is null in the optional form and a u32 link spans a pool of 4 GB. A load is rooted at pool, exactly, wherever the link was read from, which is what lets a container of links stay usable where a container of plain references would be ambiguous.

A non-optional relative reference has no null, so a value whose links should point at itself could not be built: there is nothing yet to point at. self, written as the whole initializer of such a field, denotes the value the literal is constructing. That is how a circular list gets a sentinel, after which no link is ever null and every insert and remove is the same four stores:

struct Node { val: i64, prev: Node&<u32 in pool>, next: Node&<u32 in pool> }

reusable var pool: Node[>..] = [];
let head = pool.alloc_index(Node { val: 0, prev: self, next: self });   // the sentinel

fn insert_after(p: Node&, v: i64) -> Node& {
    let n .= pool.alloc_ref(Node { val: v, prev: p, next: p.next });
    n.next.prev .= n;
    p.next .= n;
    return n;
}

fn remove(n: Node&) {
    n.prev.next .= n.next;
    n.next.prev .= n.prev;
    pool.free(pool.index_of(n));
}

fn is_end(n: Node&) -> bool { n .== pool[head] }

fn main() {
    insert_after(pool[head], 1);
    let two .= insert_after(pool[head].prev, 2);   // .=: `let two = …` would copy the node
    insert_after(pool[head].prev, 3);
    remove(two);
    var line: u8[>..] = [];
    var n .= pool[head].next;
    while !is_end(n) { format(line, n.val, " "); n .= n.next; }
    print(line, "| ", pool.len, " slots, ", 8 + 4 + 4, " bytes a node");
}
1 3 | 4 slots, 16 bytes a node

Note the .= on two: a Node is fixed-size, so let two = insert_after(…) would copy the node out of the pool, and remove(two) would then be asked to free the slot of a local copy, which the compiler rejects.

self is legal only in a struct or variant literal, only for a non-optional relative-reference field whose pointee is the type being constructed, and for an in pool field only in a literal built inside that pool (a push, an alloc_ref, an element store). The reusable pool and alloc_ref are explained in section 14.

Which form

A parameter that points into a pool is settled per call site, so a function that relinks is specialized per pool and needs nothing passed to it. A function's result cannot be declared relative: declare T& or T?, and a relative slot that receives it encodes it.

An index for a sequential array

An array of variable-size elements cannot be indexed (section 5), but a second array of 4-byte links into it can be:

enum Mark { Circle { r: f64 }, Label { text: u8[varint] }, Dot }

var marks: Mark..[>..] = [];
var mark_at: (Mark..&<u32 in marks>)[>..] = [];

fn main() {
    mark_at.push(marks.push(Mark.Label { text: "hello, goose" }));
    mark_at.push(marks.push(Mark.Circle { r: 1.5 }));
    mark_at.push(marks.push(Mark.Dot));
    for i in [2, 0, 1] {
        match mark_at[i] {
            Circle c => print(i, ": circle ", c.r),
            Label l => print(i, ": label \"", l.text, "\""),
            Dot => print(i, ": dot"),
        }
    }
    print(bytes_of(marks).len, " bytes of marks, ", mark_at.len, " links of 4 bytes");
}
2: dot
0: label "hello, goose"
1: circle 1.5
24 bytes of marks, 3 links of 4 bytes

push returns a reference to the element it built; stored into mark_at, that reference becomes the mark's offset from the base of marks. The array an in offset is measured from must be a global; a local array is indexed the same way with plain references, (Mark..&)[>..], at eight bytes an entry.


12. Lifetimes: roots, stores and writability

This is the whole of Goose's "borrow checker":

A reference or slice must not outlive the variable that owns its target, and must never observe its target at a wrong type.

There are no lifetime annotations anywhere in the language, and no aliasing or exclusivity rules: two references to the same thing are fine, because without shared-memory concurrency aliasing alone cannot break type safety here. Spec §9.1.

Roots

Every reference and slice has one or more static roots: each a local or global variable, or a temporary, that bounds the lifetime of its target. A root is exact when that variable's own storage contains the target, and inexact when it only bounds the target's lifetime. Roots are inferred, never written: &x is rooted at x exactly, a slice of a function's parameter is rooted at whatever the caller passed, and a reference read out of a container is rooted at the variables whose storage could hold its target. Functions are specialized per distinct root, which gives the precision of lifetime parameters with no syntax. Spec §9.2.

The rules

A lifetime error names both ends: the reference's root and its destination.

fn longest(a: u8[:], b: u8[:]) -> u8[:] { if a.len >= b.len { a } else { b } }

fn main() {
    var outer: u8[>..] = [];
    outer.append("outer");
    var w = outer[..];
    {
        var inner: u8[>..] = [];
        inner.append("inner text");
        w = longest(outer, inner);   // error: storing a reference rooted at inner
    }
    print(w);
}

The result of longest may point into either argument, so it is rooted at both, and inner does not outlive w. Move the declaration of w inside the block, or copy the text out, and it compiles.

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

fn main() {
    let t = make();
    let s = t[..];                   // fine: t lasts
    let u = make()[..];              // error: a view of a temporary
    print(s, " ", u);
}

Writability

Everything is writable unless its type says otherwise. const T is a value whose contents cannot be written: a const i64[3] cannot have an element assigned, and const T& and const T[:] are a reference and a slice through which the target cannot be written, grown or shrunk. Writability is inferred per instantiation, like roots:

fn upper_first(s: u8[:]) { if s.len > 0 && s[0] >= 'a' { s[0] -= 32; } }

struct Named { name: const u8[:] }

fn main() {
    var buf: u8[>..] = [];
    buf.append("goose");
    upper_first(buf);                  // a writable argument: fine
    let n = Named { name: "literal" }; // a const slot takes a literal
    let who = if buf.len > 3 { n.name } else { buf[..] };   // read-only, since one branch is
    print(buf, " ", who);
    // upper_first("literal");         // error: writes through a read-only parameter
}
Goose literal

Spec §9.5.

Where a reference read out of a container may point

A container names a scope, not the storage its contents point into, so the root of a reference read out of a container is re-derived: it may point into any variable in scope whose own storage can hold a value of its type, a candidate. Where there is exactly one candidate, the reference is rooted there exactly; where there are several, it is rooted at all of them, inexactly, and a rule that needs the exact identity of the array (a relative-link store, index_of, which array a shrink through it frees) reports the candidates it could not choose between: "n was read out of slots and may point into pool or spare". Two things narrow this: a relative link in pool always points into pool, exactly, and a local holder whose stores the checker has all seen is rooted at what was stored into it. Spec §9.5.

Reading the errors

When the checker rejects something, read the whole message. It names the reference and where it was bound, the holder and the line that stored into it, the instantiation and the call chain that produced it. Use those two locations to decide which end to change: use the view for the last time before the shrink, move the shrink, or move the declaration.


13. Growing, shrinking and assigning whole arrays

Growth is simple: push and append bump the stack, and a limited array grows within its capacity. Shrinking is where the array kinds differ, because a shrink frees elements that something may still point at.

Grow-only arrays shrink where nothing points in

"Grow-only" names a guarantee, not an operation set: a T[>..] never shrinks while a reference or slice into it can be live. Memory below the top never moves and is never reused while the owner lives, so every reference into the array stays valid for as long as it can be named. A shrink (pop, pop_n, resize downward, clear, or assigning the whole array) is legal exactly where the compiler can see that nothing rooted in the array is still in use:

The test is liveness, not scope: a slice whose last use is before the shrink is dead, and the shrink is fine. So a scratch buffer refilled per iteration hands out slices of itself freely. The error, when there is one, names the holder and the line where a reference into the array was stored into it.

fn reset(buf: u8[>..]&) { buf.clear(); }

fn main() {
    var line: u8[>..] = [];
    for i in 3 {
        reset(line);                        // nothing points into line here
        format(line, "line ", i + 1);
        let head = line[..4];
        print(head, "|", line[4..]);        // head's last use
    }
    var holder: u8[:][>..] = [];
    holder.push(line[..]);
    // line.clear();                        // error: holder may hold a reference into line,
                                            //        stored at the push above
    print(holder.len);
}
line| 1
line| 2
line| 3
1

A shrink through a reference parameter, or of a global, cannot see the caller's variables, so it is checked at every level: each function records which globals and which parameters' targets it shrinks, and a call is checked as if it made those shrinks in the caller. pop, pop_n and resize also need fixed-size elements, since a sequential array cannot find its last element. Spec §5.1.

Grow-shrink arrays keep their references in variables

A T[>..<] holds fixed-size elements and shrinks from anywhere: on a local, through a reference, on a global, as a struct's tail. Its price: references and slices into it live in variables only. They may be bound to a local, passed down and returned, but never stored into a field, an element, a global or a literal. After a pop, that stack-top memory may be reused by a different type, so no reference into it may outlive the next shrink, and keeping such references out of storage is what makes the check a scan of the variables in scope. A shrink is an error while any such variable is still used afterwards; the error names the variable and where it was bound.

fn main() {
    var stack: i64[>..<] = [];
    for i in 5 { stack.push(i * 10); }
    let top .= stack[4];
    print(top);                             // last use of top
    stack.pop();                            // fine
    var popped = 0;
    while stack.len > 2 { popped += stack.pop(); }
    print(stack, " popped ", popped);
}
40
[0, 10] popped 50

A call is balanced for an array when the array is never shorter during the call than when it began: a function that takes let mark = xs.len, pushes, and ends with xs.resize(mark). A balanced call frees nothing a view taken before it points into, so it is not a shrink where it is made. That is how a recursion shares one scratch stack (section 19). Spec §5.2.

Limited arrays

T[..k] and T[..] grow and shrink freely within their capacity, and references into them are allowed and survive pops: the capacity and the element type are fixed, so a slot once written stays a valid value of the element type forever. A stale reference reads whatever value the slot holds now, never corrupt memory. Exceeding the capacity aborts. Spec §5.3.

Assigning a whole array

An lvalue may be assigned after construction when its size cannot change, or when it can absorb the change:

struct Rec { name: u8[], score: i64 }

fn main() {
    var xs: i64[>..] = [1, 2, 3];
    xs = [4, 5];                            // a shrink, then the new contents
    var ys: i64[>..] = [xs[0] + 1];
    xs = copy(ys);                          // computed from other storage: fine
    var recs: Rec[>..] = [];
    recs.push(Rec { name: "a", score: 1 });
    for r in recs { r.score = 2; }          // a fixed-size field: assignable
    // for r in recs { r.name = "bb"; }     // error: a variable-class field is frozen
    var one = Rec { name: "a", score: 1 };
    // one = Rec { name: "bb", score: 3 };  // error: and so is a variable-class local
    recs = [Rec { name: "bb", score: 3 }];  // rebuild the container instead
    print(xs, " ", recs, " ", one.score);
}
[5] [Rec { "bb", 3 }] 1

Spec §4.4.

Construction in progress

While a value is being built in place inside an array (an element being pushed, a call's result being appended, the new contents of a whole assignment), nothing may grow that array: neither the expression being built nor a function it calls. xs.push(f(xs)) where f pushes into its argument is rejected. Spec §1.3, §4.2.


14. Reusable pools

Some data has lifetimes that do not nest: cache entries, nodes of a graph that is edited, files deleted in any order. For these, a grow-only array of fixed-size elements can be declared reusable, and the compiler pairs it with a hidden freelist:

Operation Meaning
a.alloc_index(v) -> i64 the index of a slot holding v: a freed slot if there is one, else a fresh push
a.alloc_ref(v) -> T& the same, as a reference
a.free(i) records slot i for reuse; aborts if i is out of range

free releases no memory and ends no lifetime. Every element remains a valid, live value of its type at all times, so a reference to a freed and reused slot reads a different value of the correct type: a logic bug of the same kind as a stale index, never memory corruption, type confusion or an out-of-bounds access. That is the whole extent of the unsafety, and it is what lets tree-mutating workloads run without an allocator.

struct Item { id: i32, weight: f32 }

fn main() {
    reusable var pool: Item[>..] = [];
    let s0 = pool.alloc_index(Item { 10, 1.0 });
    let s1 = pool.alloc_index(Item { 11, 1.0 });
    pool.free(s0);
    let s2 = pool.alloc_index(Item { 12, 2.0 });   // takes slot s0 back
    let node .= pool.alloc_ref(Item { 13, 3.0 });
    pool.free(pool.index_of(node));                // a reference turned back into its slot
    print("slots ", s0, " ", s1, " ", s2, ", pool length ", pool.len);
}
slots 0 1 0, pool length 3

A pool may be a local or a global. It never shrinks (its freelist keeps every slot live), and push and append still add at the end without consulting the freelist. A T[>..]& parameter bound to a pool is a pool reference: the pool's operations work through it, since it carries the freelist beside the address, and a variable that was first bound to a pool can only be rebound to a pool of the same kind. Spec §5.4.

Slice pools

reusable[] hands out runs of elements instead of single slots, with a freelist of spans that merges neighbours:

Operation Meaning
a.alloc_slice(n) -> T[:] n elements from the first free span that holds them, else from the end of the array, which grows; every element added is a default value
a.free_slice(s) records the run s for reuse
a.realloc_slice(s, n) -> T[:] s resized to n elements, in place where it can be, else freed and copied to where alloc_slice(n) puts it

This is what the child lists of an n-ary tree want: each node's entries one contiguous run that can be walked as a plain slice.

fn main() {
    reusable[] var pool: i64[>..] = [];
    let a = pool.alloc_slice(3);
    a[0] = 1; a[1] = 2; a[2] = 3;
    let b = pool.alloc_slice(2);
    b[0] = 7; b[1] = 8;
    pool.free_slice(a);
    let c = pool.alloc_slice(2);            // from the freed run, reset to defaults
    let d = pool.realloc_slice(b, 4);       // b ends the array: grows in place
    print(c, " ", d, " ", pool.len, " ", pool);
}
[0, 0] [7, 8, 0, 0] 7 [0, 0, 3, 7, 8, 0, 0]

The slice handed to free_slice or realloc_slice must be one of the pool's runs; where the checker cannot see that, the call checks at run time and aborts on a slice that is not. A grown slice may be moved, so realloc_slice is unavailable for an element type holding self-relative links. The single-slot operations do not exist on a slice pool, nor these on a plain reusable one. Spec §5.4.


15. Index types and checkfree functions

Every index is bounds-checked, and the compiler removes the checks it can prove unnecessary (section 16). The one check no analysis removes is an index read out of another array, dist[adj[k]], since that is a fact about the contents of one array against the length of another. Index types move that check to where the index enters the program.

An index type uN in a (u8, u16, u32 or u64) is an unsigned integer that is below a.len wherever it can be observed, and a bound type uN upto a is one that is at most a.len: the end of a range. a names a global var or reusable var of a grow-only type with fixed-size elements, and the type is part of the identity: u32 in nodes is neither u32 nor u32 in edges. The value is the plain integer.

struct Node { lo: u32 upto edges, hi: u32 upto edges, weight: i32 }
var nodes: Node[>..] = [];
type NodeId = u32 in nodes;          // an alias reads better at every use
var edges: NodeId[>..] = [];

An index type takes no postfix: an array of them is (u32 in nodes)[>..] or, with the alias, NodeId[>..], and a for binder's written one is parenthesized: for v: (u32 in nodes) in ….

struct Node { lo: u32 upto edges, hi: u32 upto edges, weight: i32 }
var nodes: Node[>..] = [];
type NodeId = u32 in nodes;
var edges: NodeId[>..] = [];

fn add_node(w: i32) -> NodeId {
    let none = 0 as (u32 upto edges);
    nodes.push(Node { lo: none, hi: none, weight: w });
    return (nodes.len - 1) as NodeId;
}

fn main() {
    for w in [5, 3, 8, 1] { add_node(w as i32); }
    for v in nodes.len {                                   // v links to v + 1 and v + 2
        nodes[v].lo = edges.len as (u32 upto edges);
        for d in 1..3 { if v + d < nodes.len { edges.push((v + d) as NodeId); } }
        nodes[v].hi = edges.len as (u32 upto edges);
    }
    var total = 0;
    for v in nodes.len {
        let n = nodes[v];
        for k in n.lo..n.hi { total += nodes[edges[k]].weight; }   // no checks left
    }
    let w = edges[0];
    let plain: u32 = w;                                    // as a number
    print(total, " ", w, " ", plain + 1);
}
21 1 2

Spec §3.11, docs/design/index_types.md.

checkfree fn

checkfree asks the compiler to reject the program if a bounds check is left anywhere in the function's text after bounds-check elimination, in every specialization and every copy inlined into a caller. It changes nothing about what the program computes. Every call in such a function must be to another checkfree function, a builtin or an extern fn, and a block it calls must be written in a checkfree function. The other aborts stay, assert in particular, which is how such a function states its preconditions once, outside its loops:

var nodes: i32[>..] = [];
type NodeId = u32 in nodes;

checkfree fn sum_at(ids: NodeId[:], weights: i32[>..]&) -> i64 {
    assert(weights.len >= nodes.len);          // once; the loop is then check-free
    var t = 0;
    for id in ids { t += nodes[id] + weights[id]; }
    return t;
}

fn main() {
    for w in [1, 2, 3] { nodes.push(w as i32); }
    var weights: i32[>..] = [10, 20, 30];
    let ids: NodeId[2] = [2 as NodeId, 0 as NodeId];
    print(sum_at(ids, weights));
}
44

The compiler must prove at least these inside a checkfree function: a constant index into a fixed array; an index of a's index type into a; one into b where b.len >= a.len was asserted and neither array changed since; a for index into the array whose length is its count; a[i] after a condition established 0 <= i < a.len; and a repeat of a check already passed. The error names the check that stayed and why. checkfree goes after export and before inline, simd, recursive or fn, and a nested function inherits it. Spec §7.14.


16. Expressions and control flow

The language is expression-oriented: if, match, block and a bare { … } are expressions, and a block's value is its trailing expression. Assignment, ++ and -- are statements. The operators are C's and Rust's, with Rust's precedence (Appendix A).

if

let hp = 30;
let kind = if hp > 50 { "healthy" } else if hp > 0 { "hurt" } else { "down" };
if hp > 0 { print(kind); }               // as a statement: no else needed

Used as a value, an if needs a final else, and its branches join into one type. A literal branch adapts to the other's type: if c { x } else { 0 } has x's type, a u8 for a u8 x, and if c { x } else { 0.5 } is an f32 for an f32 x. An integer branch beside a float branch converts to the float. A construct whose branches are all literals is itself literal-like and adapts wherever it meets a type. Arrays and slices of one element type join as a slice: where one branch is a string literal and the other a u8[..16] field, the result is a const u8[:] viewing the field, with nothing copied. Spec §6.4.

struct Item { name: u8[..16], count: i64 }
fn main() {
    let item = Item { name: "bolt", count: 0 };
    let name = if item.count == 0 { "none" } else { item.name };   // a const u8[:]
    let k: u8 = if item.count > 0 { 1 } else { 2 };               // literals adapt to u8
    print(name, " ", k);
}
none 2

A trailing construct that cannot supply a value, an if without else, a match with an arm that ends in a return, a loop none of whose breaks carries a value, is a statement, so a function whose result is inferred may end in one and produce nothing.

block, break with a value, and bare blocks

block { … } gives break a target without a loop, so a chain of checks can bail out with a value. A bare { … } is only a scope: a break inside it leaves the enclosing loop or block. All the break E of one construct must agree on E's type.

struct P { alive: bool, hp: i64 }
fn main() {
    let p = P { alive: true, hp: 0 };
    let verdict = block {
        guard p.alive else { break "gone"; }
        guard p.hp > 0 else { break "dead"; }
        "standing"
    };
    let doubled = { let t = p.hp + 1; t * 2 };
    var n = 0;
    let found = loop { n += 7; if n % 5 == 0 { break n; } };
    print(verdict, " ", doubled, " ", found);
}
dead 2 35

guard

guard c; runs the rest of its block only where c holds, and guard c else { … } runs the else block instead. It is if turned inside out, and it jumps nowhere by itself: directly in a function body the rest of the block is the rest of the function, in a loop body skipping the rest goes on to the next iteration, and in a function value's body it ends that call of the value. Leaving more than the block is the else block's job: guard c else { return; } leaves the function, guard c else { break; } the loop. The code after a guard knows the condition held, which includes narrowing an optional: after guard r else { return; }, r is a T&.

fn half(n: i64) -> i64 { guard n % 2 == 0 else { return -1; } n / 2 }
fn tenfold(p: i64?) -> i64 { guard p else { return 0; } p * 10 }

fn main() {
    for i in 5 { guard i % 2 == 0; print("even ", i); }
    let v = 4;
    print(half(8), " ", half(7), " ", tenfold(v), " ", tenfold(null));
}
even 0
even 2
even 4
4 -1 40 0

Spec §6.4.

Loops

for i in 3 { }              // 0, 1, 2
for i in 10..13 { }         // 10, 11, 12: half-open
for i: i32 in 0..10 { }     // i is an i32; each bound must be, or widen to, one
for i: u8 in 0..256 { }     // every u8, the end one past the type's last value
for x in xs { }             // copies, for fixed-size elements; references for others
for &x in xs { x *= 2; }    // references: the mutation form
for x, i in xs { }          // with the index, an i64
for x, i: u8 in grid { }    // the index at a narrower type, where the array's length fits it
while cond { }
loop { if done { break; } }
fn main() {
    var xs: i64[>..] = [1, 2, 3];
    for &x in xs { x *= 10; }
    for x, i in xs { if i == 0 { xs.push(x + 1); } }   // the push extends the walk
    var count: u8 = 0;
    for i: u8 in 0..256 { count += 1; }
    print(xs, " ", count);
}
[10, 20, 30, 11] 0

(count wrapped to 0 after 256 increments, as a u8 does.) Spec §6.5.

const for: expansion at specialization time

const for specializes a fragment once per entry of a compile-time sequence: the fields of a struct, the variants of an enum, a list of types, or a list of constants. It is the language's reflection, with no runtime iterator and no metadata: each copy of the body is ordinary code.

struct Point { x: i64, y: i64, label: u8[..8] }

fn show_fields<T>(v: T&) {
    const for <N, A> in fields<T> { print(N, " = ", v.A); }
}

fn scaled<T, F>(v: T&) -> T {
    T { const for <N, A> in fields<T> { A: F(N, v.A), } }
}

fn main() {
    var p = Point { x: 2, y: 3, label: "pt" };
    show_fields(p);
    const for <U> in types<i8, u16, f32> { print(default<U>()); }
    const for <C> in [1, 2, 4] { let a: u8[C] = [0; C]; print(a.len); }
    let q = scaled(p) { n, x => x };
    print(q);
}
x = 2
y = 3
label = pt
0
0
0.0
1
2
4
Point { 2, 3, "pt" }

fields<T> binds each field's name (a string constant) and an accessor usable as v.A or as the label A: in a literal of that struct type; types<…> binds a type; […] binds a constant (literals, named constants, and the integer expressions an array size allows). A statement fragment is a scope of its own per entry, and a constructor fragment contributes initializers. break and continue cannot cross an expansion.

variants<E> binds each variant's name and the variant V, written where a variant's name is: V as a type (and fields<V> for its payload), V { … } to build one, V c or V &c as a match arm, and V as a value when it has no payload. A match fragment contributes arms, checked as written ones: every variant once, _ last.

enum Shape { Circle { r: i64 }, Rect { w: i64, h: i64 }, Dot }

fn show<T>(value: T) {
    match value {
        const for <N, V> in variants<T> {
            V v => {
                print(N);
                const for <F, A> in fields<V> { print("  ", F, " = ", v.A); }
            },
        }
    }
}

fn main() {
    let s: Shape = Shape.Rect { w: 3, h: 4 };
    show(s);
    const for <N, V> in variants<Shape> { print(N); }
}
Rect
  w = 3
  h = 4
Circle
Rect
Dot

Spec §6.5, docs/design/const_for.md.

Functions that never return

abort(msg) and exit(code) never return, and the checker knows it: a branch that ends in one gives no value, statements after one are not checked, and the knowledge propagates through calls, so a function may end in a call to one without a dummy result.

fn fail(what: u8[:]) { abort(str("cannot ", what)); }

fn value(ok: bool) -> i64 {
    if ok { 42 } else { fail("compute") }
}

fn main() { print(value(true)); }
42

An unknown recursive call is assumed to return. Spec §7.3, §9.3.

Bounds checks

Every index into an array or slice is checked, and every conversion into an index type (section 15); a check that fails aborts the program. The compiler removes the checks it can prove unnecessary, and the language is designed to give that analysis what it needs: a T[>..] shrinks only at a pop, pop_n, resize or clear the analysis sees, so a bound established before a push still holds after it; % is Euclidean, so a reduction is in range by construction; fixed sizes and capacities are constants; and every call site records what it proves about its arguments. The idiom that helps most is an assert of the lengths at the top of a kernel:

let W = 8;
fn blur_row(above: u8[:], here: u8[:], below: u8[:], out: u8[:]) {
    assert(above.len == W && here.len == W && below.len == W && out.len == W);
    for x in 1..W - 1 {                     // every index is x plus a constant: no checks
        let sum = above[x - 1] as i64 + above[x] + above[x + 1] +
                  here[x - 1] + here[x] + here[x + 1] +
                  below[x - 1] + below[x] + below[x + 1];
        out[x] = (sum / 9) as! u8;
    }
}

goose --bce-lines reports, per line, which checks survived. The other aborts that remain in every build are division by zero, a limited array's capacity overflow, shrinking below empty, a relative-reference offset that does not fit its width, a failed assert, and data-stack exhaustion, all of them clean aborts with a message. Spec §9.3, §10.5.


17. Functions

fn name(a: i64, var b: f64, xs: i32[:]) -> i64 { … }
fn generic<T>(a: T, b: T) -> T { … }
fn also_generic(a, b) { … }              // untyped parameters are generic
fn scaled(a: i32, b: i32 = 0, c: f32 = 1.0) -> f32 { … }   // scaled(2) is scaled(2, 0, 1.0)

Functions are free functions. There are no methods and no impl blocks, but x.f(a) is exactly f(x, a) for any function, so method-call syntax is available wherever it reads better. Resolution tries fields and built-in members first, then functions. Top-level declarations are order-independent. Spec §7.1.

Parameters

var counter = 0;
fn bump(r: i64& = counter, by: i64 = 1) { r += by; }
fn limit(x: i64, lo: i64 = 0, hi: i64 = 100) -> i64 {
    if x < lo { lo } else if x > hi { hi } else { x }
}
fn scale<T>(x: T, by: T = 2) -> T { x * by }

fn main() {
    bump(); bump();
    var mine = 10;
    bump(mine, 5);
    let b: u8 = 21;
    print(counter, " ", mine, " ", limit(250), " ", limit(-5, -10), " ", 7.limit(0, 5), " ",
          scale(b), " ", scale(1.5));
}
2 15 100 -5 5 42 3.0

Results

A function declares its results after ->, several of them separated by commas. Several results are a calling convention, not a tuple type: a call in an ordinary value context yields the first, let a, b = f(); binds them all, x, xs[i] = f(); assigns them to places that exist, and return f() forwards all of them. By convention a trailing bool or error value says whether the rest means anything.

fn divide(a: i64, b: i64) -> i64, bool {
    if b == 0 { return 0, false; }
    return a / b, true;
}

fn main() {
    let q, ok = divide(7, 2);
    var r = 0;
    var flag = false;
    r, flag = divide(1, 0);
    print(q, " ", ok, " ", r, " ", flag, " ", divide(9, 3));
}
3 true 0 false 3

Only return and a call carry several values: a branch cannot end in a, b, so each branch returns on its own.

A result type may be left out where it can be inferred, in which case it is taken from the first return (or the body's final expression), as an un-annotated let takes its initializer's type: a non-fixed lvalue other than the function's own local, or an explicit &x, makes the result a reference, and a reference to a fixed-size value is returned as a copy. Across a recursive cycle every signature must be written out.

A non-fixed result is constructed directly in its destination (section 4): the caller's variable, the element being pushed, the argument slot of another call. When every return of such a result returns the same local, that local is the destination from its declaration on, so return xs costs nothing (the compiler warns where a second returned local keeps the first from its destination). A function's own local is moved by return; a part of it, or of a temporary, dies with the call and has to be returned as copy(x). Spec §7.1, §7.3.

Overloading

Functions overload by parameter types, resolved statically: the unique exact match wins, then a generic exact match, then a match that needs a coercion (array to slice, a literal fitting a type, implicit widening). A candidate's rank is its worst argument's rank, and ties are errors. A default is no argument, so f(a, b = 0) beside f(a) makes every one-argument call ambiguous. Only when no ordinary candidate matches is tag dispatch (section 9) tried, and a candidate that converts an integer argument to a float comes last of all: f(2) calls f(i32) beside f(f64), and sqrt(n) with an integer n is ambiguous between the two widths (sqrt(n as f64)). The expected result type never selects an overload.

fn describe(x: i64) -> u8[>..]     { str("int ", x) }
fn describe(x: f64) -> u8[>..]     { str("float ", x) }
fn describe(xs: i64[:]) -> u8[>..] { str(xs.len, " ints, first ", xs[0]) }

fn main() {
    let xs: i64[3] = [4, 8, 15];
    print(describe(7), "; ", 2.5.describe(), "; ", xs.describe(), "; ", describe(xs[1..]));
}
int 7; float 2.5; 3 ints, first 4; 2 ints, first 8

Spec §7.1.

Nested functions

A function may be declared inside a function, and it reads and writes the enclosing function's locals as free variables, under those variables' normal rules. Names resolve where the nested function is declared, and the nested functions of one block may call each other in either order, as the functions of a recursive-descent parser do; the enclosing code can call one only after its declaration. Nested functions of one name in one block overload like top-level ones, so case functions can be nested too. A nested function is a compile-time entity: it is passed as a function value and never escapes.

struct Employee { name: u8[..12], dept: u8[..8], salary: i32 }

fn main() {
    var staff: Employee[>..] = [];
    staff.push(Employee { name: "ada", dept: "eng", salary: 120 });
    staff.push(Employee { name: "linus", dept: "ops", salary: 90 });
    staff.push(Employee { name: "grace", dept: "eng", salary: 135 });
    var budget = 0;
    var seen = 0;
    fn spend(e: Employee&) {          // writes main's locals
        budget += e.salary;
        seen++;
    }
    for &e in staff { if e.dept == "eng" { spend(e); } }
    print("eng budget ", budget, " over ", seen, " people");
}
eng budget 255 over 2 people

Spec §7.5.

Function qualifiers

Qualifier Meaning Section
recursive fn the entry of a recursive cycle 19
extern fn, extern "sym" fn a C function 24
export fn, export "sym" fn a Goose function callable from C 24
thread_fn a worker's entry point 22
checkfree fn no bounds check may remain in it 15
inline fn inlined at every call, whatever its size below
simd fn built once per instruction-set level below

The qualifiers come in a fixed order: export or extern first, then checkfree, simd, inline, recursive, and fn or thread_fn.

inline asks for the function to be inlined at every call; without it the compiler decides from the function's size and its number of calls. It changes nothing about what the program computes, and is refused with extern, export, simd, recursive and thread_fn.

simd asks for the function to be built once for the target's baseline instruction set and once more for each wider one it supports (AVX2 and AVX-512 on x86-64), with the widest version the CPU has chosen at each call. It suits a function holding a hot loop the C compiler can vectorize: byte transforms, codecs, kernels over arrays. Every version computes exactly what the baseline does, so nothing about a program's output depends on it; a backend may ignore it, as the C backend does under compilers other than clang and under the in-process TinyCC backend. Spec §7.12, §7.13.

simd fn checksum(xs: u8[:]) -> u32 {
    var h: u32 = 0;
    for x in xs { h += x as u32 * 31 + 7; }
    return h;
}

inline fn clamp01(x: f32) -> f32 { if x < 0 { 0 } else if x > 1 { 1 } else { x } }

fn main() { print(checksum("goose"), " ", clamp01(1.5)); }
16806 1.0

18. Generics and function values

Type parameters

An untyped parameter is generic. An explicit <T> says that two parameters must agree, or names a type no parameter carries. Everything is monomorphized: a generic body is typechecked only at an instantiation where every type is concrete, and errors are reported at the offending instantiation with the compile-time call chain that produced it. Type variables are never checked abstractly, so a generic body may do anything the concrete types allow, with no trait bounds.

fn twice(x) { x + x }
fn span<T>(a: T, b: T) -> T { if a > b { a - b } else { b - a } }
fn zero<T>() -> T { default<T>() }

fn main() {
    print(twice(21), " ", twice(1.5), " ", span(3, 8), " ", span(2.5, 1.0), " ", zero<f64>() + 0.5);
}
42 3.0 5 1.5 0.5

Type arguments are inferred from the argument types. Where several arguments mention one type variable, the typed ones bind it and a literal then adapts: max(n, 0) with a u32 n is the u32 instantiation. A float literal binds before an integer one, and [] and null bind nothing. An explicit list f<i64>(x) binds the leading type parameters in order; it is needed when no argument mentions a parameter (qget<i64>(), zero<f64>()). f< begins a type argument list only when the <…> is followed by (, { or .Variant {; otherwise < is a comparison.

Inside generic code, .= on a location that is not a reference assigns as = does. That lets generic code move whole values of a type parameter's type with .=: where T is a reference type it moves the reference itself, where = would write through it, and where T is a value it is a plain assignment. The library's swap_at is written that way, and so swaps the elements of an array of references, relative ones included, as it does those of any other array.

Where a literal is the only thing binding a type variable, the parameter is a literal parameter inside the specialization: a constant of unknown value that adapts wherever it meets a type, so push_n(flags, 1, n) with fn push_n<A, T>(xs: A&, v: T, n: i64) pushes a u8 into a u8[>..] with no cast at the call, and push_n(flags, 300, n) is an error naming the push that takes a u8.

Floating context. An f32 destination may refine an otherwise unconstrained float computation in a generic call: with integer width and height, let scale: f32 = max(0.5, min(width / 960.0, height / 600.0)); computes both calls in f32. The rule applies to a function returning a bare T whose T parameters are bare values, given literals and float expressions of literals and integers; a typed argument commits the type. Spec §7.7.

Function values

Function values are compile-time entities passed as generic parameters, not runtime data. A function argument is a function name, a bound generic function parameter, or a block literal; the : fn(…) bound on the parameter is optional documentation. Every call of one is direct and inlinable, and it cannot escape: storing one, returning one, or putting one in data is a compile error. There are no closure objects and no function pointers, so a higher-order function compiles to exactly the loop it looks like.

fn foo<F: fn(i64)>(a: i64) { F(a); }
foo(1) { print(it) }                 // the trailing-block form, with the implicit `it`
foo(1) { x => print(x) }             // named block parameters
fn h(v: i64) { print(v) }
foo(1, h)                            // a named function
fn each_pair<T, F>(xs: T[:]) {
    for i in 0..xs.len - 1 { F(xs[i], xs[i + 1]); }
}

fn each_step<F>(lo: i64, hi: i64, step: i64 = 1) {
    var i = lo;
    while i < hi { F(i); i += step; }
}

fn first_multiple_of_7(lo: i64, hi: i64) -> i64 {
    each_step(lo, hi, 3) { if it % 7 == 0 { return it; } };   // returns from first_multiple_of_7
    return -1;
}

fn main() {
    let xs: i64[5] = [4, 8, 15, 16, 23];
    var gaps: i64[>..] = [];
    each_pair(xs) { a, b => gaps.push(b - a); };
    let evens = xs.filter() { it % 2 == 0 };
    let total = fold(xs, 0) { acc, x => acc + x };
    let big = xs.find() { it > 10 };
    sort(gaps) { a, b => a > b };
    print(gaps, " ", evens, " ", total, " ", first_multiple_of_7(10, 40), " ",
          xs.map() { it * 2 }, " ", big);
}
[7, 7, 4, 1] [4, 8, 16] 66 28 [8, 16, 30, 32, 46] 15

The library's filter and map build their result straight into the variable receiving it, with no intermediate array. A HOF never states what its function value returns: map's result element type is whatever F(x) is in that instantiation. Spec §7.6, §7.7.


19. Recursion

Recursion is opt-in and annotated. The entry function of every recursive cycle is marked recursive fn, every function in the cycle has a fully explicit signature, and the key restriction is:

No function in the cycle may call back into the cycle while one of its non-fixed locals is in scope.

A growable or variable-size local holds a data stack from its declaration to the end of its block, and if one were in scope at a recursive call, every activation would need a stack of its own. So a recursive function owns no growable data across a recursive call, and what must outlive the call is owned outside the cycle and passed in: a reference, a slice, a pool, or a struct of references to several tables. Scratch declared in a block that ends before the call costs the recursion nothing. The same holds of the temporaries of the calling statement: at a call into the cycle, no non-fixed temporary may be live, which rules out a str(…) as an argument of the call, or the call inside a print. Recursion depth then costs native stack only, and exhausting it is a clean abort.

struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? }

recursive fn show(n: Node&, depth: i64) {
    {                                      // scratch in a block that ends before the calls
        var line: u8[>..] = [];
        for i in depth { line.append("  "); }
        format(line, n.key);
        print(line);
    }
    let l = n.left;
    if l { show(l, depth + 1); }
    let r = n.right;
    if r { show(r, depth + 1); }
}

recursive fn count(n: Node?) -> i64 {
    guard n else { return 0; }
    return 1 + count(n.left) + count(n.right);
}

fn main() {
    var pool: Node[>..] = [];
    let a .= pool.push(Node { key: 1 });
    a.left .= pool.push(Node { key: 2 });
    a.right .= pool.push(Node { key: 3 });
    show(a, 0);
    print(count(a), " nodes");
}
1
  2
  3
3 nodes

Sharing scratch across the recursion

A grow-shrink array handed to the cycle by reference can serve every activation at once: each takes let mark = scratch.len, pushes its own entries above its callers', and ends with scratch.resize(mark). Such calls are balanced (section 13), so a view of the activation's own part stays usable across them.

recursive fn walk(scratch: i64[>..<]&, depth: i64) -> i64 {
    let mark = scratch.len;
    scratch.push(depth);
    let mine = scratch[mark..];
    var total = 0;
    if depth < 3 { total = walk(scratch, depth + 1); }
    total += mine[0];
    scratch.resize(mark);
    return total;
}

fn main() {
    var scratch: i64[>..<] = [];
    print(walk(scratch, 0), " ", scratch.len);
}
6 0

A program whose state is several growable tables (nodes, text, a stack) owns each as a local of a non-recursive driver function and passes the recursion one struct of references to them; or writes the recursive functions nested inside the driver, where they reach the tables as free variables with no struct at all, as the samples' parsers do.

The rules in detail

Spec §7.8.


20. Long-distance return and errors

There is no exception mechanism and no Result type, and no required error convention. For a call with two outcomes, a trailing bool is enough; for "found or not", a T?, which costs nothing because it is a reference into the input. Both are ordinary return values (section 17). The third idiom is for a failure deep inside nested calls that should land in one place.

return … from

return E from f returns E as the result of the innermost active call of f, unwinding every frame in between. The functions between keep their plain signatures: no error parameter, no ? on each call, nothing to propagate.

struct User { name: u8[..16], age: i32 }

fn load(text: u8[:]) -> User[>..], u8[] {
    var users: User[>..] = [];
    var lineno = 0;
    each_split(text, '\n') {
        lineno++;
        if it.len > 0 { users.push(parse_record(it, lineno)); }
    };
    return users, "";
}

fn parse_record(line: u8[:], lineno: i64) -> User {
    let comma = find(line, ",");
    guard comma >= 0 else { return [], str("line ", lineno, ": expected name,age") from load; }
    let age, ok = parse_int(trim(line[comma + 1..]));
    guard ok else { return [], str("line ", lineno, ": bad age") from load; }
    return User { name: trim(line[..comma]), age: age as i32 };
}

fn report(text: u8[:]) {
    let users, err = load(text);
    if err.len > 0 { print("error: ", err); return; }
    print("loaded ", users.len, " users");
}

fn main() {
    report("ada, 36\nlinus, 20\n");
    report("ada, 36\nlinus\n");
    report("ada, x\n");
}
loaded 2 users
error: line 2: expected name,age
error: line 1: bad age

The same machinery is what lets a block return from its enclosing function through a higher-order function (section 18). The library's exception module packages it as exception::try<E>(body) { error => … } and exception::throw(value) (stdlib.md). Spec §7.9.

Stopping the program

assert(c) aborts unless c holds, and narrows an optional it is given; abort(msg) aborts with a message; exit(code) ends the program with an exit code. The last two never return, which the checker uses (section 16). None of them is catchable. Spec §9.3.


21. Program structure

Files and imports

A program is compiled whole, from the root file named on the command line. import a.b.c; includes the file a/b/c.goose once, resolved relative to the root of the importing file's package (for the program's own files, the directory of the root file), then as a module of a package the root declared, then in the standard library's directory. import .a.b; with a leading dot resolves relative to the importing file instead. An import loads a file and nothing more: it creates no namespace and brings no names into scope beyond what the file declares. The standard library's modules are imported by name: import std;, import dictionary;.

Packages

import name from "source"; in the root file declares the package name, a directory holding its entry name.goose, and imports that entry: the source is a directory relative to the root file ("../mylib") or a git repository at a tag or commit ("owner/repo@v1" on GitHub, "host/owner/repo@ref", or a URL), which the compiler fetches and builds once. Only the root file declares packages; the same line in any other file, a library's own included, is a requirement that the root must satisfy, so a program's dependencies are flat, pinned and in one place. packages.md has the whole story, including a program's own C and how to make a package.

Namespaces

namespace image; at the top of a file, before its declarations, places them in the namespace image; a file without it declares into the global namespace, and several files may share a namespace. From elsewhere a declaration is image::Pixel, image::brightness(p), image::Shape.Circle { r: 1.0 }; ::name names a global declaration that a namespaced one shadows, or a builtin. A declaration may spell its namespace itself, fn image::brightness(…), or fn ::main() inside a namespaced file.

import std;
namespace geo;

struct Pt { x: i64, y: i64 }
fn norm1(p: Pt) -> i64 { abs(p.x) + abs(p.y) }     // std's abs, found globally
fn format(out: u8[>..]&, p: Pt) { format(out, "(", p.x, ",", p.y, ")"); }

fn ::main() {                                       // declared in the global namespace
    let p = geo::Pt { -3, 4 };
    print(p, " ", geo::norm1(p));                   // the format hook follows the type
}
(-3,4) 7

An unqualified name resolves lexically first (locals, type parameters, nested functions), then in the current declaration's namespace, then in the global namespace and the builtins. A function name's overload set is that of the first namespace in this order that declares the name at all; sets never merge across namespaces, so a namespaced hash overload reaches the global ones as ::hash(x). UFCS follows the same rule for the calling code's namespace, so p.norm1() inside the global main above would not find geo::norm1. The format hook is the one type-directed exception: print(p) finds geo::format for a geo::Pt from anywhere. A global variable and functions may share a name, since a call resolves past variables to functions. Namespaces affect name resolution, type identity and generated C names, and nothing else: there is no privacy, no re-export and no nesting. Spec §11.1, docs/design/namespaces.md.

Globals and initialization

Globals initialize before main, in declaration order within a file and imported files first, so an initializer may use a global from a module the file imports, and may call functions. Naming a global before its initializer has run, in an earlier initializer or in a function one calls, is an error. The whole program runs inside an implicit outermost scope that owns the globals: they are the outermost roots for the lifetime rules, and a global growable array owns the bottom of a data stack for the program's life, a natural whole-program arena. const globals of flat fixed type with compile-time initializers live in static data, shared and read-only.

var log: u8[>..];                        // a var with only a type: empty
let NAMES = ["ada", "grace"];            // static data
let FIRST = NAMES.len - 1;               // a named constant
var counts: i64[NAMES.len] = [0; NAMES.len];

fn main() {
    log.append("started");
    counts[FIRST] += 1;
    print(log, " ", NAMES[FIRST], " ", counts);
}
started grace [0, 1]

main

fn main() { } in the global namespace of the root file is the entry point. A fn main in an imported file is ignored entirely, so a runnable file doubles as a library: give it fn main_x() and a fn main() { main_x(); } wrapper, and importers call main_x. A program's arguments come through the os module (args(), arg_count()), and exit(code) sets the process exit code.

embed_file

embed_file("path") reads a file when the program is compiled, relative to the source file making the call, and gives its bytes as a const u8[:] into static data: a font, a table, a shader. The path is a string literal or a let global holding one, and each distinct file is read once. embed_shader does the same for GLSL, compiling it for the graphics module (stdlib.md). Spec §12.


22. Threads

There is no shared mutable memory. A worker is declared thread_fn, and the compiler compiles it and everything it calls as a separate program with its own data stacks and its own copies of the globals it uses, taken from the spawning program at the moment of the spawn. A worker that increments a global increments its own; main's is what it was. There are no data races to have, and no locks, atomics or memory orderings anywhere in the language.

Builtin Meaning
thread_spawn(worker, args…) -> i64 starts a worker; its id, never reused
thread_spawn_contained(worker, args…) -> i64 starts a worker whose aborts end only that worker
thread_wait(id) -> bool blocks until that worker has ended and its storage is released; false if it was contained and aborted
thread_wait(id, msg) -> bool as thread_wait(id), and the first such wait appends a contained worker's abort message to msg
hardware_threads() -> i64 a sensible worker count, never more than the data stacks allow
qput(v) -> bool puts v on the queue for its type; false, at once, if the queue is full
qget<T>() -> T takes the next T, blocking
qget<T>(ms) -> T, bool the same, waiting at most ms milliseconds; else T's zero value and false
qpoll<T>() -> T, bool takes one if there is one; else T's zero value and false
qlimit<T>(n) lets the queue for T hold at most n messages

Values cross between programs through typed queues, one per type, and whatever crosses, arguments included, must be flat: no references or slices at any depth. That is cheap because a flat Goose value is contiguous, so a job or a result carries real data, variable-size parts included, as one copy. A thread_fn's parameters have no defaults.

struct Job    { n: i32 }
struct Result { n: i32, digits: i32 }

fn count_digits(n: i32) -> i32 { var d: i32 = 1; var v = n; while v >= 10 { v /= 10; d++; } d }

thread_fn worker() {
    loop {
        let job = qget<Job>();                   // blocks on the queue for Job
        guard job.n >= 0 else { break; }         // -1 means stop
        qput(Result { n: job.n, digits: count_digits(job.n) });
    }
}

fn main() {
    let n = max(1, hardware_threads());
    for i in 8 { qput(Job { n: (1 << (i * 3)) as i32 }); }
    for i in n { qput(Job { n: -1 }); }          // one quit job per worker
    var ids: i64[>..] = [];
    for i in n { ids.push(thread_spawn(worker)); }
    var total = 0;
    for i in 8 { let r = qget<Result>(); total += r.digits; }
    for id in ids { thread_wait(id); }
    print(total, " digits in total");
}

A queue has no limit of messages until qlimit gives it one. Past it, qput returns false without waiting, so a server can turn work away rather than queue it without end; qput(v); as a statement ignores the answer. To bound the memory a queue may hold, set its limit to that memory divided by the size of a typical message.

A worker's return releases everything it owned, whether or not anyone waits for it; queued messages outlive their sender; workers still running when main returns are killed. A global that is not flat is an error in any function a thread_fn reaches, unless it is a let whose references all lead to static data, as a string literal's do. Programs with threads need a real C compiler: goose build, since the in-process TinyCC backend cannot place thread-local storage. Spec §11.2.

Contained workers

A failed check or abort(msg) in a worker started with thread_spawn_contained ends only that worker, which releases what a return would. The message goes to the waiter instead of stderr: every thread_wait for the worker returns false, and the first thread_wait(id, msg) appends the message, with its source location, to msg. Later waits get no message, so ask for it once and keep it. A supervisor can log it and start another:

thread_fn serve(attempt: i64) {
    assert(attempt >= 2);                        // a bug the first two hit
    print("attempt ", attempt, " served");
}

fn main() {
    var attempt = 0;
    var why: u8[>..] = [];
    while !thread_wait(thread_spawn_contained(serve, attempt), why) {
        print("attempt ", attempt, " failed: ", why);
        why.clear();
        attempt++;
    }
}

Stack overflow, exit(code), and aborts on main's thread still end the program, and a worker that a contained one starts with thread_spawn is not contained. Files and other resources opened through extern fn stay open. Spec §11.2.


23. Serialization

A structure whose links are self-relative offsets means the same wherever it sits in memory, so saving it is a write of its bytes and loading it is a read. Three builtins do this for any array:

Builtin Meaning
to_bytes(a) -> u8[>..], to_bytes(a, out) a framed image of the array: a varint byte count, then its element region; fresh, or appended to out
bytes_of(a) -> u8[:] the element region alone, as a read-only view: nothing copied
from_bytes<T[>..]>(b: u8[:]) -> T[>..], bool a verified array from an untrusted image; false and an empty array if it is not one

Bytes arriving from outside the program are the one place a reference could enter without the compiler having proved it, so from_bytes verifies before the bytes become a value: the framing, every tag, every length, canonical varints, and every link landing on an element start of that same image. A corrupt or hostile file is a false, never a wild reference. What it verifies is safety, not integrity: edited data bytes still describe a well-formed structure, and a checksum is the program's business.

struct Node { word: u8[..8], count: i32, left: Node&<u32>?, right: Node&<u32>? }

fn insert(pool: Node[>..]&, word: u8[:]) {
    if pool.len == 0 { pool.push(Node { word: word, count: 1 }); return; }
    var cur .= pool[0];
    loop {
        let c = compare(word, cur.word[..]);
        if c == 0 { cur.count += 1; return; }
        let next = if c < 0 { cur.left } else { cur.right };
        if next { cur .= next; continue; }
        if c < 0 { cur.left .= pool.push(Node { word: word, count: 1 }); }
        else { cur.right .= pool.push(Node { word: word, count: 1 }); }
        return;
    }
}

fn lookup(pool: Node[>..]&, word: u8[:]) -> i32 {
    guard pool.len > 0 else { return 0; }
    var cur .= pool[0];
    loop {
        let c = compare(word, cur.word[..]);
        if c == 0 { return cur.count; }
        let next = if c < 0 { cur.left } else { cur.right };
        guard next else { return 0; }
        cur .= next;
    }
}

fn main() {
    var index: Node[>..] = [];
    each_word("the quick fox and the lazy dog and the fox") { insert(index, it); };
    var image: u8[>..] = index.to_bytes();          // a framed image of the array
    var loaded, ok = from_bytes<Node[>..]>(image);  // verified before it becomes a value
    print(ok, " ", loaded.len, " nodes, 'the' x", lookup(loaded, "the"), ", ",
          bytes_of(index).len, " bytes");
    image[image.len - 3] = 200;                     // tamper with a link ...
    var bad, bok = from_bytes<Node[>..]>(image);    // ... and it is false and an empty array
    print(bok, " ", bad.len);
}
true 6 nodes, 'the' x3, 126 bytes
false 0

What comes back is an ordinary array: it indexes, it grows, and references into it are ordinary references. from_bytes also builds T[>..<] and the T[] family, and checks an array of index types against its array. Links into a field of an element, and in pool links, are not serializable; a bytes_of view is never writable, since bytes written through it would be links the checker never proved. The byte contract is in docs/design/serialization.md. Spec §3.9, §9.4, §12.


24. Calling C and being called from C

extern fn

A declaration without a body binds a Goose signature to a C function; the optional string is the C symbol, and the Goose name is used otherwise. It is top-level, fully typed, not generic, and joins overload resolution like any function. A call compiles to a direct C call.

struct Stats { lo: i32, hi: i32, mean: f64 }

extern "cbrt" fn cube_root(x: f64) -> f64;      // libm, by its C name
extern fn hypot(x: f64, y: f64) -> f64;
extern fn crc32_bytes(s: const u8[:]) -> u32;   // a slice crosses as { data, len }
extern fn stats_of(xs: i32[:], out: Stats&);    // a struct filled through a pointer
extern fn c_version(out: u8[>..]&);             // a builder C appends to

What may cross is exactly what has a plain C shape: the integer and float scalars and bool; any flat fixed-size value (structs, fixed and static-capacity arrays, fixed-mode enums) by value as its packed C type; T& to such a T as a pointer; T[:] of such a T as a { data, len } struct; and u8[>..]&, a builder C appends to through gs_bld_append. Results are one scalar or flat fixed-size value, or nothing. Everything else, varints, references inside structs, variable or resizable values by value, optionals, is rejected at the declaration. A parameter is as writable as it says, so a read-only argument needs it declared const. C must not keep a borrowed argument beyond the call. The program's own C goes in a sidecar foo.build beside foo.goose (c_sources = ["impl.c"];) or arrives with --include header.h; the math and os modules are built on this interface. Spec §7.10, packages.md.

export fn

export fn makes a Goose function callable from C: the compiler emits a C wrapper under the function's name, or the string given. The function must be top-level, non-generic, with explicit parameter types and at most one typed result; defaults, return … from, pool parameters and builders are not supported at the boundary. Scalars and flat fixed-size values are returned by value, and a const u8[:] result comes back as a pointer plus an extra int64_t * length parameter (the bytes are not NUL-terminated).

export fn hello_world() -> const u8[:] { return "Hello, world!" }
export "c_add" fn add(a: i32, b: i32) -> i32 { a + b }

fn main() { print(hello_world(), " ", add(20, 22)); }     // runs once, inside goose_init
Hello, world! 42

goose --standalone --header hello.h -o hello.c hello.goose builds the C for a C host: no main of its own, and a header declaring goose_init(argc, argv), which the host calls once before anything else. It sets up the runtime and the globals and runs the Goose main, if there is one, so main is where a library keeps its start-up code. Exported functions are not thread-safe: call them from the thread that called goose_init. Spec §7.11.


25. Builtins

The language's own functions. Everything else is the standard library, written in Goose under stdlib/ and documented in stdlib.md.

Builtin Meaning Section
print(a, b, …) the arguments' text and a newline, as one write 6
str(a, b, …) -> u8[>..] the text as a fresh string, built at its destination 6
format(out, a, b, …) the text appended to out, any growable u8 array 6
assert(c) aborts unless c, a bool or an optional, which it narrows 20
abort(msg), exit(code) end the program; neither returns 20
copy(x) -> T a fresh copy of the stored value x names 4
default<T>() -> T the value a fixed-size T has before anything is written to it, declared defaults applied 8
hardware_threads(), thread_spawn(worker, args…), thread_spawn_contained(worker, args…), thread_wait(id), thread_wait(id, msg) workers 22
qput(v), qget<T>(), qpoll<T>(), qlimit<T>(n) the typed queues 22
to_bytes(a), to_bytes(a, out), bytes_of(a), from_bytes<T[>..]>(b) serialization 23
embed_file(path), embed_shader(…) a file's bytes, or a compiled shader, read at compile time 21

The array members, .len, .cap, .push, .append, .pop, .pop_n, .resize, .clear, .index_of, are in section 5, and the pool operations .alloc_index, .alloc_ref, .free, .alloc_slice, .realloc_slice, .free_slice in section 14. Spec §12.


Appendix A. Keywords and operators

Keywords: import namespace struct enum type fn thread_fn export simd recursive let var extern const pad reusable if else while for in loop block guard match return break continue as true false null self bool varint i8 i16 i32 i64 u8 u16 u32 u64 f32 f64.

Contextual keywords, names everywhere else: from (after a return value), upto (after an unsigned type in a type), inline and checkfree (before fn, recursive, simd or thread_fn), fields and types (in a const for header).

Operators, tightest first; the precedence is Rust's:

Level Operators
postfix f(…), a[i], a[x..y], .field, x as T, x as! T
unary -, !, ~, &
multiplicative *, /, %
additive +, -
shifts <<, >>
bitwise and &
bitwise xor ^
bitwise or |
comparison <, <=, >, >=
equality ==, !=, .==, .!=
logical and &&
logical or ||

Assignment statements: =, .=, and the compound form of every binary operator (+=, <<=, &=, …); ++ and -- on integer lvalues. A range a..b appears only in slicing brackets, for headers and match arms. Spec §6.1, Appendix D.

Appendix B. Glossary