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 a description of the whole language. It is as complete as the specification, but easier to read, and it has an example program for every rule.

Each section has a pointer of the form "spec §3.3", at its end or within it, to the specification section that has the final say on the topic. Features unique to Goose get the most space. Features that work as they do in C or Rust, such as if and while, 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 either the body of a main or a set of declarations, and is written so that it can be pasted into a program. 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 (command_line.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 whatever convention the file uses.

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, fields, variants and types in a const for header, and has_default and same in a const if condition. 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 holding the byte's value, and it accepts the same escapes as a string literal.

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 """…""". They have no escapes, and the next """ ends the text. On one line, the text is everything between the delimiters, including backslashes and quotes. 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 be indented to match the surrounding code. 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 these constructs as an operand, put it in parentheses:

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 only forbids assigning to the name as a whole: the contents of a let array or struct are as writable as their type allows, 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. Assigning it in a loop it is declared outside of, or in a function called more than once, counts as assigning it twice.

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 this easy for a large array. 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 are initialized before main runs, imported files first and then in declaration order within a file, 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 named constant can be used exactly as its literal could: 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 ordinary values of their type and do not adapt to other types. 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 an overview.

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 apply const to an outer reference or slice, use parentheses: const (u8[:])& is a read-only reference to a slice variable. An index type takes no postfix of its own, so an array of an index type 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 need 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. If the types are equal, that type is the common type.
  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 is used: 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 on it. A C-style remainder would need a guard before the check could be dropped.

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 the two missing functions, 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, since that is what hashes, random number generators and bit tricks expect from unsigned math. Signed overflow aborts in a debug build (--debug) and wraps in a release build. Division or remainder by zero always aborts, and so does i64.min / -1. A debug build checks each operation as it executes after optimization. It is therefore a tool for finding bugs, not a guarantee about every intermediate value 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, local or global, whose initializer is such an expression adapts the same way at each use. This is how PI in the standard library works at 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, and if, while and guard require it. There is no integer-to-bool coercion, so if n { … } with an integer n is an error; write if n != 0 instead. && 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

Within a kind, a conversion the compiler can prove value-preserving is implicit, and anything that could lose a bit or flip a sign needs 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, not even in a debug build: it is for when losing bits is intended. Conversions to a float are never checked, in either form. Float to integer conversion is fully defined: 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 casts that 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 gives no warning, whether it decides a type that nothing else gives (let k = 5 as u32;), the width of an operation ((a as i64) * b with i32 operands), or how a float is rounded.

Comparing u64 with signed values. u64 is the only 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. Unary - applies memberwise too, 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; the language has no operator overloading.

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 constant shifted by a variable count still adapts like a constant: 1 << k takes the type of its destination.

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 four size classes:

An ordinary compound type's class is the largest of its parts' classes. A root struct always retains its class, even with only fixed fields. A type is flat when it holds no reference, slice or relative reference at any depth and is not a root struct; 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. This is the entire memory system. There is no malloc, free list, garbage collector, reference counting or destructor.

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

  1. Values are pushed in lifetime order and freed all at once when the owning scope ends. Freeing them is a single store: the stack's top is set 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 as soon as 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. This 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: such a copy takes O(size) time, so it should be visible in the source. A variable, field or element of variable or resizable class binds by reference when it reaches a destination that is a reference, a slice, an untyped parameter, an un-annotated let/var, or a for binding. At a destination that needs a value of its own, it is an error. 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 applies to calls: f(xs) passes a growable array by reference to a slice parameter, to a T[>..]& parameter and to an untyped parameter alike. Spec §4.1.

Built in place

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


5. The array family

There is no single array type. Instead there is a family of array types that differ only in what can happen to their size. All of them are represented as [metadata][elements…], inline and packed, never as 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 is fixed by its declaration (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
.move_from(src) slices of fixed-size elements copies equal-length slices with overlap-safe ordering
.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
.range_index(s, msg?) -> i64 the same the start of a wholly contained slice; checked when provenance does not prove it
.check_length(n, msg?) -> i64 grow-only, grow-shrink, fixed-size elements validates a count against the data-stack reservation; returns it without changing the array

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 that the compiler can prove points into that very array (section 12). range_index accepts slices whose origin is only known at runtime: a nonempty slice must be element-aligned and wholly within the array's current length. An empty slice, including a default or foreign empty slice, returns zero. check_length rejects negative counts and counts whose elements alone would exceed a data-stack reservation, including oversized unsigned counts. It neither allocates storage nor promises that an append fits the array's remaining space. Both checks apply in every build. A string literal as a last argument, a.range_index(s, "msg"), is the message a failed check aborts with instead of the runtime's own; the standard library's pools word theirs this way. dst.move_from(src) requires equal lengths and aborts before writing if they differ. It copies the original source values even when the slices overlap; the source is not cleared. Both operands are evaluated once, left to right. The destination must be writable, and the usual lifetime and relative-reference copy restrictions apply. Empty slices may be copied, including default slices. The standard-library pool types and deprecated reusable 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 the same 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, is called to render a T wherever a T occurs in an argument of any of the three builtins, so a type defines its text form in one place. A generic type renders through a generic overload such as fn format<T>(out: u8[>..]&, b: Box<T>), unless a non-generic one for its instance exists; an overload on a bare type variable is not used for rendering. The overload is looked up in the namespace T is declared in, then globally, so the type decides how it is printed, not the code that prints it. A T that is not fixed-size is never copied implicitly, so its overload takes it by reference.

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 operating on 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: each part takes a few bytes, and no text is 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 converts to a slice 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. A slice 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 allowed. 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, while a string literal and a slice of a const value are not. A parameter declared T[:] accepts either, and is checked against the argument it is given. A function that only reads through a slice parameter documents that by declaring the parameter 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[] }

An ordinary 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 the compiler can tell that putting them back in order changes nothing: reads and arithmetic that cannot trap are fine; calls with effects, indexing and loops are not. Construction always proceeds in declaration order, directly 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 that is evaluated at each construction that leaves the field out. It is evaluated in field order, among the explicit initializers. It may name globals and call functions, but it can never refer to the constructor's locals or to sibling fields. default<T>() (Builtins) builds a fixed-size T from its declared defaults, so an invariant that a declaration encodes as a default is kept even in 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 uses the same default again, since that 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 downside 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

An ordinary struct's last field may be a resizable array, which makes the struct resizable: it can live wherever a growable array can (a local, a global, a by-value parameter, a result), but 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.

Root structs

Like an ordinary struct, a root struct bundles related state behind named fields. It lifts the requirement that those fields form one contiguous value: each field has independent storage, so several fields can grow separately. Fields can be fixed, variable or resizable, in any order, and all share the lifetime of the owning variable.

The tradeoff is placement: an owning root struct must be a local or global variable, rather than a field or array element. It can group application state, a workspace, or a library instance:

root struct Workspace<T> {
    items: T[>..] = [],
    scratch: i64[>..<] = [],
    calls: i64 = 0
}

fn add<T>(s: Workspace<T>&, value: T) -> T& {
    s.calls += 1;
    let result .= s.items.push(value);
    s.scratch.push(s.calls);
    s.scratch.clear();                // does not invalidate result
    return result;
}

fn main() {
    var state = Workspace<i64> { .. };
    let first .= state.add(7);
    state.add(8);
    print(first, " ", state.items, " ", state.calls);
}
7 [7, 8] 2

Construct an owner with a literal directly in a local or global declaration. Field defaults and .. work normally; a typed global may omit its initializer. The compiler determines all component stacks statically and restores them on scope exit. Existing recursive-cycle restrictions apply to owners with nonfixed fields: declare such state outside the cycle and pass a reference into it.

Functions receive a root by reference. An unannotated binding from an existing owner also borrows it; .= is available to make that intent explicit. Owner references can be optional, stored in ordinary structs or arrays, returned, and captured by nested functions, subject to normal lifetime and writability rules. Field access, UFCS, reflection and formatting work through those references.

A mutable grow-only array field of a global var root can be named in compact reference and index types: Node&<u32 in pool.items>, u32 in pool.items, and u32 upto pool.items. The name resolves to that particular field's storage; another field or owner is a different array. Only direct fields are supported: the path cannot follow an alias, reference, index or nested field.

The owner itself cannot be copied, reassigned, passed or returned by value, used as a temporary, or placed inside another struct or array. This applies even to a root with no resizable fields. root only acts as a modifier before struct; variables, fields and functions may still be named root.

A root has no packed byte image, whole-value equality or arithmetic, or public C layout. pad belongs in an ordinary component struct, not on the root. Serialize eligible fields separately, for example to_bytes(state.items); that image contains only the selected component. Workers can construct local roots but cannot receive owners or owner references, or use a root global directly or through another function. A root global unused by a worker is fine. Nested owning roots and fixed arrays of owners are deferred. 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 ordinary structs memberwise, excluding pads; 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 form of dynamic polymorphism in the language; there is no inheritance, no traits and 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 }, or Shape.Dot for a variant with no payload. Those of a generic enum, such as enum Opt<T> { None, Some { v: T } }, give its type arguments: Opt<i64>.Some { v: 1 }, Opt<i64>.None.

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 the layout of a Rust enum or a C++ variant: a tag plus room for the largest payload, so every Shape is 17 bytes, including Dot. 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 gives the effect of a virtual call without a vtable, and adding a variant is a compile error until it has its own 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 ordinary types: they can be the type of 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 are passed through, and every argument is evaluated once before the tag selects 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 no longer needs room for its largest variant in every node:

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 syntax 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. A function 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 cannot be rebound, but that 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 only 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 first be narrowed by if, while, guard, assert, or a comparison with null. Inside the narrowed region it 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

A reference may point to anything except a fixed-mode enum's payload (section 9). A reference into a grow-shrink array T[>..<] has a further restriction: it may be held only in a variable and must no longer be used when the array next shrinks (section 13). References into grow-only arrays stay valid for as long as they can be named. References into limited arrays stay valid across pops and pushes, and read whatever value their slot currently holds.


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& and storing one re-encodes it; otherwise it is used like any other reference. 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 in any way: by assignment from a variable, as a by-value argument or match binder, as an element copy, or 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();, as 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 only copy that is always allowed is of a default value: a literal's .. sets a relative field to 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 array (including deprecated reusable var) or a mutable array field of a global var root, such as T&<u32 in pool.items>. The array must be grow-only and its 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. This 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. This is how a circular list gets a sentinel. After that, no link is ever null, and every insert and remove is the same four stores:

import slot_pool;

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

var pool = slot_pool<Node> {};
let head .= pool.alloc_with() { 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.items.index_of(n));
}

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

fn main() {
    insert_after(head, 1);
    let two .= insert_after(head.prev, 2);   // .=: `let two = …` would copy the node
    insert_after(head.prev, 3);
    remove(two);
    var line: u8[>..] = [];
    var n .= head.next;
    while !is_end(n) { format(line, n.val, " "); n .= n.next; }
    print(line, "| ", pool.items.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, and 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 array (a push, an element store, or a slot_pool.alloc_with builder). alloc_with selects the destination slot before calling the builder, so self points at that slot. alloc_ref accepts an already-constructed value; use it when no destination-dependent construction is needed. Both operations are explained in section 14.

Which form

Whether a parameter points into a pool is settled at each call site, so a function that relinks is specialized per pool and needs no extra argument for the pool. 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. When that reference is stored into mark_at, it 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 can be indexed the same way with an array of plain references, (Mark..&)[>..], at eight bytes per 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. A root is a local or global variable, or a temporary, that bounds the lifetime of the reference's 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 without any syntax for them. 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 identifies a scope, but not necessarily the storage its contents point into. The checker therefore derives the root again when reading a reference from a container. Each variable in scope whose storage can hold the pointee's type is a candidate. With one candidate, the reference is rooted there exactly. With several, it is rooted at all of them inexactly.

Some operations need an array's exact identity: storing a relative link, calling index_of, or determining which array a shrink through the reference frees. If the checker cannot choose, it reports the candidates: "n was read out of slots and may point into pool or spare". Two rules narrow the candidates. A relative link in pool points exactly into pool. A local holder whose stores are all known to the checker is rooted at the values 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 can hand 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. The trade-off is that 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, the freed memory at the top of the stack may be reused by a different type, so no reference into it may outlive the next shrink. Because such references are kept out of storage, the check only needs to scan 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, for example a function that takes let mark = xs.len, pushes, and ends with xs.resize(mark). A balanced call frees nothing that a view taken before it points into, so it does not count as a shrink at the place where it is made. This 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. Use the standard library's slot_pool<T> for individual slots and slice_pool<T> for contiguous runs. Each is a root struct bundling an items array and its reuse metadata in one declaration. Owners must be local or global variables; functions take them by reference.

reusable and reusable[] are deprecated. The builtins remain available for existing code and the remaining migration gaps. Their removal is a later step.

Import slot_pool to use these operations:

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. Such logic bugs are the full extent of the unsafety, and this design is what lets tree-mutating workloads run without an allocator.

import slot_pool;
struct Item { id: i32, weight: f32 }

fn main() {
    var pool = slot_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.items.index_of(node));          // a reference turned back into its slot
    print("slots ", s0, " ", s1, " ", s2, ", pool length ", pool.items.len);
}
slots 0 1 0, pool length 3

A pool may be a local or a global. Its items array never shrinks through the pool API; items.len includes free slots. Use items for ordinary array access, and pass the owner as slot_pool<T>&. Elements must be fixed-size. alloc_index and alloc_ref take normally copyable values. For fresh values with self-relative links or pool-relative self, use pool.alloc_with() { Node { value: 1, me: self } }: the builder runs once, directly in the chosen slot, and returns a reference to it. Do not edit its free_slots metadata or shrink items directly. See the library contract for supported types and lifetime rules.

Slice pools

slice_pool<T> hands out runs of elements instead of single slots, with a tree of free 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 returned element is reset to its default
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 suits the child lists of an n-ary tree: each node's entries are one contiguous run that can be walked as a plain slice.

import slice_pool;
fn main() {
    var pool = slice_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.items.len, " ", pool.items);
}
[0, 0] [7, 8, 0, 0] 7 [0, 0, 3, 7, 8, 0, 0]

Free and resize accept subranges of allocated runs. A nonempty slice must be aligned and wholly within items; where the checker cannot prove that, the call checks at runtime. Empty/default slices can be freed harmlessly, and growing an empty slice allocates a new run. Double frees remain logical errors; there is no occupancy check. A grown slice may be moved, so realloc_slice is unavailable for an element type holding self-relative links. Allocation and free do support defaultable self-relative elements. Pass an owner as slice_pool<T>&, and use its items array for indexing and slicing. The library contract covers defaults, subranges, empty slices, high-water storage and relocation.

Deprecated builtin pools

The legacy declarations reusable var pool: T[>..] = []; and reusable[] var pool: T[>..] = []; still provide the corresponding operations directly on the array, with hidden metadata. Their parameter type is T[>..]&; pool provenance carries the metadata through calls. Existing push and append operations add at the end without using freed spans. Their unchanged semantics are specified in spec §5.4.

To migrate, import the appropriate module, declare slot_pool<T> {} or slice_pool<T> {}, update owner parameters to the corresponding root reference, and use pool.items for array operations. Global index/pool-relative types can name pool.items. For a value that needs construction in its receiving slot, replace alloc_ref(value) with alloc_with() { value }; obtain its index with pool.items.index_of if needed. Ordinary value allocations and slice operations keep their method names. Owner transfer to workers remains a migration gap; keep the deprecated builtins for code requiring worker snapshots. Worker-local library pools already work.


15. Index types and checkfree functions

Every index is bounds-checked, and the compiler removes the checks it can prove unnecessary (section 16). The check that no analysis removes is an index read out of another array, dist[adj[k]], since whether it is in range depends on the contents of one array compared with 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. A bound type uN upto a is an unsigned integer that is at most a.len, used for the end of a range. a names a global var array (including deprecated reusable var) or a mutable array field of a global var root, such as u32 in graph.nodes. It must be grow-only with fixed-size elements, and the array is part of the type's 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, so an array of them is (u32 in nodes)[>..] or, with the alias, NodeId[>..]. An index type written on a for binder is parenthesized too: 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 remain, in particular assert, which such a function uses to state 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

Inside a checkfree function, the compiler must prove at least these checks unnecessary:

The error names the check that remained 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 is a statement: an if without else, a match with an arm that ends in a return, or a loop none of whose breaks carries a value. 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 does not jump anywhere 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; 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. In the code after a guard, the condition is known to hold, 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.

defer

defer s; and defer { … } run their body when the block they are written in ends, however it ends: falling off the end, break, continue, return, a block's return through a higher-order function, or a return … from passing through the function. A defer in a loop body runs at the end of every iteration. An exit runs the defers written before it, latest first, so a cleanup sits next to what it cleans up and is skipped where that never happened. The body is ordinary code run later: it sees variables as they are at the exit, and on a return it runs after the returned value is built and before the block's storage is released.

var depth = 0;

fn describe(items: i64[:]) -> i64 {
    depth++;
    defer depth--;                   // every way out of describe restores it
    print("depth ", depth);
    var count = 0;
    for x in items {
        defer count++;               // once per iteration, continue included
        if x < 0 { continue; }
        if x > 100 { return -1; }
    }
    return count;
}

fn main() {
    print(describe([1, 2, -3]), " ", describe([5, 500, 7]), " ", depth);
    {
        defer print("block done");
        print("in the block");
    }
}
depth 1
depth 1
3 -1 0
in the block
block done

The body is checked where it is written, so it names only what is declared before it, and a var optional narrowed before it is not narrowed inside it, since an assignment before an exit may undo that. Three things it may not do, each a compile error:

A defer belongs in a function: a global initializer has no scope exit. Nothing runs a defer where no exit happens: abort, a failed assert or bounds check, exit, and a contained worker's abort end the code without running any, and the process or the worker's end reclaims what the body would have released. Spec §6.6.

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, and […] binds a constant (literals, named constants, and the integer expressions an array size allows). A statement fragment is a scope of its own for each 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, which is written wherever a variant's name would be: 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, which are checked like written arms: 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.

const if: selection at specialization time

const if chooses one of its fragment lists while a function is specialized, and generates nothing of the others. It stands where a const for stands: in a statement list, a constructor's initializer list, or a match's arm list, with fragments of that list's kind in each branch. A chain continues with else const if; a plain else if after a const if is an error.

struct Item { name: u8[..8], price: i64, tax: i64 = 20, discount: i64 = 0 }
enum Shape { Circle { r: i64 }, Dot }

fn required<T>() {
    const for <N, A> in fields<T> { const if !has_default<A> { print("required: ", N); } }
}

// A copy of src with every field that declares a default reset to it.
fn reset_defaults<T>(src: T&) -> T {
    T { const for <N, A> in fields<T> { const if !has_default<A> { A: src.A, } }, .. }
}

fn describe<T>(v: T) {
    const if same<T, i64> { print("an integer ", v); }
    else const if same<T, Shape> { print("a shape"); }
    else { print("something else"); }
}

fn main() {
    required<Item>();
    var item = Item { name: "pen", price: 5, tax: 7, discount: 3 };
    print(reset_defaults(item));
    describe(1);
    describe(2.5);
    const for <C> in [1, 2, 4] {
        const if C > 1 { print(C, " is wide"); } else { print(C, " is narrow"); }
    }
}
required: name
required: price
Item { "pen", 5, 20, 0 }
an integer 1
something else
1 is narrow
2 is wide
4 is wide

The condition is a constant of the specialization: true, false or a named bool constant; a query; !, &&, || and parentheses over conditions; or a comparison of two constants of one kind, the constants a const for list accepts (literals, named constants, the integer expressions an array size allows, and enclosing const for bindings). Integers compare at the type their constants unify to, an integer beside a float converts, and booleans and strings take == and != only. A local, a call, a field read or anything else computed at runtime is an error, not a runtime branch.

The queries are valid in a const if condition only. has_default<A> is true exactly when the field an accessor A from fields<T> stands for declares a written = …, a default equal to the type's zero included; nothing is evaluated. same<T1, T2> is true when the two types are identical after substitution, const qualification included, so same<V, Shape.Dot> picks a variant and same<T, i64> a generic instantiation.

The branch not chosen is parsed but never checked, which is the point: a const if over a field can write to it in one branch and leave it alone in the other, where a runtime if on the same predicate would have to typecheck both for every field. In a statement list the chosen branch is a nested block, not an expansion: break, continue and return in it mean what they mean in an if. In a constructor the chosen entries join the initializer list, and in a match the chosen arms join the arm list, so a const for over variants<E> can leave some variants to written arms.

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

Functions that never return

abort(msg) and exit(code) never return, and the checker takes this into account: a branch that ends in one gives no value, statements after one are not checked, and this 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, as is 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:

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 are clean aborts with a message and the line of the check that failed; a check inside a standard library function names the line of the program's code that called the library. 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) calls 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, starting in the namespace of x's type (Namespaces). 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. It is then taken from the first return (or the body's final expression), the way 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 copies nothing (the compiler warns where a second returned local stops the first one from being allocated at the 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. A unique exact match is chosen first, 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 does not count as an argument, so f(a, b = 0) beside f(a) makes every one-argument call ambiguous. Tag dispatch (section 9) is tried only when no ordinary candidate matches. 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 (write 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. The nested functions of one block may call each other in either order, as the functions of a recursive-descent parser do, but the enclosing code can call a nested function 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 fn … = "sym" 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 cannot be combined with extern, export, simd, recursive or 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). At each call, the widest version the CPU has is chosen. 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. The exception is math's mul_add(x, y, z): x * y + z rounded once where the code is built for FMA and twice where it is not. The AVX2 and AVX-512 versions round once, and the baseline rounds as its target does. 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, .= moves the reference itself (where = would write through it); where T is a value, it is a plain assignment. The library's swap_at is written that way, so it swaps the elements of an array of references, including relative ones, as it swaps 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. For example, with fn push_n<A, T>(xs: A&, v: T, n: i64), push_n(flags, 1, n) 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 that returns a bare T and whose T parameters are bare values, when it is given literals and float expressions of literals and integers. A typed argument fixes 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 a function value is direct and inlinable, and a function value 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 directly in 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 is not affected by this rule. The same rule applies to 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 uses only native stack, 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) can own each as a local of a non-recursive driver function and pass the recursion one struct of references to them. Alternatively, it can write 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 copies 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 be handled 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 in between keep their plain signatures: they take no error parameter and need no ? on each call or any other code to propagate the error.

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 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), with a finally written as a defer (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. The path is resolved first relative to the root of the importing file's package (for the program's own files, that is 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 file 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, including a library's own files, is a requirement that the root must satisfy, so a program's dependencies are flat, pinned and in one place. packages.md has the full details, 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, such declarations are written 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 also write its namespace itself, as in 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), " ", p.norm1());   // the format hook and UFCS follow the type
}
(-3,4) 7 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). A UFCS call looks in the namespace of its receiver's struct or enum type first, past the lexical scopes, where that namespace declares the name. So p.norm1() finds geo::norm1 for a geo::Pt from any namespace, even one with a norm1 of its own, and so does a generic body's x.norm1(); a plain call norm1(p) does not. The format hook follows the type too: 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 only name resolution, type identity and generated C names. There is no privacy, no re-export and no nesting of namespaces. Spec §11.1, docs/design/namespaces.md.

Globals and initialization

Globals are initialized before main, in declaration order within a file, with imported files first. So an initializer may use a global from a module the file imports, and may call functions. It is an error to name a global before its initializer has run, whether in an earlier initializer or in a function that such an initializer calls. Converting into an index type of a global array, or a from_bytes verifying one, reads that array's length, so it counts as naming the array. The whole program runs inside an implicit outermost scope that owns the globals: they are the outermost roots for the lifetime rules. A global growable array owns the bottom of a data stack for the program's life, which makes it 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.

src_file and src_line

src_file() is the base name of the source file that contains the call, as a const u8[:]. src_line() is that call's 1-based line. Both read the call node the parser already stamps. A default is parsed in the function that declares it, and a call that leaves the argument out is reported instead, so

fn info(text: u8[:], file: u8[:] = src_file(), row: i64 = src_line()) {}

sees the caller's file and line. An argument the caller writes is kept. A call to src_file() or src_line() in the body of info names info's own file and the line that call is written on. The base name is the last path segment, not the path an abort prints. A local of either name hides the builtin, as a local hides any function. Spec §12, docs/design/call_location.md.


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. Those copies are taken from the spawning program at the moment of the spawn. A worker that increments a global increments its own copy, and main's copy keeps its value. So there are no data races, and the language has no locks, atomics or memory orderings.

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. Whatever crosses, including a worker's arguments, must be flat: it holds no references or slices at any depth. This is cheap because a flat Goose value is contiguous, so a job or a result carries real data, including its variable-size parts, as a single 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 message limit until qlimit gives it one. Once the queue is full, 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 result. 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, and releases everything 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 the message and start another worker:

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. A worker that a contained worker 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 only place a reference could enter without the compiler having proved it, so from_bytes verifies them before they become a value. It checks the framing, every tag, every length, that varints are canonical, and that every link lands on an element start of that same image. A corrupt or hostile file gives false, never a wild reference. What it verifies is safety, not integrity: edited data bytes still describe a well-formed structure, and adding a checksum is up to the program.

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 can be indexed and grown, and references into it are ordinary references. from_bytes also builds T[>..<] and the T[] family, except a T[>..<] whose elements hold relative references: a shrink of the result would free what its own links still name. It 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. A string after =, where the body would go, is the C symbol; without one, the Goose name is used. The older extern "cbrt" fn cube_root(...), with the string after extern, is deprecated and gives a warning. Such a declaration is top-level, fully typed and 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 fn cube_root(x: f64) -> f64 = "cbrt";    // 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:

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 its type 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"];). A program run in-process (goose run) links only against the compiler itself, which supplies the C99 <math.h> functions in both widths (all but nexttoward) and little else of the C library; other C library functions need goose build, the program's own C or an extern "lib" block (command_line.md). Headers supplied by c_headers in that sidecar or by --include header.h can use Goose's generated types directly; see Using Goose types in a C header.

extern "lib" { … }: a shared library, bound by name

An extern block names a shared library and declares functions bound from it when the program starts, with no C to compile or link. The program opens the library and looks the symbols up; a missing library or symbol is a runtime error that says which name was tried. A member is an extern fn written without the extern keyword; its string after =, if any, is its C symbol.

extern "SDL2" {
    fn SDL_Init(flags: u32) -> i32;
    fn SDL_Quit();
    fn SDL_GetTicks64() -> u64;
    fn delay(ms: u32) = "SDL_Delay";
}

"SDL2" opens SDL2.dll, libSDL2.dylib or libSDL2.so, found by the loader's search. "bin/steam_api64" opens that file, with the same naming rule applied to its last component, under the program's resource directory (resource_dir()); this is how a program ships a library beside itself. A dotted name such as "libSDL2-2.0.so.0" is taken as written. A name that differs per platform, or per installation, goes in the sidecar rather than the source:

// myprog.build
dylibs_linux = ["SDL2=libSDL2-2.0.so.0"];     // the soname every distro installs
dylibs_windows = ["SDL2=bin/SDL2.dll"];       // shipped with the program

A package's sidecar maps its own names as defaults, and the program's sidecar overrides them, platform by platform, for the names it mentions.

With no adapter in between, a member declares the library's real prototype in terms of Goose's shapes:

Only a member a call reaches is bound, and a library none of whose members is called is never opened, so a block may declare an interface wider than any one program uses. A program whose only C is such libraries runs under the in-process backend with no C toolchain installed. 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 and 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. The C has no main of its own, and comes with a header declaring goose_init(argc, argv), which the host calls once before anything else. That function 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. goose_exit() ends the program and releases what the start set up, so that goose_init can start it, or another build of it, again in the same process; it is a runtime error while a worker of the program still runs. goose build --shared --header hello.h hello.goose builds the same C straight into a shared library (libhello.so, libhello.dylib or hello.dll) with the header beside the source (command_line.md §3). Spec §7.11.


25. Builtins

These are the language's own functions. Everything else is in 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
src_file(), src_line() the call's file name and line; a default reports the call that omitted it 21

The array members, .len, .cap, .push, .append, .pop, .pop_n, .resize, .clear, .index_of, .range_index, .check_length, .move_from are in section 5. The stdlib pool APIs and deprecated builtin operations .alloc_index, .alloc_ref, .free, .alloc_slice, .realloc_slice, .free_slice are 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 defer match return break continue as true false null self bool varint i8 i16 i32 i64 u8 u16 u32 u64 f32 f64.

Contextual keywords, which are ordinary 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, variants and types (in a const for header), has_default and same (in a const if condition).

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