The Goose Language Reference
This is the reference for the Goose language: every feature, topic by topic, with examples. It sits between the two other descriptions of the language. The tutorial teaches by example and spends its time on what is different about Goose; the specification states the exact rules in the language an implementor needs. This document is for the programmer who has read the tutorial and wants the whole language laid out: exhaustive like the specification, but written to be read, and with a program for every rule.
Each section ends in, or carries, a pointer of the form "spec §3.3"
to the specification section that is the final word on it. Features that
exist nowhere else get the most room. Features that work as they do in C
or Rust, if and while among them, get a
paragraph.
Every example in this document compiles with the current compiler,
and scripts/check_doc_examples.py checks that they still
do. A complete program is shown with its output after it; a shorter
example is the body of a main, or a set of declarations,
and is written so that it can be dropped into one. Run any of them
with
goose example.goosewhich compiles the program into the compiler's own process and runs
it there, with no C compiler and no build step. goose check
typechecks without running, and goose build makes an
executable (packages.md has the rest of the
command line).
1. Lexical structure
A Goose source file is UTF-8 text. A byte-order mark at its start is
ignored, and line endings are read as \n whichever way the
file writes them.
Comments are // to the end of the line
and /* … */, which nests: /* a /* b */ c */ is
one comment.
Identifiers are [A-Za-z_][A-Za-z0-9_]*.
The keywords are listed in Appendix A. A few words
are keywords only in one position and names everywhere else:
from after a return value, upto
after an unsigned type in a type, inline and
checkfree before fn, and fields,
variants and types in a const for
header. in is a keyword throughout.
Integer literals are decimal (42) or
hexadecimal (0xFF). The whole unsigned 64-bit range is
accepted: a value above i64.max is a u64
constant. A character literal 'a' is an integer constant,
the byte's value; it takes the escapes a string does.
Float literals have a ., an exponent,
or both: 1.5, 2.5e-3, 1e9. A
. begins a fraction only when a digit follows it, so
1..2 is a range and 1. is not a number. C99
hexadecimal floats with a binary exponent are accepted too:
0x1.8p3 is 12.0.
String literals are "…" with the
escapes \n \t \r \0 \\ \" \' and \xNN (two
hexadecimal digits). A literal is static read-only data, of type
const u8[:] (section
6).
Raw strings are """…""": no escapes,
and the next """ ends the text. On one line the text is
whatever lies between the delimiters, backslashes and quotes included. A
multi-line raw string starts with its opening """ at the
end of a line and ends with the closing """ on a line of
its own; the closing delimiter's indentation is removed from every line,
so the text can sit at the code's own indentation. The line breaks just
after the opening and just before the closing delimiter are not part of
the text; an empty line before the closing delimiter adds a final
newline.
fn main() {
let usage = """
usage: wc [-l] file
-l count lines only
""";
print(usage);
print("""C:\temp\new "quoted" \n""");
}usage: wc [-l] file
-l count lines only
C:\temp\new "quoted" \n
Other literals: true and
false; null, the empty value of any optional
type (section 10); and
self, which is legal only inside a struct or variant
literal and denotes the value that literal is constructing (section 11).
Statements and semicolons
An expression statement ends in ;. A statement that
is a block-ended construct, meaning if,
match, block, a loop,
guard … else { }, a nested fn, or a call with
a trailing function block, needs none: it ends at its closing
}. An operator never continues such a statement, so to use
one of them as an operand it has to be parenthesized:
let a = 3;
let v = (if a > 2 { 1 } else { 2 }) + 3;
if a > 2 { print("big"); } // no `;` needed
let w = if a > 2 { 10 } else { 20 }; // as an initializer: fine as it is
print(v, " ", w);A stray ; after a block is harmless. Every bracketed,
comma-separated list accepts a trailing comma: struct and array
literals, call arguments, parameter and generic lists, field lists and
match arms.
Evaluation order
Everything evaluates left to right: operands, arguments, and the fields of a literal. A by-value operand is read before the operands after it run; a reference or slice operand keeps its address or view, not a snapshot of the contents. The destination of an assignment is resolved first, and a compound assignment reads its old value before its right-hand side runs. Spec §2.
2. Declarations, variables and scope
let, var and
const
let answer = 42; // a binding that cannot be reassigned
var count: i32 = 0; // a variable: reassignable
const limits = [10, 20, 30]; // neither reassigned nor written into
count += 1;
count++; // a statement, not an expressionThe type follows the name and can usually be left out: an
un-annotated declaration takes the type of its initializer.
let forbids assigning the name as a whole and nothing else:
the contents of a let array or struct are as writable as
their type says, so let xs = [1, 2, 3]; xs[0] = 9; is fine.
Read-only contents come from the type, const T (section 12), and
const x = e; declares a let of type
const T. A var of a const type is
the converse: it can be reassigned but never written into.
let xs = [1, 2, 3];
xs[0] = 9; // fine: let forbids `xs = …`, not element writes
const ys = [1, 2, 3];
// ys[0] = 9; // error: ys is const
var s: const u8[:] = "text"; // a read-only view that can be re-pointed
s = s[1..];
print(xs, " ", ys, " ", s);A let without an initializer gets its value exactly
once, and the compiler checks that it does: no path may read it before
it is assigned, and no path may assign it twice, which includes
assigning it in a loop it is declared outside of, or in a function
called more than once.
fn main() {
let n: i64;
let ready = true;
if ready { n = 1; } else { n = 2; }
print(n);
}1
Every local must be initialized before it is read, on every path.
Fixed arrays must be fully initialized; the fill literal
[v; n] makes a large one cheap. An inner scope may
redeclare a name, which shadows the outer one. Spec §4.4.
Globals
Globals are declared like locals, at the top level of a file, with
any type: scalars, structs, and growable arrays too. They initialize
before main runs, in declaration order within a file and
imported files first, and their initializers may call functions. A
var global given only a type starts out as that type's
default value: var log: u8[>..]; is an empty array. A
let or const global must be given its value.
Section 21 has the rest.
Named constants
A let or const global with no written type
and a constant initializer is a named constant. An
integer one is used exactly as the literal would be: it adapts to any
integer type its value fits, serves as an array size or a fill count,
and names a value in a match arm.
let SIZE = 16;
let HALF = SIZE / 2;
var grid: u8[SIZE] = [0; SIZE];
fn main() {
let b: u8 = SIZE; // adapts, as the literal 16 would
let wide = SIZE * 1000000000000;
grid[HALF] = 1;
print(b, " ", wide, " ", grid.len, " ", match 8 { HALF => "half", _ => "other" });
}16 16000000000000 16 half
A global with a written type
(let SIZE: i64 = 16;), a local let, and any
var are values of their type and adapt to nothing. Spec
§3.1, §11.1.
Type aliases
type Name = T; names a type. An alias is the same type,
not a new one:
type Id = u32;
type Names = u8[..16][>..];
fn main() {
let a: Id = 7;
let b: u32 = a;
var ns: Names = [];
ns.push("goose");
print(a + b, " ", ns);
}14 ["goose"]
A nominal type comes from a struct or enum
declaration (section 8, section 9).
Type syntax at a glance
Type syntax is postfix. Every form below is described in its own section; this table is the map.
| Written | Meaning | Section |
|---|---|---|
i8 … i64, u8 … u64, f32,
f64, bool, varint |
the scalars | 3 |
T[4], T[], T[varint],
T[..8], T[..], T[>..],
T[>..<] |
the array family | 5 |
T[:] |
a slice: a view of a run of Ts |
7 |
Point, Pair<i64> |
a struct, a generic struct instance | 8 |
Shape, Shape..,
Shape.Circle |
an enum in fixed mode, in variable mode, one variant | 9 |
T&, T? |
a reference, an optional (nullable) reference | 10 |
T&<u32>,
T&<u32 in pool>,
T&<u32>? |
relative references | 11 |
const T, const T&,
const T[:] |
read-only contents, a read-only reference or view | 12 |
u32 in a, u32 upto a |
an index of the global array a, a bound of it |
15 |
fn(i64) -> bool |
a function value's shape, as documentation | 18 |
const qualifies the first reference or slice built on
the base type: const u8[:] is a read-only view of bytes. To
reach an outer one, parenthesize: const (u8[:])& is a
read-only reference to a slice variable. An index type takes no postfix
of its own, so an array of them is written
(u32 in a)[>..].
3. Scalars and arithmetic
Integers
The integer types are i8 i16 i32 i64 and
u8 u16 u32 u64, two's complement, usable everywhere:
fields, elements, locals, parameters, results. i64 is the
conventional type for counts, sizes and indices; it is what
.len, integer literals and the builtins produce. The
narrower types are for data that should be compact and for kernels that
want a particular width.
Arithmetic happens at the operands' own width.
u8 + u8 is an 8-bit add, i32 * i32 a 32-bit
multiply. There is no promotion to int: the declared types
decide the operation.
A literal has no type of its own. It adapts to any
integer type whose range holds it, and is an i64 where
nothing constrains it. let x: u8 = 255; is fine and
let x: u8 = 256; is a compile error.
let byte: u8 = 255;
let answer = 42; // an i64
let wrapped: u8 = byte + 1; // an 8-bit add: 0, since unsigned math wraps
let widened = byte + answer; // the u8 widens into the i64: 297
print(wrapped, " ", widened);Mixing widths. A binary operator's operands must reach one common type, which is also the result's type. The rules, in order:
- Equal types stand.
- A literal adapts to the other operand's type (an error if it does not fit).
- Otherwise, if exactly one operand widens implicitly into the other's
type, the wider type wins:
u8 + i64is ani64add. - An integer meeting a float converts to the float's type:
i32 * f32is anf32multiply. - Anything else is a compile error asking for a cast: same-width
signed and unsigned (
u32 + i32), oru64with anything signed. Nothing is silently promoted to 64 bits.
let a: u32 = 7;
let b: i32 = 3;
// let c = a + b; // error: u32 and i32 do not unify; cast one
let c = a as i64 + b; // an i64 add
let d: u8 = 200;
let e = d + 100; // 100 adapts to u8: an 8-bit add, 44
print(c, " ", e);Shifts are the exception: x << k has
x's type whatever the count's type, and the count is masked
to the width. Comparisons unify the same way but produce
bool.
% is Euclidean. The result lies in
[0, |b|) and is never negative, at every integer type,
whatever the dividend's sign. So x % n is always a valid
index into an array of length n, and the compiler drops the
bounds check that a C-style remainder would need a guard for.
print(-7 % 3, " ", 7 % -3, " ", -7 / 3);2 1 -2
Integer / truncates toward zero, so / and
% do not form a matching quotient/remainder pair for
negative dividends. std supplies both missing partners, at
every integer width: rem(a, b) pairs with /,
and div_euclid(a, b) pairs with %.
import std;
fn main() {
print(-7 / 3, " ", rem(-7, 3));
print(div_euclid(-7, 3), " ", -7 % 3);
print(div_euclid(-1, 10), " ", -1 % 10); // tile and offset
}-2 -1
-3 2
-1 9
div_euclid rounds down for a positive divisor and up for
a negative one; its quotient overflows exactly when / does.
rem is zero or has the dividend's sign, and
rem(MIN, -1) is zero at every signed width. Both functions
abort on a zero divisor. Float % uses C fmod,
so -7.0 % 3.0 is -1.0; these std
functions take integers only.
Overflow. Unsigned arithmetic wraps modulo 2^width
in every build, by definition: that is what hashes, random number
generators and bit tricks mean by unsigned math. Signed overflow aborts
in a debug build (--debug) and wraps in a release build.
Division or remainder by zero aborts always, and so does
i64.min / -1. A debug build checks each operation as it
executes after optimization, so it is a bug-finding tool, not a promise
about every intermediate written in the source.
var h: u64 = 14695981039346656037;
for c in "goose" { h = (h ^ c) * 1099511628211; } // FNV-1a, wrapping by design
print(h);A u64 literal above i64.max, as above, is
accepted; negating one is an error except for
-9223372036854775808, which is exactly
i64.min. Spec §6.2.
Floats
f32 and f64 are IEEE floats.
f32 arithmetic stays 32-bit; an f32 widens to
f64 implicitly, never the reverse. Every integer converts
to either float type implicitly, so 2 works wherever
2.0 would.
A float literal adapts to whichever float type its destination or
other operand has, and is an f64 where nothing gives it a
type. So does an expression whose float-ness comes only from literals
and integers: n * 0.5 is computed in 32 bits when it is
passed to an f32 parameter and is an f64 in
let h = n * 0.5;. A let with no written type
whose initializer is such an expression adapts the same way at each use,
local or global, which is how PI in the standard library
serves both widths.
let PI = 3.141592653589793;
fn area32(r: f32) -> f32 { PI * r * r } // PI adapts to f32 here
fn main() {
let n = 3;
let h = n * 0.5; // an f64
let r: f32 = 2; // an integer becomes a float implicitly
print(h, " ", area32(r), " ", PI);
}1.5 12.566371 3.141592653589793
A float prints in the shortest form that reads back as the same value
of its own type, with .0 after a whole number:
1.0, 0.1, -0.0; inf,
-inf and nan. On floats % is C's
fmod. Spec §3.1, §6.3.
bool
bool holds true or false in
one byte. Comparisons produce it; if, while
and guard require it: there is no integer-to-bool coercion,
so if n { … } with an integer n is an error
(if n != 0). && and ||
short-circuit.
varint
varint is a variable-length integer storage
type for fields and array elements, one to ten bytes long depending on
the value. Reading one gives an i64; it can only be written
when its containing value is constructed. Section
8 covers it with the other field types.
Conversions
One principle: within a kind, a conversion the compiler can prove value-preserving is implicit; anything that could lose a bit or flip a sign takes a cast. Between the kinds, an integer becomes a float implicitly and a float becomes an integer only through a cast.
| Conversion | How |
|---|---|
to a wider integer of the same signedness (i8 → i32,
u16 → u64) |
implicit |
unsigned to a strictly wider signed type (u8 → i16,
u32 → i64) |
implicit |
f32 → f64 |
implicit |
any integer, u64 included, to f32 or
f64 |
implicit, rounding to the nearest float |
| a literal or named constant into any type its value fits | implicit |
any integer but u64 into a varint
store |
implicit |
narrowing, same-width signedness change, signed to unsigned,
u64 to signed, f64 → f32, float to
integer |
x as T or x as! T |
x as T converts between any two numeric types. It is
range-checked in a debug build, which aborts if the value changes, and
truncates, wraps or rounds toward zero silently in a release build.
x as! T never checks: it is for when losing bits is the
intent, in a debug build too. Conversions to a float are never checked,
in either form. Float to integer is defined exactly: truncate toward
zero, then wrap into the target's width; NaN gives 0.
let big = 300;
let n: u8 = big as! u8; // 44, by design
let f = 2.75;
let i = f as i64; // 2
let back = i as f64 / 4; // 0.5
let neg = -1;
let u = neg as! u32; // 4294967295
print(n, " ", i, " ", back, " ", u);A cast that changes nothing is a warning, so the
compiler points out the ones the rules above make unnecessary:
b as i64 of a u8 stored in an i64
says
redundant `as i64`: a u8 converts to i64 implicitly here. A
cast that decides something stays silent: a type nothing else gives
(let k = 5 as u32;), an operation's width
((a as i64) * b with i32 operands), or a
float's rounding.
Comparing u64 with signed values.
u64 is the one unsigned type with no signed type to widen
into. A comparison between a u64 and a signed operand is
allowed when the signed operand is syntactically known to be
non-negative: a non-negative literal, a .len or
.cap, or a let bound to one of those (and not
also bound to a writable reference). The comparison is then a single
unsigned one. Any other case needs a cast.
let h: u64 = 18446744073709551615;
var xs: i64[>..] = [1, 2, 3];
let n = xs.len;
print(h > 5, " ", h > xs.len, " ", h > n); // all allowed: 5, .len and a let of itSpec §6.1, §6.3.
Arithmetic on structs and fixed arrays
The arithmetic operators apply memberwise to two values of the same
struct or fixed-array type whose scalar leaves are all integers or all
floats, and so does unary - where no leaf is unsigned.
* and / also take one scalar operand, in
either order, when every leaf has exactly the same type; the scalar
converts to that type. This is how the library's vector types work, with
no operator overloading in the language.
struct V3 { x: f32, y: f32, z: f32 }
fn main() {
let a = V3 { 1, 2, 3 };
let b = V3 { 0.5, 0.5, 0.5 };
var c = a + b;
c *= 2;
print(c, " ", -a, " ", a / 2.0, " ", 2.0 / a);
let sum = [1, 2, 3] + [10, 20, 30];
print(sum);
}V3 { 3.0, 5.0, 7.0 } V3 { -1.0, -2.0, -3.0 } V3 { 0.5, 1.0, 1.5 } V3 { 2.0, 1.0, 0.6666667 }
[11, 22, 33]
+, - and % need two matching
aggregates. Both operands are evaluated once, before any member is
written. Spec §6.1.
Bitwise operators
~ & | ^ << >> work on integers and keep
the operand's type. >> shifts in zeroes on unsigned
types and copies the sign bit on signed ones. The bitwise operators bind
tighter than comparisons, so x & mask != 0 is
(x & mask) != 0, and shifts bind tighter than
&. ~c of a non-negative constant adapts
like a constant, so let a: u8 = ~4; is 251 and
flags & ~F_ROAD clears a bit of a u8. A
shift of a constant by a variable count keeps the constant's freedom:
1 << k takes the type its destination has.
var flags: u8 = 0;
let F_ROAD: u8 = 4;
flags |= F_ROAD;
flags |= 1 << 1; // the constant 2 adapts to u8
flags &= ~F_ROAD;
let k = 40;
let m: u64 = 1 << k; // a u64 shift, since m is one
print(flags, " ", m, " ", flags & 2 != 0);There are no binary literals; write 0x hexadecimal or a
shift. The complete precedence table is in Appendix A.
4. Where values live
This section is short, but everything after it rests on it.
Size classes
Every type is in one of three size classes:
- fixed: the byte size is a compile-time constant.
Scalars, structs of fixed fields, fixed arrays
T[k], static-capacity arraysT[..k], references and slices. - variable: the size is chosen when the value is
constructed and never changes afterwards.
T[],T[varint],T[..], a struct with avarintor variable field, an enum in variable mode. - resizable: the size may change after construction.
T[>..](grow-only) andT[>..<](grow-shrink), and a struct whose last field is one of them.
A compound type's class is the largest of its parts' classes. A type is flat when it holds no reference, slice or relative reference at any depth; only flat values may cross between threads (section 22). Spec §1.1.
Data stacks
A Goose program has the native call stack, some static data (string
literals and compile-time constants), and N data
stacks, where N is a number the compiler computes for the
program. A data stack is a large reservation of address space with a
bump pointer. That is the whole memory system: no malloc,
no free list, no garbage collector, no reference counts, no
destructors.
Fixed-size values live on the native stack or in registers. Every variable or resizable value lives on a data stack, owned by the variable that declares it (a local, a global, a by-value parameter, or a field or element of one of those), and the rules that make this work are:
- Values are pushed in lifetime order and freed all at once when the owning scope ends. The free is one store: the stack's top goes back to where it was.
- At most one resizable value is live per stack, and it is the topmost thing on that stack for its whole life. So growth is a pointer bump with no capacity check, and nothing ever moves.
Three growable arrays alive at once get three stacks; a stack is reused the moment a scope frees it. The compiler proves all of this statically, and there is no syntax for a stack anywhere in the language. Spec §1.2, §1.3, §10.3.
fn main() {
for round in 3 {
var scratch: u8[>..] = []; // a fresh buffer, each round
for i in 100000 { scratch.push((i % 256) as! u8); }
print("round ", round, ": ", scratch.len, " bytes");
} // freed here: one store
}round 0: 100000 bytes
round 1: 100000 bytes
round 2: 100000 bytes
Nothing moves
Because a growable array's elements never move, a reference into it stays valid however much is pushed afterwards:
struct Item { id: i32, weight: f32 }
fn main() {
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 }); // a reference to element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5; // still valid
print(items.len, " items, first is ", items[0]);
}1000000 items, first is Item { 1, 99.5 }
A std::vector or a Vec reallocates and
cannot promise this. It is why Goose code holds typed references where
other languages hold integer indices into an arena.
Copies are visible
Fixed-size values copy silently on assignment, as in C, whatever
their size. Non-fixed values are never copied
implicitly: that copy is O(size), and the source should show
it. A variable, field or element of variable or resizable class reaching
a destination binds by reference when the destination is a
reference, a slice, an untyped parameter, an un-annotated
let/var, or a for binding, and is
an error at a destination that wants a value of its own. A value-typed
destination takes a fresh value (a literal, a call's result) or an
explicit copy(x).
fn main() {
var a: i64[>..] = [1, 2, 3];
var b = copy(a); // an explicit, visible O(n) copy
b.push(4);
let c = a; // no copy: c names the same array as a
c.push(5);
var p = Item { 1, 0.5 };
var q = p; // fixed-size: a silent copy
q.id = 2;
print(a, " ", b, " ", p.id, " ", q.id);
}
struct Item { id: i32, weight: f32 }[1, 2, 3, 5] [1, 2, 3, 4] 1 2
The same rule governs calls: f(xs) hands a growable
array to a slice parameter, to a T[>..]& parameter
and to an untyped parameter alike, by reference. Spec §4.1.
Built in place
A constructed value that is not fixed-size is built directly
in its final home, through any depth of calls and through the
branch an if or match takes: a function
returning a growable array by value writes its elements straight into
the caller's variable, or into the element being pushed, or into the
field of a record being built inside another array. There is never a
temporary that is then copied into place. So returning a built-up value
costs what filling an out-parameter would, and
words.push(str("word", i)) formats the text straight into
the new element. Spec §4.3, §7.3.
5. The array family
There is no single array type. There is a family whose members differ
in one respect only: what happens to the size. In all of them the
representation is [metadata][elements…], inline and packed,
never a pointer to a separate block.
| Type | Class | What it is | Stored metadata |
|---|---|---|---|
T[k] |
fixed | k elements, k a compile-time constant |
none |
T[] |
variable | sized at construction, frozen after | a u32 length |
T[u8], T[u16], T[u32],
T[u64] |
variable | the same, with a length of that width | a length of that type |
T[varint] |
variable | the same, with a variable-length length | 1 byte for short arrays |
T[..k] |
fixed | capacity k inline; grows and shrinks within it |
a length, in the smallest type holding k |
T[..] |
variable | capacity chosen at construction; grows and shrinks within it | a u32 capacity and length |
T[>..] |
resizable | grow-only: the workhorse | a length |
T[>..<] |
resizable | grow-shrink: stacks, queues, heaps | a length |
fn main() {
var fixed: i32[4] = [1, 2, 3, 4];
let zeros: f64[8] = [0.0; 8]; // the fill form
let frozen: i64[] = [7, 8, 9]; // sized once, never changes
let packed: u8[varint] = "hi"; // a 1-byte length before the bytes
var small: u8[..8] = []; // capacity 8, inline wherever it sits
small.push('h'); small.push('i');
var sized: i64[..] = [..16]; // capacity 16 chosen now, length 0
sized.push(1);
var grow: i64[>..] = []; // the default growable array
for i in 5 { grow.push(i * i); }
var stack: i64[>..<] = []; // the one that pops
stack.push(1); stack.push(2); stack.push(3);
let top = stack.pop();
print(fixed, " ", zeros.len, " ", frozen, " ", packed, " ", small, " ",
small.len, "/", small.cap, " ", sized.cap, " ", grow, " ", stack, " ", top);
}[1, 2, 3, 4] 8 [7, 8, 9] hi hi 2/8 16 [0, 1, 4, 9, 16] [1, 2] 3
Which one to use
T[>..]is the default: arenas, pools, string builders, tree storage, scratch that is refilled between phases. "Grow-only" names the guarantee that references into it stay valid, not its operation set; it can stillpopandclearat points where the compiler sees that nothing points into it (section 13).T[>..<]for a stack, a queue or a heap. References into it may live in variables only, never in storage, so the compiler can check them at each shrink.T[..k]andT[..]when the array must sit inline inside something else: a 16-byte name inside a struct, a small list inside an array element.T[..k]is fixed-size, so a struct holding one still is.T[]andT[varint]for finished data: a string in a record, a node in a pool, anything built once and then read.T[k]for a fixed shape: a 4x4 matrix, a 256-entry table.
Only T[>..] and T[>..<] are
resizable, and only those are subject to the one-per-stack rule.
Everything else can go anywhere a value of its class can.
Compile-time sizes
The k of T[k] and T[..k], and
the count of a fill literal [v; n], is a constant
expression: literals, named constants, arithmetic on them, and the
.len of a global whose length its declaration fixes
(const names = ["a", "bc"]; var counts: i64[names.len];). A
type parameter is a type, never a size:
fn f<N>(a: i64[N]) is an error, since there are no
size parameters. A local that hides a global's name is not a constant
either. Spec §3.3.
Element restrictions
- Elements of
T[>..<],T[..k]andT[..]must be fixed-size: a shrink or a reuse of storage must never change what type a location holds. - Elements of
T[]andT[>..]may be fixed or variable, never resizable. An array of strings,u8[][>..], is one block: a length, then each string's own length and bytes, back to back. - An array with variable-size elements is sequential:
where an element starts depends on every element before it, so it can be
iterated but not indexed, and has no
pop. To reach its elements in any order, keep an index of references beside it (section 11).
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
- Fixed values go anywhere: locals, globals, any field, elements, payloads, parameters, results.
- Variable values go in locals and globals, parameters and results, payloads, array elements (which makes the array sequential), and struct fields at any position. A field after a variable-size one is reached by computing its offset, which is fine while iterating and costly for random access.
- Resizable values go in locals and globals, by-value parameters, results, and the last field of a struct, which makes the struct resizable too. Never an array element, and at most one per struct.
- Zero-size values (
T[0],T[..0], a variant with no fields) exist only as values of their own, never as a field or an element.
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
[1, 2, 3], and[]where the element type is known from the destination: a typed variable, parameter, field, element or result.[v; n]:ncopies ofv, withna compile-time constant;vis evaluated once.[..cap]: an emptyT[..]with the given run-time capacity.- A literal with no destination type is a
T[k](or aT[]when its elements are not fixed-size), and its elements decideT:[1, 2, 3]on its own is ani64[3]. A literal of constants adapts to a parameter of the shape it fits:take([0, 0, 0, 7])passes au8[4]tofn take(c: u8[4]). var out = [];with nothing else makesouta grow-onlyT[>..]whoseTis fixed by the firstpush,append,formator whole assignment into it, which must come before any other use.
fn take(c: u8[4]) -> i64 { c[3] }
fn main() {
var out = []; // a T[>..]; T is decided by the first push
out.push(2.5); // an f64[>..]
let tbl: i64[4] = [7; 4];
let names: u8[..8][3] = ["a", "bb", "ccc"];
print(out, " ", tbl, " ", names, " ", take([0, 0, 0, 7]), " ", [1, 2, 3].len);
}[2.5] [7, 7, 7, 7] ["a", "bb", "ccc"] 7 3
A struct literal and a variant literal are described in section 8 and section 9. Spec §4.2.
Members
These are the built-in operations. Per UFCS (section 17) a.push(v) is
push(a, v), so the standard library's functions on arrays
read the same way.
| Member | On | Meaning |
|---|---|---|
.len -> i64 |
every array kind and slices | the element count |
.cap -> i64 |
T[..k], T[..] |
the capacity |
.push(v) -> T& |
limited, grow-only, grow-shrink | one element, constructed in place; returns a reference to it |
.append(src) |
limited, grow-only, grow-shrink | the elements of an array, slice or array literal, copied in |
.pop() -> T |
limited, grow-shrink; grow-only where a shrink is allowed | removes and returns the last element |
.pop_n(n) |
the same | drops the last n elements |
.resize(n, v), .resize(n) |
the same | grows with copies of v, or shrinks |
.clear() |
the same | empties the array |
.index_of(r) -> i64 |
arrays of fixed-size elements | the index of the element r refers to |
Growth always supplies element values: there is no operation that
exposes an uninitialized slot. Exceeding a limited array's capacity
aborts; so does shrinking below empty. index_of needs a
reference the compiler knows points into that very array (section 12). The
reusable pool operations are in section 14. Spec §3.3, §12.
fn main() {
var xs: i64[>..<] = [];
xs.push(1);
xs.append([2, 3, 4]);
let last .= xs.push(5);
print(xs.index_of(last), " ", xs.pop(), " ", xs);
xs.resize(6, 0);
xs.pop_n(2);
print(xs, " ", xs.len);
xs.clear();
print(xs.len);
}4 5 [1, 2, 3, 4]
[1, 2, 3, 4] 4
0
Equality
== and != compare arrays structurally:
length first, then elements. Two operands of different array
kinds compare too, as slices, whenever their slices would have one type:
a u8[>..] against a string literal, a
u8[..32] field against a u8[:]. Spec §4.5.
6. Strings and text
There is no string type. A string is any array-family type with
u8 elements, and which one you pick says what you want:
u8[>..]: a builder. Appending is a pointer bump.u8[]andu8[varint]: a finished string, stored inline in whatever holds it.u8[..16]: a small string inside a struct, with no indirection.u8[:]: a view of bytes that live elsewhere. A string literal is one of these, specifically aconst u8[:], since literals are read-only static data.
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:
print(a, b, …)writes the text and a newline to standard output, as a single write, so lines from different threads never interleave.format(out, a, b, …)appends the text toout, any growableu8array.str(a, b, …)builds a freshu8[>..]of the text, constructed directly at its destination:words.push(str("item", i))writes straight into the new element.
Every scalar has a text form: integers in decimal, floats in the
shortest form that reads back to the same value, true and
false, and a u8 array as its bytes. Every
other value renders structurally: an array as [1, 2, 3], a
struct or variant as its positional literal with the type name
(Point { 1, 2 }, Circle { 1.5 }), a reference
as what it points at, and a null optional as null. A
character literal is an integer, so str('x') is
"120"; a single byte is appended with push. A
u8 array nested inside an aggregate is quoted and
escaped.
A format overload
fn format(out: u8[>..]&, v: T), taking
T by value or by reference, renders a T
through itself wherever one occurs in an argument of any of the three
builtins, so a type chooses its text once. It is looked up in the
namespace T is declared in, then globally: the type
decides, not the code that prints.
struct Person { name: u8[..16], age: i32 }
fn format(out: u8[>..]&, p: Person) { format(out, p.name, " (", p.age, ")"); }
fn main() {
var people: Person[>..] = [];
people.push(Person { name: "ada", age: 36 });
people.push(Person { name: "grace", age: 45 });
print(people, " ", str(people[0]).len);
print(['a', 'b'], " ", "text"[0], " ", str('x'));
}[ada (36), grace (45)] 8
[97, 98] 116 120
Spec §3.7. The standard library's std module adds the
usual operations, all over u8[:]: find,
trim, split, words,
join, parse_int, parse_flt,
format_int, compare, to_lower,
and more (stdlib.md).
split returns an array of slices into its input: a few
bytes per part, with no text copied.
fn main() {
let line = " key = some value ";
let body = trim(line);
let eq = find(body, "=");
print("'", trim(body[..eq]), "' -> '", trim(body[eq + 1..]), "'");
let parts = split("alice,30,paris", ',');
print(parts.len, " parts: ", parts, " ", words(" go north "));
}'key' -> 'some value'
3 parts: ["alice", "30", "paris"] ["go", "north"]
7. Slices
T[:] is a reference plus an element count: a view of a
contiguous run of Ts inside some array. It is the universal
"process a range" parameter type, since every array kind becomes one
without copying its elements.
- Slicing:
a[x..y]is elementsxup toy(exclusive); either bound may be left out, and^kmeans "len - k":a[^2..]is the last two elements. Re-slicing a slice is always allowed and never grows it. - Coercion: an array, or a reference to one, reaching
a destination whose type is a slice of its element type becomes a
whole-array slice of that array itself, as
a[..]would. That holds for a parameter, an annotated variable, an assignment, a field, a pushed element, areturn, and a branch of anifwhose other branch is a slice. - Slices of fixed-size elements index and iterate, bounds-checked against the slice's own length. Slices of variable-size elements iterate only.
- A slice of a grow-only array taken before a
pushstays valid; it just does not see the new elements.
fn total(xs: i64[:]) -> i64 { var t = 0; for x in xs { t += x; } return t; }
fn main() {
let fixed: i64[4] = [1, 2, 3, 4];
var grow: i64[>..] = [10, 20, 30, 40, 50];
let view: const i64[:] = fixed; // fixed itself, not a copy
print(total(fixed), " ", total(grow), " ", total(grow[1..4]), " ",
total(grow[^2..]), " ", view[0], " ", grow[..2].len);
let s = grow[1..3];
grow.push(60); // s still views elements 1 and 2
print(s, " ", s == [20, 30]);
}10 150 90 90 1 2
[20, 30] true
A slice is checked. It carries a static root, the
variable that bounds the lifetime of what it views, and it can neither
outlive that variable nor survive a shrink that would free the elements.
The checker tracks liveness, not scope: a slice whose last use is before
a clear is dead, and the clear is fine. Section 12 has the
rules, and section 13
the shrink side.
fn main() {
var line: u8[>..] = [];
line.append("hello world");
let w = line[..5];
print(w);
line.clear(); // fine: w's last use is above
line.append("again");
let v = line[..];
// line.clear(); // error: cannot clear while v is still used
print(v);
}hello
again
A slice is a reference, so it follows the same writability rule: a
slice of a var array is writable through, a string literal
and a slice of a const value are not, and a parameter
declared T[:] takes either and is checked with what it is
given. A function that only reads documents that with
const T[:]. Spec §3.10, §9.5.
Because a slice never copies, it is the natural key for a table. The
library's dictionary keyed by const u8[:] stores views into
text the program already holds:
import dictionary;
fn main() {
let text = "the quick brown fox jumps over the lazy dog the fox";
var counts = dictionary<const u8[:], i32> {};
each_split(text, ' ') { counts.update(it, 0) { it += 1; }; };
let n = counts.get("the");
if n { print(counts.count, " distinct words; 'the' x", n); }
}8 distinct words; 'the' x3
8. Structs
struct Unit { kind: i32, hp: i32 = 100, x: f32, y: f32, name: u8[] }A struct is its fields in declaration order, packed:
no padding is inserted, unaligned access is assumed cheap, and a struct
of an i8[3] and an i32 is 7 bytes. Struct
declarations introduce nominal types; two structs with the same fields
are different types.
Literals
A literal names its fields, or lists them positionally in declaration
order; the two forms do not mix. Fields with a declared default may be
left out: trailing ones in the positional form, any of them in the named
form. An optional field with no default is null when left out. A literal
that ends in .. may leave out any field, which
then takes its type's default value: zero, false, null, an
empty array or string. A field whose type has no default value (a
non-optional reference) must still be given.
struct Unit { kind: i32, hp: i32 = 100, x: f32, y: f32, name: u8[] }
var selected: Unit; // a global with a type only:
// every field its default
fn main() {
let a = Unit { kind: 1, x: 1.5, y: 2.5, name: "scout" }; // hp 100
let b = Unit { 2, 50, 0, 0, "" }; // positional, all given
let c = Unit { kind: 3, .. }; // hp 100, x 0, y 0, name ""
print(a, " ", b.hp, " ", c, " ", selected.hp);
}Unit { 1, 100, 1.5, 2.5, "scout" } 50 Unit { 3, 100, 0.0, 0.0, "" } 100
A global var given only a type starts as its type's
default value. A local must be initialized at its declaration when its
type is not fixed-size, since the value's storage is laid out when it is
constructed; a fixed-size local may be declared first and assigned on
every path before it is read (section 2).
Named initializers may be written out of declaration order only when swapping them back changes nothing the compiler can see: reads and arithmetic that cannot trap are fine, calls with effects, indexing and loops are not. Construction always proceeds in declaration order, straight into each field. If effects must happen in another order, do them before the literal. Spec §4.2.
Field defaults
A default is an expression evaluated at each construction that leaves
the field out, in field order among the explicit initializers. It may
name globals and call functions, and it never sees the constructor's
locals or sibling fields. default<T>() (Builtins) builds a fixed-size T
from its declared defaults, so an invariant a declaration encodes as a
default survives generic code that fills in values.
var serial = 0;
fn next() -> i64 { serial += 1; serial }
struct Rec { id: i64 = next(), tag: u8[..4] = "none", score: f64 = 1.0 }
fn main() {
let a = Rec { .. };
let b = Rec { score: 2.0 };
let c = default<Rec>();
print(a, " ", b, " ", c.id);
}Rec { 1, "none", 1.0 } Rec { 2, "none", 2.0 } 3
A default may not lead back to a construction that takes it again, which would never end. Spec §3.2.
Field qualifiers and padding
let f: Tmarks a field that is not assigned after construction.const f: Tislet f: const T: its contents are read-only too.pad ninsertsnunused bytes; a barepadaligns the next field to its scalar width, for foreign layouts. Pad bytes are never read and==ignores them.
struct Hdr { magic: u8[4], pad, size: u32, let kind: u8, const name: u8[..8] }
fn main() {
var h = Hdr { magic: "GOOS", size: 4, kind: 1, name: "x" };
h.size = 8;
// h.kind = 2; // error: kind is a let field
// h.name.push('y'); // error: name is const
print(h, " ", h.name.cap);
}Hdr { "GOOS", 8, 1, "x" } 8
Spec §3.2, §4.4.
Variable-size fields and
varint
A struct may hold variable-size parts, and they sit inline, in
declaration order. varint is a LEB128 integer field that
takes as many bytes as its value needs (one byte up to 63, two up to
8191, and so on; small negatives are as short as small positives). A
struct with a varint or a u8[] field is
variable-class, and its fields after such a field are found by walking.
A varint field is written only at construction; reads give
an i64.
struct Item { sku: u8[varint], qty: varint, cents: varint }
struct Order { id: varint, customer: u8[varint], items: Item[varint] }
fn main() {
var book: Order[>..] = [];
book.push(Order { id: 1001, customer: "alice",
items: [Item { sku: "SKU-441", qty: 2, cents: 1999 },
Item { sku: "SKU-7", qty: 1, cents: 500 }] });
for o in book {
var t = 0;
for it in o.items { t += it.qty * it.cents; }
print(o.customer, " owes ", t, " cents over ", o.items.len, " items, ",
bytes_of(book).len, " bytes in all");
}
}alice owes 4498 cents over 2 items, 29 bytes in all
That order is 29 bytes with no pointer in it. The price is that
book is sequential: iterate it, or keep an index beside it.
A variable-class field or element is frozen once built: to change one,
rebuild its container. Spec §3.4, §3.6.
A resizable tail
A struct's last field may be a resizable array, which makes the struct resizable: it lives where a growable array can (a local, a global, a by-value parameter, a result), never as an element or a middle field. The tail grows through the struct like any growable array.
struct Stack { name: u8[..8], items: i64[>..<] }
fn push_twice(s: Stack&, v: i64) { s.items.push(v); s.items.push(v); }
fn main() {
var s = Stack { name: "main", items: [] };
push_twice(s, 7);
s.items.pop();
print(s.name, " ", s.items, " ", s.items.len);
}main [7] 1
Spec §3.4, Appendix C.2.
Generic structs
struct Pair<T> { a: T, b: T }
struct Node<T> { v: T, next: Node<T>? }
fn main() {
let p = Pair<i64> { 1, 2 };
let q = Pair { a: 0.5, b: 1.5 }; // T inferred from the fields
var n = Node<u8> { v: 1, next: null };
print(p, " ", q, " ", n.next == null);
}Pair<i64> { 1, 2 } Pair<f64> { 0.5, 1.5 } true
A generic struct is monomorphized like a function, and its
instantiations must be finite: a field naming the struct again with a
larger type argument each round
(struct P<T> { v: T, next: P<T[]>? }) is
rejected. Spec §3.2.
Equality and arithmetic
== and != compare structs memberwise, pads
excluded; a reference or slice field compares by identity
(address and length), since recursing through it could loop. The
arithmetic operators apply memberwise (section 3). Ordering
operators exist on numbers only. Spec §4.5.
9. Enums (algebraic data types)
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }An enum is a tag followed by the payload of one of its variants. It
is the only dynamic polymorphism in the language: no inheritance, no
traits, no vtables. A variant's payload is declared like a struct's
fields, with the same defaults and .. rules, and a variant
with no payload is a plain constant. An enum whose variants all have no
payload is a C-like enum, one byte. A variant literal is
Shape.Circle { r: 1.0 }, and Shape.Dot for a
payload-less one.
Two modes
Every enum type can be stored two ways, chosen at the point of use by the type written there:
Fixed mode (Shape) is what a Rust
enum or a C++ variant is: a tag plus room for
the largest payload, 17 bytes for every Shape,
Dot included. It is fixed-size, so an array of them is
indexable, and a value may be overwritten in place with a different
variant. In exchange, nothing may point into its payload:
match binds the payload by value only. It is usable only
when every payload is fixed-size.
Variable mode (Shape.., with the
trailing dots) gives each value exactly its variant's size: 1 byte for a
Dot, 9 for a Circle, 17 for a
Rect. An array of them is sequential, and a value can never
change variant, since its neighbours are packed right behind it. In
exchange you may take references into a payload and edit it in place. A
variant with a variable-size payload (a string, say) can only ever be
used in this mode.
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
fn main() {
var shapes: Shape[>..] = []; // fixed mode: indexable
shapes.push(Shape.Circle { r: 1.0 });
shapes.push(Shape.Dot);
shapes[1] = Shape.Rect { w: 1.0, h: 1.0 }; // a different variant, in place
print(shapes);
var packed: Shape..[>..] = []; // variable mode: packed
packed.push(Shape.Circle { r: 1.0 });
packed.push(Shape.Rect { w: 2.0, h: 3.0 });
packed.push(Shape.Dot);
for s in packed {
match s {
Rect &r => { r.w += 1.0; }, // edits the payload inside the array
_ => {},
}
}
print(packed, " ", bytes_of(packed).len, " bytes");
}[Circle { 1.0 }, Rect { 1.0, 1.0 }]
[Circle { 1.0 }, Rect { 3.0, 3.0 }, Shape.Dot] 27 bytes
Allowing either variant replacement or interior references, but never both, is what keeps the type system sound. Spec §3.5.
match
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
fn name(s: Shape) -> u8[:] {
match s {
Circle c => if c.r > 1.0 { "big circle" } else { "circle" },
Rect r => if r.w == r.h { "square" } else { "rectangle" },
Dot => "dot",
}
}
fn round(s: Shape) -> bool {
match s { Circle, Dot => true, Rect => false }
}
fn main() {
print(name(Shape.Circle { r: 2.0 }), " ", name(Shape.Rect { w: 1.0, h: 1.0 }), " ",
round(Shape.Dot));
}big circle square true
- A match over an enum is exhaustive: every variant has an arm, or
_covers the rest. No variant is named twice. Circle c =>binds the payload by value, a copy, which cannot be written.Circle &c =>binds it by reference, which is legal only for a variable-mode scrutinee, and through which the variant can never be replaced.- An arm may list several variants,
Circle, Dot =>, and then binds nothing, since their payloads have different types. matchis an expression; its arms must agree on a type, with the usual adaptation of literals (section 16). An arm of several statements is a block,Rect &r => { … },.
Over integers, the arms are literals, named
constants, half-open ranges, or a comma-separated list of those, and
_ is required. The first arm that matches is taken; an arm
that an earlier arm covers entirely is an error. A name is a variant
when the scrutinee is an enum and a constant when it is an integer.
let GROUND = 0;
let SHALLOW = 1;
let DEEP = 4;
let LAVA = 7;
fn terrain(t: i64) -> u8[:] {
match t {
GROUND => "ground",
SHALLOW..DEEP => "water",
LAVA, 10..12, -3 => "hot",
_ => "rock",
}
}
fn main() {
for t in [0, 2, 7, 11, -3, 20] { print(t, " ", terrain(t)); }
}0 ground
2 water
7 hot
11 hot
-3 hot
20 rock
Spec §8.1.
Case functions
The arms of a match may instead be written as separate
functions, one per variant type, and called with the enum value. The
call dispatches on the tag through a jump table, and exhaustiveness is
checked exactly as for a match: every variant needs exactly
one applicable overload, and the result types must agree. This is the
virtual-call idiom, without a vtable, and a new variant is a compile
error until it gets its case.
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
fn area(s: Shape.Circle) -> f64 { 3.14159 * s.r * s.r }
fn area(s: Shape.Rect) -> f64 { s.w * s.h }
fn area(s: Shape.Dot) -> f64 { 0.0 }
fn main() {
var shapes: Shape[>..] = [];
shapes.push(Shape.Circle { r: 1.0 });
shapes.push(Shape.Rect { w: 2.0, h: 3.0 });
shapes.push(Shape.Dot);
var total = 0.0;
for s in shapes { total += area(s); }
let c = Shape.Circle { r: 2.0 }; // a variant type is an ordinary type
print(total, " ", area(c));
}9.14159 12.56636
Variant types (Shape.Circle) are first-class: a value, a
parameter, a field, or a reference Shape.Circle&. An
overload taking Shape.Rect& binds the payload by
reference and needs a variable-mode scrutinee, as a
&-binder does. Dispatch is on one parameter position;
the other arguments pass through, and every argument is evaluated once
before the tag picks the case. Tag dispatch is tried only when no
ordinary overload matches the call. Spec §8.2.
Enums with references
An enum may refer to its own type through a reference, which is what makes linked and tree-shaped data in a pool possible. Combined with variable mode and the narrow links of section 11, a tree of mostly-small nodes stops paying for its largest variant everywhere:
enum Expr {
Num { v: f64 },
Add { l: Expr..&<u16>, r: Expr..&<u16> },
}
fn eval(e: Expr.Num&) -> f64 { e.v }
recursive fn eval(e: Expr.Add&) -> f64 { eval(e.l) + eval(e.r) }
fn main() {
var pool: Expr..[>..] = [];
let a = pool.push(Expr.Num { v: 1.5 });
let b = pool.push(Expr.Num { v: 2.0 });
let sum = pool.push(Expr.Add { l: a, r: b });
print(eval(sum), " in ", bytes_of(pool).len, " bytes");
}3.5 in 23 bytes
Each Num is 9 bytes and the Add is 5: a tag
and two 2-byte offsets. The recursive keyword is explained
in section 19.
10. References and optionals
T& is a reference to a T: one machine
address, with no pointer arithmetic, no casts, and no way to make it
dangle. Spec §3.8.
References are transparent
An expression that denotes a reference behaves as its target in every
value context: reads, arithmetic, comparisons (r1 == r2
compares the targets), passing to a by-value parameter,
print(r). There is no dereference operator and no
->. A write through a reference, r = v or
r += v, writes the target.
Since the reference itself is invisible in ordinary expressions, there is a separate spelling for "the reference, not the target":
let r .= e;andvar r .= e;declareras a reference toe, whatevereis: a variable, a field, an element, or a call that returns a reference.r .= e;rebinds an existing reference variable, field or element to point ate.r1 .== r2andr1 .!= r2compare two references by address.
fn main() {
var xs: i64[>..] = [10, 20, 30];
var r .= xs[0]; // r refers to element 0
var c = xs[0]; // c is a copy of element 0
r = 11; // writes xs[0]
c = 99; // writes the copy
r .= xs[2]; // r now refers to element 2
r += 1;
print(xs, " ", c, " ", r, " ", r .== xs[2], " ", r == xs[2]);
}[11, 20, 31] 99 31 true true
Making and binding references
&x makes a reference to an lvalue: a variable, a
field, an element, or what another reference points at.
& of a temporary is an error. & of a
location that holds a reference yields the stored reference: there are
no references to references.
Where the destination's declared type is a reference, an lvalue of
the pointee type binds without &, and
writing the & is a warning:
fn inc(n: i64&) { n += 1; }
struct Cursor { line: i64& }
fn main() {
var count = 0;
inc(count); // binds count by reference
inc(count);
let r: i64& = count; // an annotated reference variable
r += 10;
let cur = Cursor { line: count };
cur.line += 100;
print(count);
}112
Where a variable's type is inferred, a reference to a
fixed-size value decays to a copy of the target:
let x = r; copies. The exceptions are an explicit
&lvalue initializer, which infers the reference type,
and the .= form. A non-fixed lvalue binds
by reference whatever the form, since non-fixed values never copy
implicitly (section 4):
fn main() {
var n = 1;
var xs: i64[>..] = [1];
let a = &n; // a reference: explicit &
let b .= n; // a reference: the .= form
let c = xs; // a reference: xs is not fixed-size
var r .= n;
let d = r; // a copy of n: r decays
a += 1; b += 1; c.push(2);
print(n, " ", d, " ", xs);
}3 1 [1, 2]
A function whose result type is written T& returns a
reference; one whose result type is inferred returns a reference where
it returns a non-fixed lvalue or an explicit &x, and a
copy where it returns a reference to a fixed-size value. To bind a
reference-returning call to a fixed-size element, use .= or
annotate: let e .= pool.push(v);.
A let reference variable does not rebind; it says
nothing about the target, which is as writable as it was:
let r .= xs[i]; r = 5; writes the element.
Optionals
References are never null. T? is an optional
T: a nullable reference, represented as a
reference whose null is address 0, at no space cost. It composes with
any type (i64? is an optional integer, by reference), and
applied to a reference type it just makes that reference nullable:
T&? is T?. The literal null
is its empty value, and an optional struct field with no declared
default is null when left out.
An optional is not transparent. It must be
narrowed first, by if, while,
guard, assert, or a comparison with
null, and inside the narrowed region it simply is
a T&: transparent, writable through, and bindable with
.=. Rebinding the variable ends the narrowing, except in a
while whose condition is the test, which runs again before
each iteration.
fn first_at_least(xs: i64[:], min: i64) -> i64? {
for &x in xs { if x >= min { return x; } }
return null;
}
struct Node { v: i64, next: Node? }
fn main() {
let xs: i64[4] = [1, 5, 8, 2];
let big = first_at_least(xs, 5);
if big { print("first >= 5 is ", big); } else { print("none"); }
let none = first_at_least(xs, 100);
print(none == null);
var c = Node { v: 3, next: null };
var b = Node { v: 2, next: c };
var a = Node { v: 1, next: b };
var cur: Node? = a;
var sum = 0;
while cur { // narrowed for the body, re-tested each time
sum += cur.v;
cur .= cur.next;
}
print(sum);
}first >= 5 is 5
true
6
An optional rebinds with .= to a reference, which
narrows it, or to null, which does not. A narrowed optional
writes through like any reference; there is no way to write through one
without narrowing it. assert(o) narrows for the
rest of the block. Spec §3.8.
What references may point to
Anything, with two restrictions that the sections on enums and
shrinking explain: nothing may point into the payload of a fixed-mode
enum (section 9), and a reference into a
grow-shrink array T[>..<] may live in a variable only
and must be done with before the array next shrinks (section 13).
A reference into a grow-only array stays valid for as long as it can be
named, and references into limited arrays survive pops and pushes,
reading whatever value the slot holds now.
11. Relative references
A T& is eight bytes, and an address means nothing
outside the process that made it. A relative reference
is the same link stored as a narrow offset:
T&<u8>, T&<u16>,
T&<u32>, T&<u64> or
T&<varint>. Loading one yields an ordinary
T&, storing one re-encodes it, and nothing else about
using it changes. There are two forms, differing in what the offset is
measured from:
- Self-relative, the default,
T&<u32>: the offset from the field itself to the target, which must live in the same enclosing array. The structure is position-independent, so it can be copied, saved and loaded as a whole (section 23). - Pool-relative,
T&<u32 in pool>: the offset from the base of the named global arraypool. A store is a subtraction from a base already in a register, and other arrays can hold links into the pool.
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.
Copying values that hold self-relative links
A self-relative offset is only meaningful where it was stored, so a
value that contains one cannot be copied: not by assignment
from a variable, not as a by-value argument or match binder, not as an
element copy, not by copy(x). Such values are constructed
in place (a literal pushed into the array), bound by reference
(let n .= f(); rather than let n = f();, which
the error suggests), matched with &-binders, and taken
by reference in case functions and format overloads. A
value whose links are all in pool copies like any other,
since its offsets do not depend on where it sits. The one copy that is
always allowed is a default value: a literal's .. gives a
relative field null. Spec §3.9.
A function or trailing block can return a fresh value with relative links. It is constructed at the receiver, including across function calls and with optimization disabled:
struct Node { value: i64, next: Node&<u32>? }
fn linked(first: Node&, value: i64) -> Node {
Node { value: value, next: first }
}
fn add<F>(nodes: Node[>..]&) { nodes.push(F()); }
fn main() {
var nodes: Node[>..] = [Node { value: 1 }];
nodes.push(linked(nodes[0], 2));
let first .= nodes[0];
add(nodes) { Node { value: 3, next: first } };
let next = nodes[2].next;
if next { print(next.value); } // 1
}Each call must place the result in the same root as the linked
element. For example, let n = linked(nodes[0], 2) is an
error: the new local is separate storage. Returning an existing node by
value is still a prohibited copy. A result that needs its receiver's
root also cannot be used as a resize fill or converted through a
temporary array representation.
Pool-relative links and
self
pool in T&<u32 in pool> names a
global var or reusable var of a grow-only type
whose elements hold a T; the pool is part of the type's
identity. The stored value is the offset from the pool's base plus one,
so 0 is null in the optional form and a u32 link spans a
pool of 4 GB. A load is rooted at pool, exactly, wherever
the link was read from, which is what lets a container of links stay
usable where a container of plain references would be ambiguous.
A non-optional relative reference has no null, so a value whose links
should point at itself could not be built: there is nothing yet to point
at. self, written as the whole initializer
of such a field, denotes the value the literal is constructing. That is
how a circular list gets a sentinel, after which no link is ever null
and every insert and remove is the same four stores:
struct Node { val: i64, prev: Node&<u32 in pool>, next: Node&<u32 in pool> }
reusable var pool: Node[>..] = [];
let head = pool.alloc_index(Node { val: 0, prev: self, next: self }); // the sentinel
fn insert_after(p: Node&, v: i64) -> Node& {
let n .= pool.alloc_ref(Node { val: v, prev: p, next: p.next });
n.next.prev .= n;
p.next .= n;
return n;
}
fn remove(n: Node&) {
n.prev.next .= n.next;
n.next.prev .= n.prev;
pool.free(pool.index_of(n));
}
fn is_end(n: Node&) -> bool { n .== pool[head] }
fn main() {
insert_after(pool[head], 1);
let two .= insert_after(pool[head].prev, 2); // .=: `let two = …` would copy the node
insert_after(pool[head].prev, 3);
remove(two);
var line: u8[>..] = [];
var n .= pool[head].next;
while !is_end(n) { format(line, n.val, " "); n .= n.next; }
print(line, "| ", pool.len, " slots, ", 8 + 4 + 4, " bytes a node");
}1 3 | 4 slots, 16 bytes a node
Note the .= on two: a Node is
fixed-size, so let two = insert_after(…) would copy the
node out of the pool, and remove(two) would then be asked
to free the slot of a local copy, which the compiler rejects.
self is legal only in a struct or variant literal, only
for a non-optional relative-reference field whose pointee is the type
being constructed, and for an in pool field only in a
literal built inside that pool (a push, an
alloc_ref, an element store). The reusable
pool and alloc_ref are explained in section 14.
Which form
- Self-relative for position-independent blobs, meaning a compact tree that is saved, mapped or moved whole, and for structures living in a local or parameter pool, which has no name to write in a type.
in poolfor relink-heavy structures in a global pool, where a store is a subtraction from a register; for links into the pool from other arrays, which self-relative cannot express; and where the offset is wanted as an index, since&pool[i]encodes asi * sizeof(T) + 1andindex_ofreads it back.
A parameter that points into a pool is settled per call site, so a
function that relinks is specialized per pool and needs nothing passed
to it. A function's result cannot be declared relative: declare
T& or T?, and a relative slot that
receives it encodes it.
An index for a sequential array
An array of variable-size elements cannot be indexed (section 5), but a second array of 4-byte links into it can be:
enum Mark { Circle { r: f64 }, Label { text: u8[varint] }, Dot }
var marks: Mark..[>..] = [];
var mark_at: (Mark..&<u32 in marks>)[>..] = [];
fn main() {
mark_at.push(marks.push(Mark.Label { text: "hello, goose" }));
mark_at.push(marks.push(Mark.Circle { r: 1.5 }));
mark_at.push(marks.push(Mark.Dot));
for i in [2, 0, 1] {
match mark_at[i] {
Circle c => print(i, ": circle ", c.r),
Label l => print(i, ": label \"", l.text, "\""),
Dot => print(i, ": dot"),
}
}
print(bytes_of(marks).len, " bytes of marks, ", mark_at.len, " links of 4 bytes");
}2: dot
0: label "hello, goose"
1: circle 1.5
24 bytes of marks, 3 links of 4 bytes
push returns a reference to the element it built; stored
into mark_at, that reference becomes the mark's offset from
the base of marks. The array an in offset is
measured from must be a global; a local array is indexed the same way
with plain references, (Mark..&)[>..], at eight
bytes an entry.
12. Lifetimes: roots, stores and writability
This is the whole of Goose's "borrow checker":
A reference or slice must not outlive the variable that owns its target, and must never observe its target at a wrong type.
There are no lifetime annotations anywhere in the language, and no aliasing or exclusivity rules: two references to the same thing are fine, because without shared-memory concurrency aliasing alone cannot break type safety here. Spec §9.1.
Roots
Every reference and slice has one or more static
roots: each a local or global variable, or a temporary,
that bounds the lifetime of its target. A root is exact
when that variable's own storage contains the target, and inexact when
it only bounds the target's lifetime. Roots are inferred, never written:
&x is rooted at x exactly, a slice of a
function's parameter is rooted at whatever the caller passed, and a
reference read out of a container is rooted at the variables whose
storage could hold its target. Functions are specialized per distinct
root, which gives the precision of lifetime parameters with no syntax.
Spec §9.2.
The rules
- Store. A reference may be stored into a location (a variable, a field, an element, a global) only if its root outlives the location's owner. A declaration stores its value into the variable it declares.
- Return. A returned reference's root must be visible to the caller: a caller-supplied argument, a global, or the function's own in-place constructed result. A part of the function's own local dies with it.
- Branches. The value of an
if,match,blockor loop is received after the scopes its branches opened have ended, so it may not be rooted at a variable declared inside a branch. - Merged values. A value that may be any of several,
the branches of an
if, the bindings of avarreference, the returns of a function, has every one of their roots, and must satisfy each rule by all of them. - Rebinding. A reference variable commits to its
first binding's root:
.=may rebind it within the same root or to one at the same scope depth, which is the common case of walking a container. Anything else needs a new variable. - Temporaries. A literal, a call's result, a
copy, or the value of a control construct lasts for the rest of its statement. A view into one may be passed down, but not stored in a variable that outlives the statement, nor returned:let s = f()[..];is an error, andlet t = f(); let s = t[..];is fine. - Holders. A struct with reference fields, an array of slices, a payload with a reference, is rooted at what was stored into it, and the checker records every such store, so a shrink or a scope exit that would strand a reference inside a holder is caught too.
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:
- Read-only: a string literal;
&xorx[..]of aconstvalue, of a field of one, or of a by-valueforormatchbinding; a view into a temporary; abytes_ofview; whatever is read out of a slot declaredconst. - Writable: everything else,
&xandx[..]of avaror a plainletincluded. - Parameters and results are generic over constness.
A parameter declared
u8[:]takes a writable or a read-only argument, and the body is checked with what it is given: a write through a parameter given a literal is an error at that call, reported with the call chain. A parameter declaredconst u8[:]is read-only whatever it is given, which is how a function documents that it only reads. Addingconstis implicit everywhere; dropping it never is. - Slots say what they hold. A field, element, global
or annotated variable holds a read-only reference only if its type is
const:struct Named { name: const u8[:] }can hold a literal,struct Buf { bytes: u8[:] }cannot. An un-annotated variable takes its initializer's constness, sovar t = trim(line);for a read-onlylineis aconst u8[:]cursor. constis shallow: a reference read out of a field of aconst Box&is as writable as the field's own type says.
fn upper_first(s: u8[:]) { if s.len > 0 && s[0] >= 'a' { s[0] -= 32; } }
struct Named { name: const u8[:] }
fn main() {
var buf: u8[>..] = [];
buf.append("goose");
upper_first(buf); // a writable argument: fine
let n = Named { name: "literal" }; // a const slot takes a literal
let who = if buf.len > 3 { n.name } else { buf[..] }; // read-only, since one branch is
print(buf, " ", who);
// upper_first("literal"); // error: writes through a read-only parameter
}Goose literal
Spec §9.5.
Where a reference read out of a container may point
A container names a scope, not the storage its contents point into,
so the root of a reference read out of a container is re-derived: it may
point into any variable in scope whose own storage can hold a value of
its type, a candidate. Where there is exactly one
candidate, the reference is rooted there exactly; where there are
several, it is rooted at all of them, inexactly, and a rule that needs
the exact identity of the array (a relative-link store,
index_of, which array a shrink through it frees) reports
the candidates it could not choose between: "n was read out
of slots and may point into pool or
spare". Two things narrow this: a relative link
in pool always points into pool, exactly, and
a local holder whose stores the checker has all seen is rooted at what
was stored into it. Spec §9.5.
Reading the errors
When the checker rejects something, read the whole message. It names the reference and where it was bound, the holder and the line that stored into it, the instantiation and the call chain that produced it. Use those two locations to decide which end to change: use the view for the last time before the shrink, move the shrink, or move the declaration.
13. Growing, shrinking and assigning whole arrays
Growth is simple: push and append bump the
stack, and a limited array grows within its capacity. Shrinking is where
the array kinds differ, because a shrink frees elements that something
may still point at.
Grow-only arrays shrink where nothing points in
"Grow-only" names a guarantee, not an operation set: a
T[>..] never shrinks while a reference or slice into it
can be live. Memory below the top never moves and is never
reused while the owner lives, so every reference into the array stays
valid for as long as it can be named. A shrink (pop,
pop_n, resize downward, clear, or
assigning the whole array) is legal exactly where the compiler can see
that nothing rooted in the array is still in use:
- no reference or slice variable rooted at the array is used after the shrink (in its own block, an enclosing block, or a later iteration of a loop around it), and
- no holder (a struct with a reference field, an array of slices) still in use had a reference into the array stored into it, and
- the shrink's own statement does not use a reference into the array
that it evaluated earlier (
use(a[0], a.pop())is an error,a[0] + a.pop()reads its left operand first and is not).
The test is liveness, not scope: a slice whose last use is before the shrink is dead, and the shrink is fine. So a scratch buffer refilled per iteration hands out slices of itself freely. The error, when there is one, names the holder and the line where a reference into the array was stored into it.
fn reset(buf: u8[>..]&) { buf.clear(); }
fn main() {
var line: u8[>..] = [];
for i in 3 {
reset(line); // nothing points into line here
format(line, "line ", i + 1);
let head = line[..4];
print(head, "|", line[4..]); // head's last use
}
var holder: u8[:][>..] = [];
holder.push(line[..]);
// line.clear(); // error: holder may hold a reference into line,
// stored at the push above
print(holder.len);
}line| 1
line| 2
line| 3
1
A shrink through a reference parameter, or of a global, cannot see
the caller's variables, so it is checked at every level: each function
records which globals and which parameters' targets it shrinks, and a
call is checked as if it made those shrinks in the caller.
pop, pop_n and resize also need
fixed-size elements, since a sequential array cannot find its last
element. Spec §5.1.
Grow-shrink arrays keep their references in variables
A T[>..<] holds fixed-size elements and shrinks
from anywhere: on a local, through a reference, on a global, as a
struct's tail. Its price: references and slices into it live in
variables only. They may be bound to a local, passed down and
returned, but never stored into a field, an element, a global or a
literal. After a pop, that stack-top memory may be reused by a different
type, so no reference into it may outlive the next shrink, and keeping
such references out of storage is what makes the check a scan of the
variables in scope. A shrink is an error while any such variable is
still used afterwards; the error names the variable and where it was
bound.
fn main() {
var stack: i64[>..<] = [];
for i in 5 { stack.push(i * 10); }
let top .= stack[4];
print(top); // last use of top
stack.pop(); // fine
var popped = 0;
while stack.len > 2 { popped += stack.pop(); }
print(stack, " popped ", popped);
}40
[0, 10] popped 50
A call is balanced for an array when the array is
never shorter during the call than when it began: a function that takes
let mark = xs.len, pushes, and ends with
xs.resize(mark). A balanced call frees nothing a view taken
before it points into, so it is not a shrink where it is made. That is
how a recursion shares one scratch stack (section 19). Spec §5.2.
Limited arrays
T[..k] and T[..] grow and shrink freely
within their capacity, and references into them are allowed and survive
pops: the capacity and the element type are fixed, so a slot once
written stays a valid value of the element type forever. A stale
reference reads whatever value the slot holds now, never corrupt memory.
Exceeding the capacity aborts. Spec §5.3.
Assigning a whole array
An lvalue may be assigned after construction when its size cannot change, or when it can absorb the change:
- fixed-size
varlvalues: a plain overwrite; - a whole resizable array, or a value whose tail is one: clear,
then construct, so the right-hand side is built where the old
elements were and may not use the array at all, not even its length
(
x = f(x)is an error; build the new contents in a variable and assigncopy()of it); - limited arrays, if the new length fits;
- fixed-mode enum lvalues, with any variant;
- not assignable: variable-class lvalues
(
T[]fields, variable-mode enum values,varintfields), which are frozen at construction. Rebuild the container instead.
struct Rec { name: u8[], score: i64 }
fn main() {
var xs: i64[>..] = [1, 2, 3];
xs = [4, 5]; // a shrink, then the new contents
var ys: i64[>..] = [xs[0] + 1];
xs = copy(ys); // computed from other storage: fine
var recs: Rec[>..] = [];
recs.push(Rec { name: "a", score: 1 });
for r in recs { r.score = 2; } // a fixed-size field: assignable
// for r in recs { r.name = "bb"; } // error: a variable-class field is frozen
var one = Rec { name: "a", score: 1 };
// one = Rec { name: "bb", score: 3 }; // error: and so is a variable-class local
recs = [Rec { name: "bb", score: 3 }]; // rebuild the container instead
print(xs, " ", recs, " ", one.score);
}[5] [Rec { "bb", 3 }] 1
Spec §4.4.
Construction in progress
While a value is being built in place inside an array (an element
being pushed, a call's result being appended, the new contents of a
whole assignment), nothing may grow that array: neither the expression
being built nor a function it calls. xs.push(f(xs)) where
f pushes into its argument is rejected. Spec §1.3,
§4.2.
14. Reusable pools
Some data has lifetimes that do not nest: cache entries, nodes of a
graph that is edited, files deleted in any order. For these, a grow-only
array of fixed-size elements can be declared reusable, and
the compiler pairs it with a hidden freelist:
| Operation | Meaning |
|---|---|
a.alloc_index(v) -> i64 |
the index of a slot holding v: a freed slot if there is
one, else a fresh push |
a.alloc_ref(v) -> T& |
the same, as a reference |
a.free(i) |
records slot i for reuse; aborts if i is
out of range |
free releases no memory and ends no lifetime.
Every element remains a valid, live value of its type at all
times, so a reference to a freed and reused slot reads a
different value of the correct type: a logic bug of the same
kind as a stale index, never memory corruption, type confusion or an
out-of-bounds access. That is the whole extent of the unsafety, and it
is what lets tree-mutating workloads run without an allocator.
struct Item { id: i32, weight: f32 }
fn main() {
reusable var pool: Item[>..] = [];
let s0 = pool.alloc_index(Item { 10, 1.0 });
let s1 = pool.alloc_index(Item { 11, 1.0 });
pool.free(s0);
let s2 = pool.alloc_index(Item { 12, 2.0 }); // takes slot s0 back
let node .= pool.alloc_ref(Item { 13, 3.0 });
pool.free(pool.index_of(node)); // a reference turned back into its slot
print("slots ", s0, " ", s1, " ", s2, ", pool length ", pool.len);
}slots 0 1 0, pool length 3
A pool may be a local or a global. It never shrinks (its freelist
keeps every slot live), and push and append
still add at the end without consulting the freelist. A
T[>..]& parameter bound to a pool is a pool
reference: the pool's operations work through it, since it carries
the freelist beside the address, and a variable that was first bound to
a pool can only be rebound to a pool of the same kind. Spec §5.4.
Slice pools
reusable[] hands out runs of elements instead
of single slots, with a freelist of spans that merges neighbours:
| Operation | Meaning |
|---|---|
a.alloc_slice(n) -> T[:] |
n elements from the first free span that holds them,
else from the end of the array, which grows; every element added is a
default value |
a.free_slice(s) |
records the run s for reuse |
a.realloc_slice(s, n) -> T[:] |
s resized to n elements, in place where it
can be, else freed and copied to where alloc_slice(n) puts
it |
This is what the child lists of an n-ary tree want: each node's entries one contiguous run that can be walked as a plain slice.
fn main() {
reusable[] var pool: i64[>..] = [];
let a = pool.alloc_slice(3);
a[0] = 1; a[1] = 2; a[2] = 3;
let b = pool.alloc_slice(2);
b[0] = 7; b[1] = 8;
pool.free_slice(a);
let c = pool.alloc_slice(2); // from the freed run, reset to defaults
let d = pool.realloc_slice(b, 4); // b ends the array: grows in place
print(c, " ", d, " ", pool.len, " ", pool);
}[0, 0] [7, 8, 0, 0] 7 [0, 0, 3, 7, 8, 0, 0]
The slice handed to free_slice or
realloc_slice must be one of the pool's runs; where the
checker cannot see that, the call checks at run time and aborts on a
slice that is not. A grown slice may be moved, so
realloc_slice is unavailable for an element type holding
self-relative links. The single-slot operations do not exist on a slice
pool, nor these on a plain reusable one. Spec §5.4.
15. Index types and checkfree functions
Every index is bounds-checked, and the compiler removes the checks it
can prove unnecessary (section 16). The one
check no analysis removes is an index read out of another
array, dist[adj[k]], since that is a fact about the
contents of one array against the length of another. Index types move
that check to where the index enters the program.
An index type uN in a (u8,
u16, u32 or u64) is an unsigned
integer that is below a.len wherever it can be observed,
and a bound type uN upto a is one that is
at most a.len: the end of a range. a names a
global var or reusable var of a grow-only type
with fixed-size elements, and the type is part of the identity:
u32 in nodes is neither u32 nor
u32 in edges. The value is the plain integer.
struct Node { lo: u32 upto edges, hi: u32 upto edges, weight: i32 }
var nodes: Node[>..] = [];
type NodeId = u32 in nodes; // an alias reads better at every use
var edges: NodeId[>..] = [];- Making one.
x as (uN in a)aborts unless0 <= x < a.len, with the errora[x]would give, in every build: it is a bounds check, moved, and the bounds-check pass elides it where it would elidea[x].as!cannot make one, and a literal never adapts to one. - Using one. Indexing
awith an index ofaneeds no check. A value converts implicitly to its plain integer type, so arithmetic on it gives plain integers; compound assignment on a place of an index type is an error, and so is memberwise arithmetic on a struct or fixed array holding one. - Loops.
for i in lo..hiwhose bounds are index or bound types of one array givesithat array's index type; so doesfor i in nwithnof one. - Passing through. Declarations, parameters, results,
fields, payloads and elements carry the type, and a branch whose arms
all have one array's index types keeps it (at the widest width). A
reference to such storage is
(u32 in a)&, never au32&. - Never shrunk. An array an index type names cannot
be shrunk anywhere, so a value once in range stays in range. It has no
default value.
from_byteschecks every index in an image against its array.
An index type takes no postfix: an array of them is
(u32 in nodes)[>..] or, with the alias,
NodeId[>..], and a for binder's written one
is parenthesized: for v: (u32 in nodes) in ….
struct Node { lo: u32 upto edges, hi: u32 upto edges, weight: i32 }
var nodes: Node[>..] = [];
type NodeId = u32 in nodes;
var edges: NodeId[>..] = [];
fn add_node(w: i32) -> NodeId {
let none = 0 as (u32 upto edges);
nodes.push(Node { lo: none, hi: none, weight: w });
return (nodes.len - 1) as NodeId;
}
fn main() {
for w in [5, 3, 8, 1] { add_node(w as i32); }
for v in nodes.len { // v links to v + 1 and v + 2
nodes[v].lo = edges.len as (u32 upto edges);
for d in 1..3 { if v + d < nodes.len { edges.push((v + d) as NodeId); } }
nodes[v].hi = edges.len as (u32 upto edges);
}
var total = 0;
for v in nodes.len {
let n = nodes[v];
for k in n.lo..n.hi { total += nodes[edges[k]].weight; } // no checks left
}
let w = edges[0];
let plain: u32 = w; // as a number
print(total, " ", w, " ", plain + 1);
}21 1 2
Spec §3.11, docs/design/index_types.md.
checkfree fn
checkfree asks the compiler to reject the program if a
bounds check is left anywhere in the function's text after bounds-check
elimination, in every specialization and every copy inlined into a
caller. It changes nothing about what the program computes. Every call
in such a function must be to another checkfree function, a
builtin or an extern fn, and a block it calls must be
written in a checkfree function. The other aborts stay,
assert in particular, which is how such a function states
its preconditions once, outside its loops:
var nodes: i32[>..] = [];
type NodeId = u32 in nodes;
checkfree fn sum_at(ids: NodeId[:], weights: i32[>..]&) -> i64 {
assert(weights.len >= nodes.len); // once; the loop is then check-free
var t = 0;
for id in ids { t += nodes[id] + weights[id]; }
return t;
}
fn main() {
for w in [1, 2, 3] { nodes.push(w as i32); }
var weights: i32[>..] = [10, 20, 30];
let ids: NodeId[2] = [2 as NodeId, 0 as NodeId];
print(sum_at(ids, weights));
}44
The compiler must prove at least these inside a
checkfree function: a constant index into a fixed array; an
index of a's index type into a; one into
b where b.len >= a.len was asserted and
neither array changed since; a for index into the array
whose length is its count; a[i] after a condition
established 0 <= i < a.len; and a repeat of a check
already passed. The error names the check that stayed and why.
checkfree goes after export and before
inline, simd, recursive or
fn, and a nested function inherits it. Spec §7.14.
16. Expressions and control flow
The language is expression-oriented: if,
match, block and a bare { … } are
expressions, and a block's value is its trailing expression. Assignment,
++ and -- are statements. The operators are
C's and Rust's, with Rust's precedence (Appendix A).
if
let hp = 30;
let kind = if hp > 50 { "healthy" } else if hp > 0 { "hurt" } else { "down" };
if hp > 0 { print(kind); } // as a statement: no else neededUsed as a value, an if needs a final else,
and its branches join into one type. A literal branch adapts to the
other's type: if c { x } else { 0 } has x's
type, a u8 for a u8 x, and
if c { x } else { 0.5 } is an f32 for an
f32 x. An integer branch beside a float branch
converts to the float. A construct whose branches are all literals is
itself literal-like and adapts wherever it meets a type. Arrays and
slices of one element type join as a slice: where one branch is
a string literal and the other a u8[..16] field, the result
is a const u8[:] viewing the field, with nothing copied.
Spec §6.4.
struct Item { name: u8[..16], count: i64 }
fn main() {
let item = Item { name: "bolt", count: 0 };
let name = if item.count == 0 { "none" } else { item.name }; // a const u8[:]
let k: u8 = if item.count > 0 { 1 } else { 2 }; // literals adapt to u8
print(name, " ", k);
}none 2
A trailing construct that cannot supply a value, an if
without else, a match with an arm that ends in
a return, a loop none of whose
breaks carries a value, is a statement, so a function whose
result is inferred may end in one and produce nothing.
block,
break with a value, and bare blocks
block { … } gives break a target without a
loop, so a chain of checks can bail out with a value. A bare
{ … } is only a scope: a break inside it
leaves the enclosing loop or block. All the
break E of one construct must agree on E's
type.
struct P { alive: bool, hp: i64 }
fn main() {
let p = P { alive: true, hp: 0 };
let verdict = block {
guard p.alive else { break "gone"; }
guard p.hp > 0 else { break "dead"; }
"standing"
};
let doubled = { let t = p.hp + 1; t * 2 };
var n = 0;
let found = loop { n += 7; if n % 5 == 0 { break n; } };
print(verdict, " ", doubled, " ", found);
}dead 2 35
guard
guard c; runs the rest of its block only where
c holds, and guard c else { … } runs the
else block instead. It is if turned inside
out, and it jumps nowhere by itself: directly in a function body the
rest of the block is the rest of the function, in a loop body skipping
the rest goes on to the next iteration, and in a function value's body
it ends that call of the value. Leaving more than the block is the
else block's job: guard c else { return; }
leaves the function, guard c else { break; } the loop. The
code after a guard knows the condition held, which includes narrowing an
optional: after guard r else { return; }, r is
a T&.
fn half(n: i64) -> i64 { guard n % 2 == 0 else { return -1; } n / 2 }
fn tenfold(p: i64?) -> i64 { guard p else { return 0; } p * 10 }
fn main() {
for i in 5 { guard i % 2 == 0; print("even ", i); }
let v = 4;
print(half(8), " ", half(7), " ", tenfold(v), " ", tenfold(null));
}even 0
even 2
even 4
4 -1 40 0
Spec §6.4.
Loops
for i in 3 { } // 0, 1, 2
for i in 10..13 { } // 10, 11, 12: half-open
for i: i32 in 0..10 { } // i is an i32; each bound must be, or widen to, one
for i: u8 in 0..256 { } // every u8, the end one past the type's last value
for x in xs { } // copies, for fixed-size elements; references for others
for &x in xs { x *= 2; } // references: the mutation form
for x, i in xs { } // with the index, an i64
for x, i: u8 in grid { } // the index at a narrower type, where the array's length fits it
while cond { }
loop { if done { break; } }- A range's bounds unify like operands, and
iruns at that type;for i in nis0..n. Bounds are evaluated once; an empty or reversed range does nothing. for x in arrcopies fixed-size elements and binds variable-size ones by reference. A copy cannot be written (x.f = 1is an error naming&x), since the write would update the copy and nothing else. An element that is a reference binds as the reference it holds.for &x in arrbinds every element by reference. Over aT[>..<]the binding is a live reference, so no shrink may happen inside the loop.- A loop iterates its sequence in place. A resizable array's length is
read again before each iteration, so pushes inside a by-value
foradd to the traversal; a slice keeps its own length. Whatever the loop walks in place is live for the whole body, so the array it lies in may not shrink there. continuegoes to the next element;breakleaves the innermost loop orblock. There are no labels.
fn main() {
var xs: i64[>..] = [1, 2, 3];
for &x in xs { x *= 10; }
for x, i in xs { if i == 0 { xs.push(x + 1); } } // the push extends the walk
var count: u8 = 0;
for i: u8 in 0..256 { count += 1; }
print(xs, " ", count);
}[10, 20, 30, 11] 0
(count wrapped to 0 after 256 increments, as a
u8 does.) Spec §6.5.
const for:
expansion at specialization time
const for specializes a fragment once per entry of a
compile-time sequence: the fields of a struct, the variants of an enum,
a list of types, or a list of constants. It is the language's
reflection, with no runtime iterator and no metadata: each copy of the
body is ordinary code.
struct Point { x: i64, y: i64, label: u8[..8] }
fn show_fields<T>(v: T&) {
const for <N, A> in fields<T> { print(N, " = ", v.A); }
}
fn scaled<T, F>(v: T&) -> T {
T { const for <N, A> in fields<T> { A: F(N, v.A), } }
}
fn main() {
var p = Point { x: 2, y: 3, label: "pt" };
show_fields(p);
const for <U> in types<i8, u16, f32> { print(default<U>()); }
const for <C> in [1, 2, 4] { let a: u8[C] = [0; C]; print(a.len); }
let q = scaled(p) { n, x => x };
print(q);
}x = 2
y = 3
label = pt
0
0
0.0
1
2
4
Point { 2, 3, "pt" }
fields<T> binds each field's name (a string
constant) and an accessor usable as v.A or as the
label A: in a literal of that struct type;
types<…> binds a type; […] binds a
constant (literals, named constants, and the integer expressions an
array size allows). A statement fragment is a scope of its own per
entry, and a constructor fragment contributes initializers.
break and continue cannot cross an
expansion.
variants<E> binds each variant's name and the
variant V, written where a variant's name is:
V as a type (and fields<V> for its
payload), V { … } to build one, V c or
V &c as a match arm, and V as
a value when it has no payload. A match fragment
contributes arms, checked as written ones: every variant once,
_ last.
enum Shape { Circle { r: i64 }, Rect { w: i64, h: i64 }, Dot }
fn show<T>(value: T) {
match value {
const for <N, V> in variants<T> {
V v => {
print(N);
const for <F, A> in fields<V> { print(" ", F, " = ", v.A); }
},
}
}
}
fn main() {
let s: Shape = Shape.Rect { w: 3, h: 4 };
show(s);
const for <N, V> in variants<Shape> { print(N); }
}Rect
w = 3
h = 4
Circle
Rect
Dot
Spec §6.5, docs/design/const_for.md.
Functions that never return
abort(msg) and exit(code) never return, and
the checker knows it: a branch that ends in one gives no value,
statements after one are not checked, and the knowledge propagates
through calls, so a function may end in a call to one without a dummy
result.
fn fail(what: u8[:]) { abort(str("cannot ", what)); }
fn value(ok: bool) -> i64 {
if ok { 42 } else { fail("compute") }
}
fn main() { print(value(true)); }42
An unknown recursive call is assumed to return. Spec §7.3, §9.3.
Bounds checks
Every index into an array or slice is checked, and every conversion
into an index type (section 15); a check
that fails aborts the program. The compiler removes the checks it can
prove unnecessary, and the language is designed to give that analysis
what it needs: a T[>..] shrinks only at a
pop, pop_n, resize or
clear the analysis sees, so a bound established before a
push still holds after it; % is Euclidean, so
a reduction is in range by construction; fixed sizes and capacities are
constants; and every call site records what it proves about its
arguments. The idiom that helps most is an assert of the
lengths at the top of a kernel:
let W = 8;
fn blur_row(above: u8[:], here: u8[:], below: u8[:], out: u8[:]) {
assert(above.len == W && here.len == W && below.len == W && out.len == W);
for x in 1..W - 1 { // every index is x plus a constant: no checks
let sum = above[x - 1] as i64 + above[x] + above[x + 1] +
here[x - 1] + here[x] + here[x + 1] +
below[x - 1] + below[x] + below[x + 1];
out[x] = (sum / 9) as! u8;
}
}goose --bce-lines reports, per line, which checks
survived. The other aborts that remain in every build are division by
zero, a limited array's capacity overflow, shrinking below empty, a
relative-reference offset that does not fit its width, a failed
assert, and data-stack exhaustion, all of them clean aborts
with a message. Spec §9.3, §10.5.
17. Functions
fn name(a: i64, var b: f64, xs: i32[:]) -> i64 { … }
fn generic<T>(a: T, b: T) -> T { … }
fn also_generic(a, b) { … } // untyped parameters are generic
fn scaled(a: i32, b: i32 = 0, c: f32 = 1.0) -> f32 { … } // scaled(2) is scaled(2, 0, 1.0)Functions are free functions. There are no methods and no
impl blocks, but x.f(a) is exactly
f(x, a) for any function, so method-call syntax is
available wherever it reads better. Resolution tries fields and built-in
members first, then functions. Top-level declarations are
order-independent. Spec §7.1.
Parameters
- A parameter is a
letbinding: it cannot be reassigned, while its contents are as writable as its type says.var b: f64makes a by-value parameter a reassignable local. - A parameter with no type is generic (section 18).
- A reference parameter,
n: i64&, binds an lvalue of the pointee type without&. A slice parameter takes every array kind. A by-value non-fixed parameter,xs: u8[>..], is constructed in the argument slot and owned by the callee like a local:f(copy(xs))orf(make()). - Defaults. A trailing parameter may declare a
default,
c: f32 = 1.0, and a call may leave out any number of trailing parameters that have one. The default is evaluated at each such call, after the written arguments, as a value of the parameter's type; it may name globals and call functions, and never sees the caller's locals. A generic parameter's default adapts to the type the arguments bind (fn scale<T>(x: T, by: T = 2)doubles au8and anf64alike) and decides no type variable itself.
var counter = 0;
fn bump(r: i64& = counter, by: i64 = 1) { r += by; }
fn limit(x: i64, lo: i64 = 0, hi: i64 = 100) -> i64 {
if x < lo { lo } else if x > hi { hi } else { x }
}
fn scale<T>(x: T, by: T = 2) -> T { x * by }
fn main() {
bump(); bump();
var mine = 10;
bump(mine, 5);
let b: u8 = 21;
print(counter, " ", mine, " ", limit(250), " ", limit(-5, -10), " ", 7.limit(0, 5), " ",
scale(b), " ", scale(1.5));
}2 15 100 -5 5 42 3.0
Results
A function declares its results after ->, several of
them separated by commas. Several results are a calling convention, not
a tuple type: a call in an ordinary value context yields the first,
let a, b = f(); binds them all,
x, xs[i] = f(); assigns them to places that exist, and
return f() forwards all of them. By convention a trailing
bool or error value says whether the rest means
anything.
fn divide(a: i64, b: i64) -> i64, bool {
if b == 0 { return 0, false; }
return a / b, true;
}
fn main() {
let q, ok = divide(7, 2);
var r = 0;
var flag = false;
r, flag = divide(1, 0);
print(q, " ", ok, " ", r, " ", flag, " ", divide(9, 3));
}3 true 0 false 3
Only return and a call carry several values: a branch
cannot end in a, b, so each branch returns on its own.
A result type may be left out where it can be inferred, in which case
it is taken from the first return (or the body's final
expression), as an un-annotated let takes its initializer's
type: a non-fixed lvalue other than the function's own local, or an
explicit &x, makes the result a reference, and a
reference to a fixed-size value is returned as a copy. Across a
recursive cycle every signature must be written out.
A non-fixed result is constructed directly in its
destination (section 4): the
caller's variable, the element being pushed, the argument slot of
another call. When every return of such a result returns
the same local, that local is the destination from its
declaration on, so return xs costs nothing (the compiler
warns where a second returned local keeps the first from its
destination). A function's own local is moved by
return; a part of it, or of a temporary, dies with the call
and has to be returned as copy(x). Spec §7.1, §7.3.
Overloading
Functions overload by parameter types, resolved statically: the
unique exact match wins, then a generic exact match, then a match that
needs a coercion (array to slice, a literal fitting a type, implicit
widening). A candidate's rank is its worst argument's rank, and ties are
errors. A default is no argument, so f(a, b = 0) beside
f(a) makes every one-argument call ambiguous. Only when no
ordinary candidate matches is tag dispatch (section 9) tried, and a candidate that
converts an integer argument to a float comes last of all:
f(2) calls f(i32) beside f(f64),
and sqrt(n) with an integer n is ambiguous
between the two widths (sqrt(n as f64)). The expected
result type never selects an overload.
fn describe(x: i64) -> u8[>..] { str("int ", x) }
fn describe(x: f64) -> u8[>..] { str("float ", x) }
fn describe(xs: i64[:]) -> u8[>..] { str(xs.len, " ints, first ", xs[0]) }
fn main() {
let xs: i64[3] = [4, 8, 15];
print(describe(7), "; ", 2.5.describe(), "; ", xs.describe(), "; ", describe(xs[1..]));
}int 7; float 2.5; 3 ints, first 4; 2 ints, first 8
Spec §7.1.
Nested functions
A function may be declared inside a function, and it reads and writes the enclosing function's locals as free variables, under those variables' normal rules. Names resolve where the nested function is declared, and the nested functions of one block may call each other in either order, as the functions of a recursive-descent parser do; the enclosing code can call one only after its declaration. Nested functions of one name in one block overload like top-level ones, so case functions can be nested too. A nested function is a compile-time entity: it is passed as a function value and never escapes.
struct Employee { name: u8[..12], dept: u8[..8], salary: i32 }
fn main() {
var staff: Employee[>..] = [];
staff.push(Employee { name: "ada", dept: "eng", salary: 120 });
staff.push(Employee { name: "linus", dept: "ops", salary: 90 });
staff.push(Employee { name: "grace", dept: "eng", salary: 135 });
var budget = 0;
var seen = 0;
fn spend(e: Employee&) { // writes main's locals
budget += e.salary;
seen++;
}
for &e in staff { if e.dept == "eng" { spend(e); } }
print("eng budget ", budget, " over ", seen, " people");
}eng budget 255 over 2 people
Spec §7.5.
Function qualifiers
| Qualifier | Meaning | Section |
|---|---|---|
recursive fn |
the entry of a recursive cycle | 19 |
extern fn, extern "sym" fn |
a C function | 24 |
export fn, export "sym" fn |
a Goose function callable from C | 24 |
thread_fn |
a worker's entry point | 22 |
checkfree fn |
no bounds check may remain in it | 15 |
inline fn |
inlined at every call, whatever its size | below |
simd fn |
built once per instruction-set level | below |
The qualifiers come in a fixed order: export or
extern first, then checkfree,
simd, inline, recursive, and
fn or thread_fn.
inline asks for the function to be inlined at every
call; without it the compiler decides from the function's size and its
number of calls. It changes nothing about what the program computes, and
is refused with extern, export,
simd, recursive and
thread_fn.
simd asks for the function to be built once for the
target's baseline instruction set and once more for each wider one it
supports (AVX2 and AVX-512 on x86-64), with the widest version the CPU
has chosen at each call. It suits a function holding a hot loop the C
compiler can vectorize: byte transforms, codecs, kernels over arrays.
Every version computes exactly what the baseline does, so nothing about
a program's output depends on it; a backend may ignore it, as the C
backend does under compilers other than clang and under the in-process
TinyCC backend. Spec §7.12, §7.13.
simd fn checksum(xs: u8[:]) -> u32 {
var h: u32 = 0;
for x in xs { h += x as u32 * 31 + 7; }
return h;
}
inline fn clamp01(x: f32) -> f32 { if x < 0 { 0 } else if x > 1 { 1 } else { x } }
fn main() { print(checksum("goose"), " ", clamp01(1.5)); }16806 1.0
18. Generics and function values
Type parameters
An untyped parameter is generic. An explicit <T>
says that two parameters must agree, or names a type no parameter
carries. Everything is monomorphized: a generic body is typechecked only
at an instantiation where every type is concrete, and errors are
reported at the offending instantiation with the compile-time call chain
that produced it. Type variables are never checked abstractly, so a
generic body may do anything the concrete types allow, with no trait
bounds.
fn twice(x) { x + x }
fn span<T>(a: T, b: T) -> T { if a > b { a - b } else { b - a } }
fn zero<T>() -> T { default<T>() }
fn main() {
print(twice(21), " ", twice(1.5), " ", span(3, 8), " ", span(2.5, 1.0), " ", zero<f64>() + 0.5);
}42 3.0 5 1.5 0.5
Type arguments are inferred from the argument types. Where several
arguments mention one type variable, the typed ones bind it and a
literal then adapts: max(n, 0) with a u32
n is the u32 instantiation. A float literal
binds before an integer one, and [] and null
bind nothing. An explicit list f<i64>(x) binds the
leading type parameters in order; it is needed when no argument mentions
a parameter (qget<i64>(),
zero<f64>()). f< begins a type
argument list only when the <…> is followed by
(, { or .Variant {; otherwise
< is a comparison.
Inside generic code, .= on a location that is not a
reference assigns as = does. That lets generic code move
whole values of a type parameter's type with .=: where
T is a reference type it moves the reference itself, where
= would write through it, and where T is a
value it is a plain assignment. The library's swap_at is
written that way, and so swaps the elements of an array of references,
relative ones included, as it does those of any other array.
Where a literal is the only thing binding a type variable, the
parameter is a literal parameter inside the specialization: a
constant of unknown value that adapts wherever it meets a type, so
push_n(flags, 1, n) with
fn push_n<A, T>(xs: A&, v: T, n: i64) pushes a
u8 into a u8[>..] with no cast at the call,
and push_n(flags, 300, n) is an error naming the
push that takes a u8.
Floating context. An f32 destination
may refine an otherwise unconstrained float computation in a generic
call: with integer width and height,
let scale: f32 = max(0.5, min(width / 960.0, height / 600.0));
computes both calls in f32. The rule applies to a function
returning a bare T whose T parameters are bare
values, given literals and float expressions of literals and integers; a
typed argument commits the type. Spec §7.7.
Function values
Function values are compile-time entities passed as generic
parameters, not runtime data. A function argument is a function name, a
bound generic function parameter, or a block literal; the
: fn(…) bound on the parameter is optional documentation.
Every call of one is direct and inlinable, and it cannot escape: storing
one, returning one, or putting one in data is a compile error. There are
no closure objects and no function pointers, so a higher-order function
compiles to exactly the loop it looks like.
fn foo<F: fn(i64)>(a: i64) { F(a); }
foo(1) { print(it) } // the trailing-block form, with the implicit `it`
foo(1) { x => print(x) } // named block parameters
fn h(v: i64) { print(v) }
foo(1, h) // a named function- A trailing block
{ … }after a call's parentheses is the last argument. Its parameters areit, or named ones before=>. Goose allows one trailing block per call; a second function value is passed by name. - A block sees the enclosing function's locals and writes them, like a nested function.
- A
returninside a block returns from the lexically enclosing named function, not from the function calling the block, so a hand-written iterator has the same structuring power asfor. A reference it returns may be one the function calling the block handed it, which points where that call's argument does, but not one into that function's own locals or the block's, which die as the return leaves them. - A named function keeps its parameter defaults as a value; a block's parameters have none. A function value may follow fewer arguments than the callee has parameters where the rest have defaults.
- A name with several overloads passes them all, and each call of the value resolves among them.
- A block infers its value's type without the destination type its
call meets, so a block cannot be
[]. Its receiving storage is still available for checking relative links in a fresh result.
fn each_pair<T, F>(xs: T[:]) {
for i in 0..xs.len - 1 { F(xs[i], xs[i + 1]); }
}
fn each_step<F>(lo: i64, hi: i64, step: i64 = 1) {
var i = lo;
while i < hi { F(i); i += step; }
}
fn first_multiple_of_7(lo: i64, hi: i64) -> i64 {
each_step(lo, hi, 3) { if it % 7 == 0 { return it; } }; // returns from first_multiple_of_7
return -1;
}
fn main() {
let xs: i64[5] = [4, 8, 15, 16, 23];
var gaps: i64[>..] = [];
each_pair(xs) { a, b => gaps.push(b - a); };
let evens = xs.filter() { it % 2 == 0 };
let total = fold(xs, 0) { acc, x => acc + x };
let big = xs.find() { it > 10 };
sort(gaps) { a, b => a > b };
print(gaps, " ", evens, " ", total, " ", first_multiple_of_7(10, 40), " ",
xs.map() { it * 2 }, " ", big);
}[7, 7, 4, 1] [4, 8, 16] 66 28 [8, 16, 30, 32, 46] 15
The library's filter and map build their
result straight into the variable receiving it, with no intermediate
array. A HOF never states what its function value returns:
map's result element type is whatever F(x) is
in that instantiation. Spec §7.6, §7.7.
19. Recursion
Recursion is opt-in and annotated. The entry function of every
recursive cycle is marked recursive fn, every function in
the cycle has a fully explicit signature, and the key restriction
is:
No function in the cycle may call back into the cycle while one of its non-fixed locals is in scope.
A growable or variable-size local holds a data stack from its
declaration to the end of its block, and if one were in scope at a
recursive call, every activation would need a stack of its own. So a
recursive function owns no growable data across a recursive call, and
what must outlive the call is owned outside the cycle and passed in: a
reference, a slice, a pool, or a struct of references to several tables.
Scratch declared in a block that ends before the call costs the
recursion nothing. The same holds of the temporaries of the calling
statement: at a call into the cycle, no non-fixed temporary may be live,
which rules out a str(…) as an argument of the call, or the
call inside a print. Recursion depth then costs native
stack only, and exhausting it is a clean abort.
struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? }
recursive fn show(n: Node&, depth: i64) {
{ // scratch in a block that ends before the calls
var line: u8[>..] = [];
for i in depth { line.append(" "); }
format(line, n.key);
print(line);
}
let l = n.left;
if l { show(l, depth + 1); }
let r = n.right;
if r { show(r, depth + 1); }
}
recursive fn count(n: Node?) -> i64 {
guard n else { return 0; }
return 1 + count(n.left) + count(n.right);
}
fn main() {
var pool: Node[>..] = [];
let a .= pool.push(Node { key: 1 });
a.left .= pool.push(Node { key: 2 });
a.right .= pool.push(Node { key: 3 });
show(a, 0);
print(count(a), " nodes");
}1
2
3
3 nodes
Sharing scratch across the recursion
A grow-shrink array handed to the cycle by reference can serve every
activation at once: each takes let mark = scratch.len,
pushes its own entries above its callers', and ends with
scratch.resize(mark). Such calls are balanced (section
13), so a view of the activation's own part stays usable across
them.
recursive fn walk(scratch: i64[>..<]&, depth: i64) -> i64 {
let mark = scratch.len;
scratch.push(depth);
let mine = scratch[mark..];
var total = 0;
if depth < 3 { total = walk(scratch, depth + 1); }
total += mine[0];
scratch.resize(mark);
return total;
}
fn main() {
var scratch: i64[>..<] = [];
print(walk(scratch, 0), " ", scratch.len);
}6 0
A program whose state is several growable tables (nodes, text, a stack) owns each as a local of a non-recursive driver function and passes the recursion one struct of references to them; or writes the recursive functions nested inside the driver, where they reach the tables as free variables with no struct at all, as the samples' parsers do.
The rules in detail
- Signatures. No result type is inferred across a cycle, and none may be inferred as a reference.
- Parameters. No function in a cycle has a by-value non-fixed parameter: it takes a reference or a slice.
- Stores. A reference rooted at a cycle function's own local may be passed down, but not stored anywhere or returned, since activations of one local cannot be told apart. References rooted outside the cycle are unrestricted: a pool passed in by reference, a local of an enclosing non-recursive function, a global. Every recursive call must pass such a pool on as the entry call did.
- Results. A cycle function may return the result of a recursive call; the roots of such results are worked out in rounds over the cycle.
- Polymorphic recursion. A recursive call may instantiate its callee with other type arguments, but the instantiations must be finite: a call whose types grow with each round is rejected once it would put more than 16 specializations of one function in progress.
Spec §7.8.
20. Long-distance return and errors
There is no exception mechanism and no Result type, and
no required error convention. For a call with two outcomes, a trailing
bool is enough; for "found or not", a T?,
which costs nothing because it is a reference into the input. Both are
ordinary return values (section 17). The third
idiom is for a failure deep inside nested calls that should land in one
place.
return … from
return E from f returns E as the result of
the innermost active call of f, unwinding every frame in
between. The functions between keep their plain signatures: no error
parameter, no ? on each call, nothing to propagate.
struct User { name: u8[..16], age: i32 }
fn load(text: u8[:]) -> User[>..], u8[] {
var users: User[>..] = [];
var lineno = 0;
each_split(text, '\n') {
lineno++;
if it.len > 0 { users.push(parse_record(it, lineno)); }
};
return users, "";
}
fn parse_record(line: u8[:], lineno: i64) -> User {
let comma = find(line, ",");
guard comma >= 0 else { return [], str("line ", lineno, ": expected name,age") from load; }
let age, ok = parse_int(trim(line[comma + 1..]));
guard ok else { return [], str("line ", lineno, ": bad age") from load; }
return User { name: trim(line[..comma]), age: age as i32 };
}
fn report(text: u8[:]) {
let users, err = load(text);
if err.len > 0 { print("error: ", err); return; }
print("loaded ", users.len, " users");
}
fn main() {
report("ada, 36\nlinus, 20\n");
report("ada, 36\nlinus\n");
report("ada, x\n");
}loaded 2 users
error: line 2: expected name,age
error: line 1: bad age
- It is checked statically. Every call of a function
containing
return … from loadmust lie within a call ofload, which the whole-program compiler verifies in call-graph order. Nothing is uncaught at run time. - It is a hidden discriminant, not an unwinder. Each frame on the path checks a flag after the call and returns at once in the propagate case; normal epilogues run, so every data stack restores itself. There are no tables and no destructors, because there is nothing to destroy.
- The value lands where
load's caller wanted it.str(…)builds the message directly atload's return destination, straight past the frames being unwound. fis resolved where the returning function is written: a nested function in scope, else the name's overload set, withfrom ns::parseexplicit. An unrelated function that merely shares the name never catches the return.- The current compiler lets a long-distance return written in a named function carry references to globals and static data only; return owned values, as above.
The same machinery is what lets a block return from its
enclosing function through a higher-order function (section 18). The library's
exception module packages it as
exception::try<E>(body) { error => … } and
exception::throw(value) (stdlib.md). Spec §7.9.
Stopping the program
assert(c) aborts unless c holds, and
narrows an optional it is given; abort(msg) aborts with a
message; exit(code) ends the program with an exit code. The
last two never return, which the checker uses (section 16). None of them is
catchable. Spec §9.3.
21. Program structure
Files and imports
A program is compiled whole, from the root file named on the
command line. import a.b.c; includes the file
a/b/c.goose once, resolved relative to the root of the
importing file's package (for the program's own files, the directory of
the root file), then as a module of a package the root declared, then in
the standard library's directory. import .a.b; with a
leading dot resolves relative to the importing file instead. An import
loads a file and nothing more: it creates no namespace and brings no
names into scope beyond what the file declares. The standard library's
modules are imported by name: import std;,
import dictionary;.
Packages
import name from "source"; in the root file declares the
package name, a directory holding its entry
name.goose, and imports that entry: the source is a
directory relative to the root file ("../mylib") or a git
repository at a tag or commit ("owner/repo@v1" on GitHub,
"host/owner/repo@ref", or a URL), which the compiler
fetches and builds once. Only the root file declares packages; the same
line in any other file, a library's own included, is a
requirement that the root must satisfy, so a program's
dependencies are flat, pinned and in one place. packages.md has the whole story, including a
program's own C and how to make a package.
Namespaces
namespace image; at the top of a file, before its
declarations, places them in the namespace image; a file
without it declares into the global namespace, and several files may
share a namespace. From elsewhere a declaration is
image::Pixel, image::brightness(p),
image::Shape.Circle { r: 1.0 }; ::name names a
global declaration that a namespaced one shadows, or a builtin. A
declaration may spell its namespace itself,
fn image::brightness(…), or fn ::main() inside
a namespaced file.
import std;
namespace geo;
struct Pt { x: i64, y: i64 }
fn norm1(p: Pt) -> i64 { abs(p.x) + abs(p.y) } // std's abs, found globally
fn format(out: u8[>..]&, p: Pt) { format(out, "(", p.x, ",", p.y, ")"); }
fn ::main() { // declared in the global namespace
let p = geo::Pt { -3, 4 };
print(p, " ", geo::norm1(p)); // the format hook follows the type
}(-3,4) 7
An unqualified name resolves lexically first (locals, type
parameters, nested functions), then in the current declaration's
namespace, then in the global namespace and the builtins. A function
name's overload set is that of the first namespace in this order that
declares the name at all; sets never merge across namespaces, so a
namespaced hash overload reaches the global ones as
::hash(x). UFCS follows the same rule for the calling
code's namespace, so p.norm1() inside the global
main above would not find geo::norm1. The
format hook is the one type-directed exception:
print(p) finds geo::format for a
geo::Pt from anywhere. A global variable and functions may
share a name, since a call resolves past variables to functions.
Namespaces affect name resolution, type identity and generated C names,
and nothing else: there is no privacy, no re-export and no nesting. Spec
§11.1, docs/design/namespaces.md.
Globals and initialization
Globals initialize before main, in declaration order
within a file and imported files first, so an initializer may use a
global from a module the file imports, and may call functions. Naming a
global before its initializer has run, in an earlier initializer or in a
function one calls, is an error. The whole program runs inside an
implicit outermost scope that owns the globals: they are the outermost
roots for the lifetime rules, and a global growable array owns the
bottom of a data stack for the program's life, a natural whole-program
arena. const globals of flat fixed type with compile-time
initializers live in static data, shared and read-only.
var log: u8[>..]; // a var with only a type: empty
let NAMES = ["ada", "grace"]; // static data
let FIRST = NAMES.len - 1; // a named constant
var counts: i64[NAMES.len] = [0; NAMES.len];
fn main() {
log.append("started");
counts[FIRST] += 1;
print(log, " ", NAMES[FIRST], " ", counts);
}started grace [0, 1]
main
fn main() { } in the global namespace of the root file
is the entry point. A fn main in an imported file is
ignored entirely, so a runnable file doubles as a library: give it
fn main_x() and a fn main() { main_x(); }
wrapper, and importers call main_x. A program's arguments
come through the os module (args(),
arg_count()), and exit(code) sets the process
exit code.
embed_file
embed_file("path") reads a file when the program is
compiled, relative to the source file making the call, and
gives its bytes as a const u8[:] into static data: a font,
a table, a shader. The path is a string literal or a let
global holding one, and each distinct file is read once.
embed_shader does the same for GLSL, compiling it for the
graphics module (stdlib.md). Spec
§12.
22. Threads
There is no shared mutable memory. A worker is declared
thread_fn, and the compiler compiles it and everything it
calls as a separate program with its own data stacks
and its own copies of the globals it uses, taken from the spawning
program at the moment of the spawn. A worker that increments a global
increments its own; main's is what it was. There are no data races to
have, and no locks, atomics or memory orderings anywhere in the
language.
| Builtin | Meaning |
|---|---|
thread_spawn(worker, args…) -> i64 |
starts a worker; its id, never reused |
thread_spawn_contained(worker, args…) -> i64 |
starts a worker whose aborts end only that worker |
thread_wait(id) -> bool |
blocks until that worker has ended and its storage is released;
false if it was contained and aborted |
thread_wait(id, msg) -> bool |
as thread_wait(id), and the first such wait appends a
contained worker's abort message to msg |
hardware_threads() -> i64 |
a sensible worker count, never more than the data stacks allow |
qput(v) -> bool |
puts v on the queue for its type; false,
at once, if the queue is full |
qget<T>() -> T |
takes the next T, blocking |
qget<T>(ms) -> T, bool |
the same, waiting at most ms milliseconds; else
T's zero value and false |
qpoll<T>() -> T, bool |
takes one if there is one; else T's zero value and
false |
qlimit<T>(n) |
lets the queue for T hold at most n
messages |
Values cross between programs through typed queues,
one per type, and whatever crosses, arguments included, must be
flat: no references or slices at any depth. That is
cheap because a flat Goose value is contiguous, so a job or a result
carries real data, variable-size parts included, as one copy. A
thread_fn's parameters have no defaults.
struct Job { n: i32 }
struct Result { n: i32, digits: i32 }
fn count_digits(n: i32) -> i32 { var d: i32 = 1; var v = n; while v >= 10 { v /= 10; d++; } d }
thread_fn worker() {
loop {
let job = qget<Job>(); // blocks on the queue for Job
guard job.n >= 0 else { break; } // -1 means stop
qput(Result { n: job.n, digits: count_digits(job.n) });
}
}
fn main() {
let n = max(1, hardware_threads());
for i in 8 { qput(Job { n: (1 << (i * 3)) as i32 }); }
for i in n { qput(Job { n: -1 }); } // one quit job per worker
var ids: i64[>..] = [];
for i in n { ids.push(thread_spawn(worker)); }
var total = 0;
for i in 8 { let r = qget<Result>(); total += r.digits; }
for id in ids { thread_wait(id); }
print(total, " digits in total");
}A queue has no limit of messages until qlimit gives it
one. Past it, qput returns false without
waiting, so a server can turn work away rather than queue it without
end; qput(v); as a statement ignores the answer. To bound
the memory a queue may hold, set its limit to that memory divided by the
size of a typical message.
A worker's return releases everything it owned, whether or not anyone
waits for it; queued messages outlive their sender; workers still
running when main returns are killed. A global that is not
flat is an error in any function a thread_fn reaches,
unless it is a let whose references all lead to static
data, as a string literal's do. Programs with threads need a real C
compiler: goose build, since the in-process TinyCC backend
cannot place thread-local storage. Spec §11.2.
Contained workers
A failed check or abort(msg) in a worker started with
thread_spawn_contained ends only that worker, which
releases what a return would. The message goes to the waiter instead of
stderr: every thread_wait for the worker returns
false, and the first thread_wait(id, msg)
appends the message, with its source location, to msg.
Later waits get no message, so ask for it once and keep it. A supervisor
can log it and start another:
thread_fn serve(attempt: i64) {
assert(attempt >= 2); // a bug the first two hit
print("attempt ", attempt, " served");
}
fn main() {
var attempt = 0;
var why: u8[>..] = [];
while !thread_wait(thread_spawn_contained(serve, attempt), why) {
print("attempt ", attempt, " failed: ", why);
why.clear();
attempt++;
}
}Stack overflow, exit(code), and aborts on main's thread
still end the program, and a worker that a contained one starts with
thread_spawn is not contained. Files and other resources
opened through extern fn stay open. Spec §11.2.
23. Serialization
A structure whose links are self-relative offsets means the same wherever it sits in memory, so saving it is a write of its bytes and loading it is a read. Three builtins do this for any array:
| Builtin | Meaning |
|---|---|
to_bytes(a) -> u8[>..],
to_bytes(a, out) |
a framed image of the array: a varint byte count, then its element
region; fresh, or appended to out |
bytes_of(a) -> u8[:] |
the element region alone, as a read-only view: nothing copied |
from_bytes<T[>..]>(b: u8[:]) -> T[>..], bool |
a verified array from an untrusted image;
false and an empty array if it is not one |
Bytes arriving from outside the program are the one place a reference
could enter without the compiler having proved it, so
from_bytes verifies before the bytes become a value: the
framing, every tag, every length, canonical varints, and every link
landing on an element start of that same image. A corrupt or hostile
file is a false, never a wild reference. What it verifies
is safety, not integrity: edited data bytes still describe a
well-formed structure, and a checksum is the program's business.
struct Node { word: u8[..8], count: i32, left: Node&<u32>?, right: Node&<u32>? }
fn insert(pool: Node[>..]&, word: u8[:]) {
if pool.len == 0 { pool.push(Node { word: word, count: 1 }); return; }
var cur .= pool[0];
loop {
let c = compare(word, cur.word[..]);
if c == 0 { cur.count += 1; return; }
let next = if c < 0 { cur.left } else { cur.right };
if next { cur .= next; continue; }
if c < 0 { cur.left .= pool.push(Node { word: word, count: 1 }); }
else { cur.right .= pool.push(Node { word: word, count: 1 }); }
return;
}
}
fn lookup(pool: Node[>..]&, word: u8[:]) -> i32 {
guard pool.len > 0 else { return 0; }
var cur .= pool[0];
loop {
let c = compare(word, cur.word[..]);
if c == 0 { return cur.count; }
let next = if c < 0 { cur.left } else { cur.right };
guard next else { return 0; }
cur .= next;
}
}
fn main() {
var index: Node[>..] = [];
each_word("the quick fox and the lazy dog and the fox") { insert(index, it); };
var image: u8[>..] = index.to_bytes(); // a framed image of the array
var loaded, ok = from_bytes<Node[>..]>(image); // verified before it becomes a value
print(ok, " ", loaded.len, " nodes, 'the' x", lookup(loaded, "the"), ", ",
bytes_of(index).len, " bytes");
image[image.len - 3] = 200; // tamper with a link ...
var bad, bok = from_bytes<Node[>..]>(image); // ... and it is false and an empty array
print(bok, " ", bad.len);
}true 6 nodes, 'the' x3, 126 bytes
false 0
What comes back is an ordinary array: it indexes, it grows, and
references into it are ordinary references. from_bytes also
builds T[>..<] and the T[] family, and
checks an array of index types against its array. Links into a
field of an element, and in pool links, are not
serializable; a bytes_of view is never writable, since
bytes written through it would be links the checker never proved. The
byte contract is in docs/design/serialization.md. Spec
§3.9, §9.4, §12.
24. Calling C and being called from C
extern fn
A declaration without a body binds a Goose signature to a C function; the optional string is the C symbol, and the Goose name is used otherwise. It is top-level, fully typed, not generic, and joins overload resolution like any function. A call compiles to a direct C call.
struct Stats { lo: i32, hi: i32, mean: f64 }
extern "cbrt" fn cube_root(x: f64) -> f64; // libm, by its C name
extern fn hypot(x: f64, y: f64) -> f64;
extern fn crc32_bytes(s: const u8[:]) -> u32; // a slice crosses as { data, len }
extern fn stats_of(xs: i32[:], out: Stats&); // a struct filled through a pointer
extern fn c_version(out: u8[>..]&); // a builder C appends toWhat may cross is exactly what has a plain C shape: the integer and
float scalars and bool; any flat fixed-size value (structs,
fixed and static-capacity arrays, fixed-mode enums) by value as its
packed C type; T& to such a T as a
pointer; T[:] of such a T as a
{ data, len } struct; and u8[>..]&, a
builder C appends to through gs_bld_append. Results are one
scalar or flat fixed-size value, or nothing. Everything else,
varints, references inside structs, variable or resizable
values by value, optionals, is rejected at the declaration. A parameter
is as writable as it says, so a read-only argument needs it declared
const. C must not keep a borrowed argument beyond the call.
The program's own C goes in a sidecar foo.build beside
foo.goose (c_sources = ["impl.c"];) or arrives
with --include header.h; the math and
os modules are built on this interface. Spec §7.10, packages.md.
export fn
export fn makes a Goose function callable from C: the
compiler emits a C wrapper under the function's name, or the string
given. The function must be top-level, non-generic, with explicit
parameter types and at most one typed result; defaults,
return … from, pool parameters and builders are not
supported at the boundary. Scalars and flat fixed-size values are
returned by value, and a const u8[:] result comes back as a
pointer plus an extra int64_t * length parameter (the bytes
are not NUL-terminated).
export fn hello_world() -> const u8[:] { return "Hello, world!" }
export "c_add" fn add(a: i32, b: i32) -> i32 { a + b }
fn main() { print(hello_world(), " ", add(20, 22)); } // runs once, inside goose_initHello, world! 42
goose --standalone --header hello.h -o hello.c hello.goose
builds the C for a C host: no main of its own, and a header
declaring goose_init(argc, argv), which the host calls once
before anything else. It sets up the runtime and the globals and runs
the Goose main, if there is one, so main is
where a library keeps its start-up code. Exported functions are not
thread-safe: call them from the thread that called
goose_init. Spec §7.11.
25. Builtins
The language's own functions. Everything else is the standard
library, written in Goose under stdlib/ and documented in
stdlib.md.
| Builtin | Meaning | Section |
|---|---|---|
print(a, b, …) |
the arguments' text and a newline, as one write | 6 |
str(a, b, …) -> u8[>..] |
the text as a fresh string, built at its destination | 6 |
format(out, a, b, …) |
the text appended to out, any growable u8
array |
6 |
assert(c) |
aborts unless c, a bool or an optional,
which it narrows |
20 |
abort(msg), exit(code) |
end the program; neither returns | 20 |
copy(x) -> T |
a fresh copy of the stored value x names |
4 |
default<T>() -> T |
the value a fixed-size T has before anything is written
to it, declared defaults applied |
8 |
hardware_threads(),
thread_spawn(worker, args…),
thread_spawn_contained(worker, args…),
thread_wait(id), thread_wait(id, msg) |
workers | 22 |
qput(v), qget<T>(),
qpoll<T>(), qlimit<T>(n) |
the typed queues | 22 |
to_bytes(a), to_bytes(a, out),
bytes_of(a),
from_bytes<T[>..]>(b) |
serialization | 23 |
embed_file(path), embed_shader(…) |
a file's bytes, or a compiled shader, read at compile time | 21 |
The array members, .len, .cap,
.push, .append, .pop,
.pop_n, .resize, .clear,
.index_of, are in section 5, and the
pool operations .alloc_index, .alloc_ref,
.free, .alloc_slice,
.realloc_slice, .free_slice in section 14. Spec §12.
Appendix A. Keywords and operators
Keywords:
import namespace struct enum type fn thread_fn export simd recursive let var extern const pad reusable if else while for in loop block guard match return break continue as true false null self bool varint i8 i16 i32 i64 u8 u16 u32 u64 f32 f64.
Contextual keywords, names everywhere else:
from (after a return value), upto
(after an unsigned type in a type), inline and
checkfree (before fn, recursive,
simd or thread_fn), fields and
types (in a const for header).
Operators, tightest first; the precedence is Rust's:
| Level | Operators |
|---|---|
| postfix | f(…), a[i], a[x..y],
.field, x as T, x as! T |
| unary | -, !, ~,
& |
| multiplicative | *, /, % |
| additive | +, - |
| shifts | <<, >> |
| bitwise and | & |
| bitwise xor | ^ |
| bitwise or | | |
| comparison | <, <=, >,
>= |
| equality | ==, !=, .==,
.!= |
| logical and | && |
| logical or | || |
Assignment statements: =, .=, and the
compound form of every binary operator (+=,
<<=, &=, …); ++ and
-- on integer lvalues. A range a..b appears
only in slicing brackets, for headers and match arms. Spec
§6.1, Appendix D.
Appendix B. Glossary
- Balanced call: a call during which an array is never shorter than when the call began, so views taken before it survive it. §13.
- Candidate: a variable whose own storage could hold the target of a reference read out of a container; the roots of such a reference. §12.
- Case function: one overload per variant type,
called with the enum;
matchas an overload set. §9. - Data stack: a large address-space reservation with a bump pointer; where every non-fixed value lives. §4.
- Exact root: a root whose own storage holds the reference's target, as opposed to one that only bounds its lifetime. §12.
- Fixed, variable, resizable: the size classes: constant size, size chosen at construction, size that can change. §4.
- Flat: holding no reference, slice or relative reference at any depth; what may cross between threads. §4, §22.
- Function value: a named function or a block passed as a generic parameter; a compile-time entity, never data. §18.
- Grow-only, grow-shrink:
T[>..], which shrinks only where nothing points into it, andT[>..<], whose interior references live in variables only. §5, §13. - Holder: a value that contains references: a struct with a reference field, an array of slices. §12.
- Index type, bound type:
uN in a, an integer belowa.len;uN upto a, one at mosta.len. §15. - Limited array:
T[..k]orT[..], a length within an inline capacity. §5. - Named constant: an untyped
letorconstglobal with a constant initializer, used as a literal is. §2. - Narrowing: the region after an
if,while,guardorassertin which an optional is a plain reference. §10. - Pool: a grow-only array used as storage for linked
data;
reusableadds a freelist. §11, §14. - Relative reference: a link stored as a narrow offset, self-relative or measured from a named pool's base. §11.
- Root: the variable that bounds the lifetime of a reference's target; inferred, never written. §12.
- Sequential array: one whose elements vary in size, which can be iterated but not indexed. §5.
- Slot: a location with a declared type (a field, an element, a global, an annotated variable), which says whether what it holds may be read-only. §12.
- Temporary: a value that lasts for the rest of its
statement: a literal, a call's result, a
copy, a control construct's value. §12. - Transparent: a reference's behaving as its target in every value context. §10.
- Variable mode, fixed mode: an enum stored as
exactly its variant's size (
Shape..), or as a tag plus room for the largest variant (Shape). §9.
Goose