# The Goose Language — Specification (v4 draft)
Table of contents
- [1. Core concepts](#1-core-concepts)
- [2. Lexical structure and syntax style](#2-lexical-structure-and-syntax-style)
- [3. Types](#3-types)
- [4. Values, copying, and assignment](#4-values-copying-and-assignment)
- [5. The resizable/shrink rules](#5-the-resizableshrink-rules)
- [6. Expressions and arithmetic](#6-expressions-and-arithmetic)
- [7. Functions](#7-functions)
- [8. ADTs in use](#8-adts-in-use)
- [9. References, lifetimes, and safety](#9-references-lifetimes-and-safety)
- [10. Compilation model](#10-compilation-model)
- [11. Program structure](#11-program-structure)
- [12. Builtins, the standard library, and what v1 leaves out](#12-builtins-the-standard-library-and-what-v1-leaves-out)
- [Appendix A. Worked examples (informative)](#appendix-a-worked-examples-informative)
- [Appendix B. TODO / open items](#appendix-b-todo--open-items)
- [Appendix C. Representation and calling convention (normative for the C backend)](#appendix-c-representation-and-calling-convention-normative-for-the-c-backend)
- [Appendix D. Grammar sketch (informative)](#appendix-d-grammar-sketch-informative)
- [Appendix E. Implementation notes (informative)](#appendix-e-implementation-notes-informative)
This working specification defines Goose's syntax, semantics, and compilation
model. It provides the detail needed to implement the language, without the
formal structure of an ISO specification. Appendix B lists unresolved items.
A readable account of the same rules, with an example for each, is
[the language reference](reference.md).
Priorities, in order: **Speed, Safety, Expressiveness.**
Safety here means: no out-of-bounds access, no access to memory at a type
other than the one it was written with, no dangling references into freed or
retyped memory. It does *not* mean Rust-style aliasing control; type-safe
reuse of an element (reading a valid but logically stale value) is permitted
where explicitly noted.
---
## 1. Core concepts
### 1.1 Size classes
Every type belongs to exactly one *size class*:
* **fixed** — byte size is a compile-time constant (after monomorphization).
* **variable** — size is chosen at construction time and never changes
afterwards.
* **resizable** — size may change after construction. Two flavors:
*grow-only* and *grow-shrink*.
A compound type's class is the max over its parts (fixed < variable <
resizable), subject to the placement rules in §3.4.
Additionally, a type is **flat** if it contains no references, slices, or
relative references at any depth. Only flat values may cross thread
boundaries (§11.2).
### 1.2 Memory model
A Goose program's memory consists of:
* The **native call stack** (and registers) — holds fixed-size locals and
temporaries whenever possible, plus variable-size locals via `alloca`-style
allocation when profitable.
* **N data stacks** — large virtual-address-space reservations (reserved
up front, committed on use). N is computed statically by the compiler
(§10.3). Each data stack is a bump pointer. Growth never moves memory;
references into a data stack are stable for the life of the data beneath
them.
* **Static data** — string literals and `const` globals with compile-time
initializers. This read-only storage is shared by all program instances
(§11.2).
There is no general-purpose heap and no `malloc`. All dynamic allocation is
expressed as values living on data stacks, owned by locals (or globals).
(The runtime itself may allocate internally for thread queues, §11.2.)
Rewinding a data stack makes its storage reusable, while normally keeping
its pages committed. After a large workload, `trim_memory()` from `os`
can ask the operating system to discard unused complete pages of the
calling thread program's stacks. This preserves all live values and the
virtual reservations; subsequent growth remains valid.
### 1.3 The stack invariant
For each data stack:
1. Values are pushed in lifetime order and popped en masse when the owning
scope exits. Lifetimes on one stack are strictly nested.
2. At most one *resizable* value is live per stack, and for its entire
lifetime it is the topmost value on that stack. The region from a live
resizable's first element byte to the stack's current top belongs
exclusively to its elements; its own length header lives *below* its
elements or outside the stack entirely (Appendix C). Growth is a pointer
bump, and no capacity checks are emitted: address space is pre-reserved,
and guard pages catch pathological overflow and abort safely.
3. Two simultaneously-live resizable values never share a stack. Locals with
disjoint (sequential) lifetimes reuse stacks.
4. A *variable* value may be buried under later allocations on the same stack
once its construction completes. While a value is under construction on
stack S, nothing else may allocate on S, nor may the resizable value it is
being added to shrink. Nested variable parts are constructed in order, as
part of the same construction.
5. Fixed-size values are placed on the native stack / in registers whenever
possible; the length header of an outermost resizable local likewise
lives in the owning frame, not on the data stack (Appendix C).
The compiler proves all of this statically; there is no runtime bookkeeping
beyond the bump pointers themselves.
---
## 2. Lexical structure and syntax style
The syntax follows C and Rust: `{}` blocks, `//` and `/* */` comments (block comments
nest), semicolon-terminated statements, postfix type annotations (`x: T`).
Identifiers `[A-Za-z_][A-Za-z0-9_]*`. A UTF-8 byte-order mark at the start of
a source file is ignored.
Literals:
* Integer: decimal and `0x` hex; the full unsigned 64-bit range is accepted
(a value above `i64.max` is a `u64` constant, §6.2). Character
literals `'a'` are integer constants.
* Float: with `.` and/or a decimal exponent (`1.5`, `2.5e-3`); C99-style hex
floats with a mandatory binary exponent (`0x1.8p3`). A `.` starts a
fraction only when a digit follows, so `1..2` lexes as a range.
* String `"..."`, with escapes `\n \t \r \0 \\ \" \' \xNN` (two hex digits).
* Raw string `"""..."""`: no escapes, and the next `"""` ends it. On one
line, its text is what lies between the delimiters (`"""a "quoted"
C:\path"""`). For a multiline string, the opening `"""` must end its line. The closing
`"""` must start its line after any indentation. The content is the lines
between them, joined by `\n`, with the closing delimiter's indentation
removed from each line. Every content line must start with that indentation;
a whitespace-only line may be shorter and then becomes empty. The line
breaks immediately after the opening delimiter and before the closing
delimiter are excluded. To include a final newline, add an empty content
line before the closing delimiter. Source line endings are normalized to
`\n`.
```goose
let usage = """
usage: goose [options] file.goose
-o file.c write C instead of running it
"""; // "usage: goose [options] file.goose\n -o file.c write C ..."
```
* `null` — the empty value of any optional type `T?` (§3.8).
* `self` — inside a struct or variant literal, the value that literal is
constructing; it exists to initialize non-optional relative-reference
fields (§3.9, §4.2).
The language is **expression-oriented**: `if`, `match`, `block` and a bare
`{ … }` block are expressions; a block's value is its trailing expression.
Assignment and `++`/`--` are *statements*, not expressions. A trailing
construct that cannot supply a value is a statement rather than the block's
value. Such constructs are an `if` without a final `else` (including an
`else if` chain) or with a branch that cannot supply a value, a `match`
with an arm that cannot, a `loop` none of whose `break`s carries a value
(§6.4), and a block or scope ending in one of those. A function whose
result type is inferred, and a function value's body, may therefore end in
one of them and produce nothing.
**Statement termination.** Expression statements end with `;`. A statement
that *is* a block-ended construct needs no `;`: `if`, `match`, `block`, the
loops, `guard … else { }`, a nested `fn`, or a call with a trailing function
block (§7.6). Such a statement ends at its closing `}`. Operators never
continue it (`foo(1) { it }` followed by `-x;` is two statements), and
using one as an *operand* requires parens
(`let v = (if c { 1 } else { 2 }) + 3;`). A redundant `;` after a block is
harmless. Trailing commas are allowed in every comma-separated bracketed
list: struct/array literals, call arguments, parameter and generic lists,
field lists, match arms.
Type syntax is postfix throughout: `T[k]` array of T, `T&` reference to T,
`T[:]` slice of T, `T?` optional T, `T&` relative reference,
`T&` relative reference measured from a named pool, `Shape..`
variable-mode ADT. The only prefix, `const`, qualifies the first reference
or slice built on the base type: `const T&` and `const T[:]` are a
reference and a slice through which the pointee, or the elements, cannot be
written (§9.5). To qualify an outer reference or slice, parenthesize the
inner type, as in `const (u8[:])&`.
`ns::name` names a declaration of namespace `ns` (§11.1) wherever a
declaration can be named: a type, a call, a global, a `return … from`
target, or a pool. `::name` explicitly names a global declaration. The `::`
token is distinct from `.`, so `image::Shape.Circle` is the variant of a
namespaced enum.
Evaluation order is left-to-right everywhere: operands, arguments, and field
initializers. Named struct literals keep field initializers in construction
order by requiring declaration order (§4.2). Overlapping copies have
memmove semantics.
An earlier by-value operand is read before a later operand executes; a
reference or slice retains its address or view, not a snapshot of its
contents. Indexing and slicing sample the receiver's region and length
before their index or bounds run. Assignment resolves the destination
first; compound assignment also reads its old value before the right-hand
side. Whole-resizable assignment clears before constructing (§4.4).
A grammar sketch and precedence table are in Appendix D.
---
## 3. Types
### 3.1 Scalars
* `i8 i16 i32 i64 u8 u16 u32 u64` — the integer types, two's-complement,
usable everywhere: fields, array elements, locals, parameters, returns,
and expression temporaries. Every integer operation computes at its
operands' exact type and width (§6.2), so a loop over `i32` data runs in
32-bit registers throughout, which lets backends vectorize it.
`i64` is the conventional default for indices, sizes, and counts (it is
what `.len`, integer literals, and the builtins produce); the other integer widths
are useful for compact data and kernels that require a particular width.
* `varint` — variable-length integer **storage type** (§3.6), the only
integer type restricted to fields and array elements. Reads decode to
`i64`, and it is writable only at construction.
* `f32 f64` — IEEE floats, likewise usable everywhere. `f32` arithmetic
stays 32-bit; `f32` widens to `f64` implicitly (§6.3), never the reverse.
Every integer converts to either float type implicitly, so where a float
is expected `2` works as well as `2.0`. Float literals adapt to either
type, and so does an expression whose float-ness comes only from float
literals and integers: `n * 0.5` computes in 32 bits where an `f32` is
expected. Where nothing asks for a type such an expression is an `f64`,
and so is an untyped `let` bound to one (§6.3).
* `bool` — 1-byte storage, values `true`/`false`. Produced by comparisons;
required by `if`/`while` conditions (no int-to-bool coercion).
Integer literals are *constants without a committed type*. They adapt to any
integer type that can hold their value: `let x: u8 = 255;` is valid, but `=
256` is a compile error. Without a type constraint, they are `i64`, or `u64`
for values above `i64.max`. A **named constant** is a `let`
or `const` global with no written type whose initializer is an integer
constant expression (`let SZ = 15;`, `let B = SZ * 2;`, `let NEG = -3;`).
It is also a constant without a committed type: each use of it adapts as
its value would if written there, both at a destination (`D { len: SZ }`
with `len: u8`) and as an operand (`b + SZ` with `b: u8` is an 8-bit add).
Where nothing constrains it, it is an `i64`. A global with a written type
(`let SZ: i64 = 15;`), a local `let` and a `var` are values of their type.
(An untyped float `let`, local or global, adapts too, §6.3.) A use that
adapts a named constant relies on the constant keeping its initializer's
value, and a writable reference to it could change that value. So, as for
a compile-time size (§11.1), such a reference is an error, whether it
comes before or after the use. A division by a named constant that is zero
is left to run time, rather than rejected as a constant division by zero
(§6.2). All other conversions are
**implicit when provably value-preserving, and need an explicit cast
otherwise**, with one exception: an integer becomes a float implicitly,
rounding as float arithmetic does (§6.3).
### 3.2 Structs
```goose
struct X { a: i8[3], b: i32, c: i64 = 0 }
```
* Packed by default: a struct of `i8[3]` + `i32` is 7 bytes. Unaligned
access is assumed cheap; there is no automatic padding.
* `pad n` inserts n bytes explicitly; bare `pad` aligns the *next* real
field to its scalar storage width, for compatibility with foreign layouts:
`struct X { a: i8[3], pad, b: i32 }`. Pad bytes are never read and are
ignored by `==`. Plain references and slices use pointer alignment
(8 in the C backend); composite fields use alignment 1. No automatic
tail padding is added. In variable-class layouts (where offsets are dynamic)
`pad n` still inserts n bytes, but bare `pad` has no defined alignment to
aim for and inserts nothing.
* Fields are mutable by default; `let` before a field name means the field
is not assigned after construction (§4.4), and `const f: T` is `let f:
const T`: its contents are read-only too (§9.5).
* A field may declare a default value (`c: i64 = 0` above, `name: u8[] =
[]`), and constructors may then omit that field (§4.2). A literal ending
in `..` may also omit a field without a default; that field takes its
type's default value (for an array, the empty array). Defaults resolve in
the declaration's namespace and generic environment, can name globals,
and cannot name the constructor's locals or sibling fields. A used
default executes at each construction, in field order among explicit
initializers; it is not a cached value. Its effects obey the same rules
as an explicit initializer's.
* A default may not lead to a construction that takes the same default
again, in any instantiation of its type. Such a construction can be in
the default's own expression (`S { .. }` in
`struct S { a: i64 = 1, d: i64 = S { .. }.a }`, a `default()`, or a
slice pool's allocation, §5.4). It can also be reached through the
defaults those constructions take, through the parameter defaults of the
calls the default makes (§7.1), or through the blocks and functions
written in the default. Such a default would run without end. It is an
error even where a condition would end it at run time, since every
construction checks the defaults it takes. If the construction that
takes the default again is in the body of a function declared outside
the default, which the default calls, it makes that function recursive
instead (§7.8).
* Layout is declaration order; variable/resizable fields obey §3.4.
Struct and enum declarations introduce **nominal** types. `type Name = T;`
declares an alias. Generic structs: `struct Pair { a: T, b: T }`,
monomorphized like functions.
A generic type's instantiations must be finite, as a function's
specializations must be (§7.7). Each instantiation checks its fields' types
(including a variant's payload) and instantiates the types they name.
Fields that name the declaration again, directly or through other
declarations, with a larger type argument each round would therefore
instantiate without end: `struct P { v: T, next: P? }` needs a
`P`, which needs a `P`, and so on, whether or not a program
ever follows `next`. As for polymorphic recursion (§7.8), the compiler
rejects an instantiation that would make a chain of more than 16
instantiations of one declaration, each needed by the one before, and the
error names the first few. Fields that name the declaration with the same
arguments (`struct L { v: T, next: L? }`), or with arguments that stop
changing, reach an instantiation that was already made instead. So only a
type that nests one declaration more than 16 deep in its own arguments
reaches the limit.
### 3.3 The array family
All array types share element type `T` and differ in how their size behaves
and is stored. In all of them, the representation is `[metadata][element
data...]`, inline, packed (exact layouts in Appendix C).
| Type | Class | Stored metadata | Notes |
|---|---|---|---|
| `T[k]` | fixed | none | `k` a compile-time constant expression (literals, named constants, a global's fixed `.len`/`.cap`, arithmetic) |
| `T[]` | variable | length (`u32` default) | size chosen at construction |
| `T[u8]` etc. | variable | length of given unsigned int type | explicit length field type: `u8`–`u64` only |
| `T[varint]` | variable | varint length | most compact |
| `T[..k]` | fixed | length only (smallest int type fitting `k`) | capacity `k` is static (constant expression); in-place grow/shrink up to `k` |
| `T[..]` | variable | capacity + length (`u32` default) | capacity chosen at construction; in-place grow/shrink up to it |
| `T[>..]` | resizable (grow-only) | length | element region tops a data stack; grow = bump |
| `T[>..<]` | resizable (grow-shrink) | length | as above, may also shrink |
A size or capacity `k` is fixed at compile time, as is the count `n` of a
fill literal `[v; n]` (§4.2). A name in a size or count is a named constant
(a `let` or `const` global, §11.1), and resolves as any name does,
lexically first. Such an expression may also use `X.len` of a global `X`
whose declaration fixes its length, and `X.cap` of a global whose
declaration fixes its capacity (`const names = ["a", "bc"]; var counts:
i64[names.len];`). A written type `T[k]` fixes the length, and `T[..k]`
the capacity. With no written type, an array, fill or string literal
initializer fixes the length, and an initializer naming another such
global fixes what that global's declaration fixes. Where `X`'s type also
fixes the length or capacity (a written type, or the `T[k]` an array or
fill literal gives), nothing can change it, and `X` may be a `var`. Where
the type does not, as a string literal's `const u8[:]` does not (§3.7), the
length is taken as a named constant's value is: `X` may not be a `var`, and
may not be bound to a writable reference (§11.1), which a `let` holding a
literal's slice never is (§9.5). `X.len` and `X.cap` are `i64` values, as
any `.len` is, and do not adapt as a literal does (§3.1).
A local with the same name as such a global hides the global there and is
not a constant: a parameter, a local variable, or a local around a nested
function or block. So do a type parameter and a nested function of that
name. A size naming any of these is an error, as a match pattern naming
one is (§8.1); a qualified name (`::K`, `ns::K`) reaches the global past
it. A size is also
an error where it names one of a function's or a block's parameters or
type parameters in that function's or block's parameter and result types,
or one of a generic struct's or enum's type parameters in its field
types. A type parameter stands for a type, and there are no size
parameters (`fn f(a: i64[N])` is an error). The names in a type
alias's sizes resolve where the alias is declared.
Indexing is bounds-checked: against `k` for fixed arrays (with checks
statically elided where provable), and against the current length for all
others.
A stored length must hold the actual element count without truncation.
For example, `T[u8]` can hold at most 255 elements. A construction exceeding
its length field's range must be rejected when known statically or abort
when discovered at runtime, including when adapting another array, a call
result or a string. It must never produce an image whose metadata and
element region disagree.
Element restrictions:
* Elements of `T[>..<]`, `T[..k]`, `T[..]` must be **fixed-size** (shrink or
reuse of storage must never change the type layout of a location).
* Elements of `T[]` and `T[>..]` may be fixed or **variable** (e.g. an array
of strings, all flat). They may not be resizable.
* An array with variable-size elements is *sequential*: it cannot be indexed
randomly, only iterated / advanced element-by-element (offsets are data-
dependent). It also has no `pop`-like operations (the start of the last
element is not findable). Random access needs an index that the program
keeps itself: a second array of references to the elements, stored as
they are pushed (`T&`, 4 bytes each, where the array is a
global pool, §3.9).
Growth operations (`push`, `append`, …) exist only on resizable arrays and on
limited arrays (`[..k]`, `[..]`) up to capacity (exceeding capacity aborts).
Shrink operations (`pop`, `pop_n`, `resize` downward, `clear`) exist on
limited arrays anywhere, on `[>..<]` wherever no live variable refers into it
(§5.2), and on a grow-only `[>..]` exactly where the compiler can see that
no reference or slice into it is live (§5.1). They abort when they would
shrink below empty (`pop` on an empty array, `pop_n` of more elements than
the array holds, `resize` to a negative length).
Built-in members, which exist as the rules above allow:
* `.len` (always), which returns `i64`.
* `.cap` (limited arrays).
* `.push(v)`, which returns a reference to the new element on resizable and
limited arrays. This is the idiomatic way to link up just-built data.
* `.append(src)`, where src is an array or slice of the element type. Its
elements are copied: the references they hold must outlive the array
(§9.2), and none may be self-relative (§3.9). An array literal given as
src is instead built as a run of such elements (§4.2).
* `.pop()`.
* `.pop_n(n)`, which drops the last `n` elements at once and returns
nothing. A negative `n` aborts.
* `.resize(n, v)`, which grows with fill value `v` (copied into every slot
added) or shrinks.
* `.resize(n)`, which only shrinks.
* `.clear()`.
* `.index_of(r) -> i64` (fixed, limited and resizable arrays of fixed-size
elements): the index of the element `r` refers to,
`(addr − base) / elemsize`. `r` must be rooted at the array *exactly*
(§9.2), so the division is exact and the result is in range without a
runtime check. A root names a variable, not a part of it. So where the
array is a field or an element of what its root holds, the reference
counts as rooted at the array only if that storage holds the element type
nowhere else: not in another field or element, nor inside an element. A
parameter's storage is checked at each call (§10.2). A reference rooted
elsewhere is a compile error.
Growth always supplies element values, so no operation can expose
uninitialized slots (§5.3). Per UFCS these are ordinary functions:
`a.push(v)` is `push(a, v)`.
### 3.4 Placement rules (what may contain what)
* **fixed** values: anywhere (locals, globals, fields at any position, array
elements, ADT payloads, params, returns).
* **variable** values: locals/globals, params, returns, ADT payloads, array
elements (making the array sequential), and struct fields at *any* position.
A field placed after a variable-size field is reached by dynamic offset
computation (fine when iterating, costly for random access; this is a
performance note, not a rule).
* **resizable** values: locals/globals, by-value params, returns, and the
**final field** of a struct. This applies recursively: a struct ending in
a resizable is itself resizable and may only appear where resizables may.
A resizable may also be the payload of a variable ADT (making that ADT
resizable, with the same tail rules). It is never an array element.
* **zero-size** values, although they are fixed: zero-length arrays (`T[0]`,
and `T[..0]` of capacity 0) and variant types with no fields
(`Shape.Point`, or a variant with only pads that insert nothing, §3.2),
whose payload takes no bytes behind its ADT's tag. They appear only as
values of their own (locals/globals, params, returns, and behind
references), never as a struct field, a payload field, or the element
type of an array or slice (including one an array literal infers). C has
no empty arrays or structs, so the C backend gives each zero-size value an
element slot or a byte (Appendix C.2). Inside another value, that slot or
byte would take bytes that the value's layout does not count.
At most one resizable per struct (the tail). (Future extension: two
resizables with memmove-on-insert semantics, opt-in.)
No type contains itself by value, either directly or through what it
contains (fields, payloads in either ADT mode, variant types, array
elements), since it would then be part of itself. A type may refer to
itself, since a reference or a slice is fixed-size whatever it points at:
an ADT's payload may link to its own ADT or one of its variants, and a type
behind a reference may contain the type holding the reference
(`enum List { Nil, Cons { v: i64, next: List? } }`, or `struct A { b: B? }`
with `struct B { a: A }`).
### 3.5 Algebraic data types
```goose
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
Point,
}
```
ADTs are the only dynamic-polymorphism mechanism in the language: there is
no inheritance, and there are no traits or vtables. An ADT is represented
as an integer tag (the smallest storage type that fits the variant count,
default `u8`) followed by the payload.
Every ADT *type* can be used in two modes, chosen at the use site by the
containing declaration:
* **fixed mode** (`x: Shape`) — usable only if all payloads are fixed-size
and hold no self-relative references (§3.9). Size = tag + max payload
size, padded to the largest variant (padding never read, ignored by
`==`). A fixed-mode value may be **overwritten in place by a different
variant**, but interior references into its payload may never be
created. Its payloads therefore bind only by value (§8.1, §8.2), and a
copy does not keep self-relative references (`in pool` references copy
fine).
* **variable mode** (`x: Shape..`) — size = tag + the actual variant's
payload. The class is variable (or resizable, if the stored variant's
payload contains a resizable tail). A variable-mode value may have
interior references into the payload, but its variant may **never be
replaced in place**, except by a whole assignment of a resizable ADT
(§4.4). A resizable ADT therefore binds its payloads only by value, as
fixed mode does (§8.1, §8.2). So a payload of a resizable ADT that is not
itself resizable holds no self-relative references, since a copy does
not keep those (§3.9); plain or `in pool` references copy fine.
Allowing either variant replacement or interior references, but not both,
preserves the soundness of existential types (Grossman, "Existential Types for Imperative
Languages").
Payload-less ADTs are ordinary C-like enums and are fixed, 1 byte by default.
Variant types (`Shape.Circle`) are themselves nominal struct-like types
(§8.2).
### 3.6 varint fields
`varint` is **LEB128**: little-endian base-128, 7 payload bits per byte,
high bit = continuation; 1–10 bytes; full 64-bit range. Stored encodings
are canonical: the shortest encoding of the unsigned or zigzag-transformed
value. A verified byte image must preserve that invariant (§12).
LEB was chosen because this type is optimized for values that are usually very small but
have occasional outliers, where it outperformed other formats (measurements
summarized in Appendix B, Resolved, item 1).
There is one `varint` type, with two encodings that depend on its position.
The difference is user-visible whenever Goose data is serialized directly:
* As an array **length field type** (`T[varint]`): unsigned ULEB128.
* Everywhere else (struct fields, ADT payloads, self-relative offsets):
**signed**, zigzag-transformed (`(v << 1) ^ (v >> 63)`), so small
negatives are as compact as small positives. A pool-relative offset (§3.9)
is unsigned, so it is ULEB128 like a length. The transform sits on the
value path, not the length/advance path, so it costs no decode latency.
Because the two encodings differ, varint values are never copied byte-wise
between contexts: any varint-to-varint construction (e.g. a struct varint
field initialized from `arr.len`) goes through `i64`: decode, then
re-encode.
Restrictions: a `varint` field/element is written only at construction of
its containing value; changing it means reconstructing the container. Reads
decode to `i64`; construction stores accept any integer type except `u64`
(a varint holds exactly the `i64` value range). A struct containing
`varint` fields is variable-class.
References to `varint` fields are always read-only (§3.8). Such a field is
still storage of type `varint`. A `varint&` destination binds it without
`&`, as a reference destination binds any lvalue of its pointee type
(§4.1). An `i64&` does not bind it, although it reads as an `i64`. That
`i64` is not storage, so a format overload taking an `i64` by reference is
given it in a read-only temporary (§3.7).
### 3.7 Strings
There is no built-in string type. A "string" is any array-family type with
element `u8`: `u8[]` (flat string with a fixed length), `u8[varint]`
(compact), `u8[>..]` (string builder), etc. String literals are static
constant `u8` data. They are implicitly copyable into any of these
representations and usable directly as slices of type `const u8[:]`
(§9.5). That type makes them read-only, so a literal is stored only in a
slot declared `const`. No encoding is enforced; UTF-8 is a library-level
convention.
**Text.** Every scalar, `bool`, and `u8` array or slice has a text form:
* integers in decimal;
* floats in the shortest form that reads back to the same value of their own
type. An `f32` 0.1 prints as `0.1`, without the extra digits of its widened
`f64` value. Whole numbers get `.0` so they still read as floats (`1.0`,
`-0.0`, `2147483600.0`); exponent forms such as `1e+20` keep that form.
Infinities print as `inf` and `-inf`, and every NaN as `nan`;
* `true`/`false`;
* a `u8` array's bytes as they are.
Three builtins produce it. Each takes any number of arguments and inserts
nothing between them:
* `print(a, b, …)` writes the forms to standard output followed by a
newline, as one write, so lines printed by different threads (§11) never
interleave.
* `format(out, a, b, …)` appends them to `out`, which can be any growable
`u8` array.
* `str(a, b, …)` builds a fresh `u8[>..]` of them. Like any resizable
result, it is constructed at its destination (§7.3), so
`words.push(str("item", i))` writes directly into the element.
A character literal is an integer (§2), so `str('x')` is `"120"`; a byte is
appended with `push`.
Every other value type renders structurally, so `print(v)` shows any value:
an array or slice of other elements as `[1, 2, 3]`, a struct or ADT variant
as its positional literal with the type name (`vec3 { 1.0, 2.0, 3.0 }`,
`Circle { 1.5 }`, a payload-less variant as `Shape.Dot`), a reference as
its pointee and a null optional as `null`. A `u8` array nested inside an
aggregate is quoted and escaped. References are followed as deep as they
go, including into values of the same type as the one holding them. A
value whose references lead back into itself therefore never finishes
rendering: its thread runs out of native stack, which is a safe abort
(§9.3).
A user overload `fn format(out: u8[>..]&, v: T)` (taking `T` by value or by
reference) is used instead to render a `T`, wherever a `T` occurs in an
argument, so a type can choose its own text once for all three builtins.
A `T` that is not fixed-size is never copied implicitly (§4.1), so its
overload takes it by reference. An overload that takes the `T` by reference is given the storage the `T`
lies in, as a reference parameter is (§4.1): the variable, field or element
holding it, or what a reference argument names, and it is as writable as
that storage is (§9.5). A `T` that is not storage lies in a read-only
temporary. That is the case for a call's, an operator's or a cast's result,
a literal, a `.len` or `.cap`, and the value of an `if`, `match` or block,
which is a copy of what the branch taken gives, even where that names a
variable (§4.1). The overload is looked up in the namespace `T` is
declared in, then globally (§11.1): rendering follows the type, not the
namespace of the code printing it. A type that reaches itself through
references renders structurally only where no overload renders any part of
it, at any depth. Otherwise the type needs an overload of its own, which may
render its parts through their overloads.
The three builtins evaluate and render their arguments in order, evaluating
each one just before rendering its text. They call the overload in the
middle of rendering the argument that holds the `T`. So neither the
variables named by the arguments after that one, nor the rest of that
argument, may refer into what the overload shrinks (§5.1).
An array's elements, and a variable-mode ADT's payload (§3.5), are rendered
where they lie, using the count and the variant read when their rendering
begins. So an overload called for one of their parts may not shrink the
storage they lie in. Such a shrink either frees the elements or, as a whole
assignment does (§4.4), rebuilds every variable-size part of the value
holding them. A reference to a whole resizable value is no exception.
A slice's elements are rendered from the view the slice had when their
rendering began, and a value behind a reference from where the reference
pointed at that time. An overload that re-points the slice or rebinds the
reference in the meantime does not change how the rest is rendered (§2). A
fixed-mode ADT's payload is rendered from a copy taken once its tag is
read, since an overload may overwrite the ADT with another variant and
nothing may refer into its payload (§3.5). The rest of the payload is
therefore still the variant the rendering began with, and an overload that
takes a part of it by reference is given that part of the copy, read-only.
The payload renders as its variant's literal even where there is an
overload for the variant type; that overload renders values of the variant
type.
### 3.8 References
`T&` is a reference to a `T`. It is one machine address, with no pointer
arithmetic and no casts, and it never dangles.
**References are transparent** (like C++ references): an expression that
denotes a reference behaves as its pointee in every value context. Reads,
arithmetic, comparisons (`r1 == r2` compares the pointees), passing to a
by-value parameter, and `print(r)` all operate on the target. There is no
dereference operator; the load is implicit (`copy(r)` names a copy of the
pointee where §4.1 asks for one).
* Created with `&lvalue`. Lvalues are: local/global names, fields, indexed
elements, dereferenced references, and (for reference creation) variable
elements reached by iteration. `&` of an rvalue (temporary) is an error.
`&` of a location that itself holds a reference yields the *stored*
reference (there are no references to references).
* Writes: `r = v` (and `r += v`, `r++`, …) write the pointee, subject to
writability (§9.5) and the target's own rules. A `const T&` does not allow
writes to its pointee.
* Rebinding: the special assignment `r .= &x` updates the reference *value*
itself, rather than writing through it. `.=` applies to any
reference-typed location: variables (subject to their `let`/`var`),
fields and elements. On non-reference locations `.=` is an error, except
in generic code, where it assigns as `=` does. Generic code here means the
body of a function with type parameters or untyped parameters, and the
nested functions and function values written in one. Generic code thus
stores a whole value of a type parameter's type with `.=`: where the
parameter is bound to a reference type, `.=` moves the reference itself,
while `=` would write through it. It keeps such a value in a local of
that type, where an inferred `let` would copy the pointee (below). For
example, std's `swap_at` is
`let t: T = xs[i]; xs[i] .= xs[j]; xs[j] .= t;`. It swaps the elements of
an array of references, including relative ones (the local holds the
plain reference, §3.9), as it swaps those of any other array. The
declaration form `let r .= e;` / `var r .= e;` binds `r` to `e` by
reference whatever `e` is: an lvalue of any size class
(`var cur .= pool[0];` names the element, without `&`), a
reference-returning call (`let e .= pool.push(v);`), or a narrowed
optional. This is how a reference variable is declared without writing
its type: the `.=` says the binding is a reference, as it does in the
rebind. `let` on a reference variable means it does not rebind, not that
its pointee is const: `let r .= xs[i]; r = 5;` writes the element (§9.5).
* Identity: `r1 .== r2` and `r1 .!= r2` compare the references themselves,
that is, whether they have the same address (`==` compares the pointees).
Both operands are references to the same pointee type, or `null` for an
optional. Storage (a variable, field or element) is taken by reference,
as `.=` takes it, so `n .== pool[head]` asks whether `n` names that
element.
* Binding contexts keep the reference rather than loading through it. An
initializer, argument or field whose *declared type* is a reference type
binds the reference value, and binds an lvalue of the pointee type by
reference without `&` (§4.1; writing the `&` warns). Where a *variable's*
type is inferred (`let x = r;`), a reference to a fixed-size value decays
to a copy of the pointee. The exception is an explicit `&lvalue`
initializer, which infers the reference type; a construct choosing one,
such as `if c { &a } else { &b }`, does not, and its value is a copy
(§4.1). A non-fixed lvalue, by contrast, binds by reference
(`let w = words[0];` names the element), and so does a reference to a
non-fixed value (`let a = c.args;`, with `args: u8[>..]&`, binds what the
field refers to, as `.=` does). To bind a reference-returning call to a
fixed-size value, use `.=` (`let e .= pool.push(v);`) or annotate
(`let e: T& = pool.push(v);`). A value holding self-relative references
cannot be copied at all (§3.9). A function whose result type is inferred
takes it from a return in the same way (§7.1). An untyped parameter is an
anonymous type variable and binds the argument's exact type, reference or
not, exactly as an explicit `` does (§7.7). For a fixed value, `f(&x)`
hands `f` a reference and `f(x)` a copy; a non-fixed lvalue is a
reference either way.
* References to `varint` fields are always read-only (varints are written
only at construction, §3.6).
**Optionals.** References are non-nullable by default. `T?` is an *optional
T*: it is represented as a nullable reference to T (null = address 0, so it
takes no extra space), with all reference semantics and restrictions
(roots, lifetimes, writability). This composes with any type: pass `i64?`
for an optional integer. Applied to a type that is already a reference,
`?` makes that reference nullable (`T&?` ≡ `T?`). The same holds for a type
argument that is a reference: `T?` of `Node&` is `Node?`, and of
`Node&` it is the optional relative reference. `T&` of a
reference type argument is that reference itself, just as `&` of a
location holding a reference gives the stored reference (loaded, if
relative). The literal `null` is the empty value of any optional type; an
optional struct field with no declared default defaults to null. Optionals
are *not* transparent: access requires narrowing via
`if`/`while`/`guard`/`assert(r)`/`== null`/`!= null` tests. Flow typing:
inside the guarded region the optional *is* a `T&` in every respect. It is
transparent, can be written through, and is bound by `.=`
(`if n { let m .= n; … }`). A `while` condition narrows its body in the
same way even when the body rebinds the variable, since the test runs
again before each iteration (`while n { …; n .= n.next; }`). Elsewhere,
rebinding the variable ends the narrowing. `o .= &x` / `o .= null` rebind
an optional. A rebind to a plain reference narrows it, and a rebind to
anything that may be null un-narrows it.
**What references may point to.** Anything except the interior of a
fixed-mode ADT payload (§3.5). A reference into a grow-shrink array `[>..<]`
is held only in a variable, and must no longer be live when the array next
shrinks (§5.2).
References into a grow-only resizable `[>..]` remain valid for as long as
they can be named: grown memory never moves, and the array shrinks only
where the compiler can see that no such reference or slice is live (§5.1).
References into limited arrays `[..k]`/`[..]` are also allowed and stay
type-valid across pop/push reuse (§5.3).
### 3.9 Relative references
A relative reference is a storage form of reference, stored as a narrow
offset instead of an address. It is written `T&`, `T&`,
`T&`, `T&` or `T&`; widths are written as unsigned types
(or `varint`), like array length field types. It comes in two forms,
differing only in what the offset is measured from: **self-relative**, the
default, measured from the offset field itself; and **pool-relative**,
written `T&`, measured from a named pool's base.
Both forms can be made optional, written `T&?`, which reserves offset 0
for null. A pool-relative offset is biased by one. A self-relative field can
share its address with an enclosing value, so a non-null store must not
encode as the optional's null. Such a store aborts, and a collision that is
known statically may be rejected during compilation. A non-optional
self-relative reference has no null sentinel: offset 0 denotes the address
of its own field, as with `self` in a value's first field. Null is
therefore representable in any relative location, whatever the location's
root, and a value known to be null stores into an optional relative slot
without a root check of its own. The values known to be null are:
* the `null` literal;
* a variable bound to nothing but null so far (§9.5), a parameter given
null, and a call whose every `return` gives null, none of which has a
root;
* an optional reference whose only root is static data, where static data
holds nothing but null: a writable one (§9.5), or one whose pointee type
no literal supplies, since a read-only `u8` reference may point into a
string literal.
As one alternative of a merged value (§9.2), a null names no array:
`if c { null } else { &pool[i] }` is a link into `pool`. So a
sentinel-ended chain does not force plain links on the whole structure.
**Self-relative.** Constrained to point within the *same enclosing
array/pool* as the location storing it.
* Stored as a signed offset of the given width (`varint` offsets use the
signed zigzag encoding, §3.6). Loading one yields an ordinary `T&` (base =
address of the offset field itself). Storing one requires the compiler to
see that both the reference and the destination location derive from the
same root array: the two roots must be the same variable *and* both exact
(§9.2), since a root that only bounds a lifetime does not say which array
the offset would span. A reference read out of a container qualifies
exactly when the read-back rule of §9.5 names a single candidate.
Otherwise, the error names the candidates the rule could not choose
between. The offset is range-checked at the store (aborting on overflow
of the width). Both ends lie in one root array, so the offset cannot
exceed that array's span. The check therefore exists only where a root
can be wider than the width's signed range. A root on a data stack spans
at most the reservation (§10.4), so a reservation of 2 GB or less needs no
check for `u32`, and one of 32 KB or less needs none for `u16`.
Varint-width relative references are written only at construction, like
varint fields (re-encoding could change the byte length); fixed widths may
be re-stored with `.=`/`=`.
* Copying a *value that contains* self-relative references is a compile
error, since the copied offsets would still be measured from the source
location. This prohibits assignment from an lvalue, by-value arguments, by-value match
binders or case function parameters (§8.1, §8.2), and element copies. It
also prohibits `append` except with an array literal, by-value inputs to a
`format` overload (§3.7), `resize` fill values, and `copy(x)`. A `resize`
fill value is copied into every added slot, even when it is a literal.
What a reference names is copied in the same way wherever a
value destination meets the reference (§3.8, §4.1). For an `f` returning
`Node&`, `let n = f();` copies the node out of the array its links point
within. So do a `var`, an annotated declaration, a by-value argument, a
return (including one whose result type is inferred), an assignment, a
literal's field or element, a pushed element, and each reference result
of a multi-value binding, when given such a reference. Where a
declaration was likely meant to bind a reference, the error suggests the
binding `let n .= f();` (§3.8). Nor may a link be read out of a copy
that a temporary holds, out of a popped element, or out of a control
construct's value, which copies the value of its branch even where that
names storage (§4.1). The fields or elements of such a value that hold
self-relative references are not read, sliced or iterated; a `match`
does not bind its payload by reference; and `print`, which renders a
reference as its pointee, does not render the value. A scalar field of
such a value reads as usual (`a.pop().v`).
Construct such values in
place, with literals. A field that a literal's `..` fills takes its
type's default, whose links are null and whose limited arrays are empty,
and is not a copy. Bind their match payloads by reference, and have case
functions take their variants by reference and `format` overloads take
them by reference. An ADT whose payloads hold self-relative references is
used in variable mode, whose payloads bind by reference where the ADT is
not resizable (§3.5).
A call's by-value result is not a copy either,
since it is built where it is received (§4.3, §7.3); this includes a
fixed-size aggregate and a trailing block's fresh result. Links to
existing elements require the receiving storage and those elements to
share an exact root, checked at every call, including forwarded and
recursive calls. An argument slot, discarded result, or operand temporary
does not inherit the enclosing result's root. A result relying on that
root cannot be a resize fill, and cannot undergo a representation
conversion that stages or moves its bytes; a variant may be constructed
behind its ADT's tag.
A function's named result (§7.3) is not a copy
either: it is a local that every return of the function gives, built at
the destination from its declaration. A named result that is a struct
ending in a resizable needs the fields before its resizable tail to be
free of self-relative references, and of anything that a self-relative
reference in the tail points at, since those fields reach the caller as
a copy (C.3). Nor is a default value (a literal's `..`, `default()`) a
copy: its links are nulls, or `self` where a field's default says so.
(TODO 16: track the region a relative reference ranges over, so provably
whole-region copies can be allowed.)
* Because they are position-independent, structures linked by self-relative
references can be serialized or mapped directly: `to_bytes(a)` writes an
array's image out, `bytes_of(a)` views it without copying, and
`from_bytes..]>(bytes)` verifies one back in (§12,
`design/serialization.md`). Without verification, the arriving bytes
would be the one place where a reference could enter the program
unproven (§9.4). Verification is also why a `bytes_of` view is never
writable: bytes written through it would be links the checker never
proved.
**Pool-relative (`in pool`).** `pool` names a *global* `var` (or `reusable
var`) of a grow-only resizable type (`[>..]`) whose storage can hold a `T`
by value: as an element, or as a by-value field of an element,
transitively (the candidate notion of §9.5). Anything else is a compile
error. For a local or parameter pool, the error points to the
self-relative form, since the pool is named where the field is declared,
and a local's name means nothing there.
* The pool is part of the type's identity: `Node&` is neither
`Node&` nor `Node&`. Loading either form yields an
ordinary `Node&`, and storing re-encodes.
* The stored value is `(target − base(pool)) + 1`, unsigned, so 0 is the
null of the optional form and a `uN` width covers a pool of up to
2^N − 1 bytes; `varint` is the unsigned LEB form (§3.6). Loading is
`base(pool) + stored − 1`, and the result is a `T&` rooted at `pool`
*exactly* (§9.2) whatever it was read out of.
* Storing requires the value's root to be exactly `pool`. Otherwise the
error names the value's root or, for an inexact read-back, the candidates
§9.5 could not choose between. The destination may be anywhere (another
global, a local, a parameter's pointee), since the offset does not depend
on where it is stored. This permits links between arrays: a slot array can
hold 4-byte links into the pool. Since the pool is global, its lifetime
satisfies the §9.2 store rule.
* A width bounds the *pool*, not the distance between the two ends, so the
store is range-checked exactly where the pool's reservation can exceed it
(§10.4): `u32` at a 2 GB reservation needs no check, and `u16` at any
realistic reservation does. `base(pool)` never moves: a grow-only
global's element region starts at its stack's reservation, and growth
only bumps the top.
* A value whose relative fields are *all* `in pool` copies like any other
(assignment, by-value arguments, element copies, `pop`): its offsets do
not depend on where it sits. A value mixing both forms does not.
* Whether a *parameter* points into `pool` is settled at each call site, so
a function that relinks (`fn front(head: Node&, n: Node&)`) is specialized
per pool like every other root class (§10.2) and needs nothing passed to
it.
**Values.** A relative reference is storage (a field, an element, or a
variable or parameter whose declaration writes the type or an alias of
it), never a value. Loading one yields the plain reference, and the plain
reference is what anything else receives. A function's result may not be
declared relative; declare the plain `T&` or `T?` instead, which a
relative slot receiving it encodes. A result that a type argument makes
relative, such as the `-> T` of `fn get(a: T[>..]&, i: i64)` over an
array of links, is the plain reference, as indexing gives it. So is a
variable or parameter that a type argument makes relative: `let t: T` and
`v: T` over an array of links hold the plain reference. This is how generic
code that moves elements through a `T` local (std's `swap_at`, §3.8) moves
links as it moves plain references. Each store back into an element
encodes the reference again, so a self-relative reference must still point
within that element's root, and an `in pool` reference into its pool. A
field or element of type `T` (`T[4]`, `struct Box { x: T }`) is a link.
`default()` of an optional relative reference is likewise the plain
null, at any width.
**`self`.** A non-optional relative reference has no null, so a value whose
links point back at itself (the sentinel of a circular list, the first node
of a pool) could not be constructed at all: there is nothing yet for the
links to point at. `self`, written as the entire initializer of such a
field in a struct or variant literal, denotes the value that literal is
constructing: `Node { key: -1, prev: self, next: self }`. It is legal only
there, and only when the field's type is a non-optional relative reference
whose pointee is the type of the value the literal constructs (for a
variant literal in either enum mode, §3.5, that is the enum, tag included).
`self` in a nested literal names the literal it is written in, never an
enclosing one. That type must not be resizable: an offset alone cannot
reach a resizable value's header. For the same reason, the same-root rule
keeps every *other* relative reference away from a resizable value. In a
self-relative field, the stored offset is minus the field's own byte offset
within the value, so it is the only relative reference whose meaning does
not depend on where the value is stored. In an `in pool` field, the stored
offset is the value's own offset in the pool, which only a literal being
built *inside* `pool` has: in a `push` or `append`, an
`alloc_index`/`alloc_ref`, or an element store into the pool. Anywhere
else, `self` in an `in pool` field is a compile error. This allows a
structure to use non-optional links throughout. A sentinel eliminates the
need for null links, and loading a non-optional relative reference requires
only an addition, without a null test.
**Which form.** Use self-relative references for position-independent
blobs, such as a compact tree that is saved, mapped or moved whole
(single-byte links to nearby nodes, see A.2), and for structures stored in
a local or parameter pool, which has no name to write. Use `in pool` for
relink-heavy structures in a global pool, where a store is a subtraction
from a base already in a register rather than from the field's own
address. Use it also for links *into* that pool from other arrays, which
self-relative references cannot express at all, and wherever the offset is
wanted as an index, since `&pool[i]` encodes as `i * sizeof(T) + 1` and
`index_of` (§3.3) reads it back.
### 3.10 Slices
`T[:]` is a reference plus an element count, referring to a contiguous run
of `T`s inside some root. Slices are the universal "process a range"
parameter type, unifying all array representations. Like references,
slices carry a root and participate fully in the lifetime system (§9),
which prevents dangling slices. They are the intended *read* path, and
mutation idiomatically goes through references, but writes through a slice
are legal when its provenance is writable (§9.5).
* Created by slicing any array-family value or slice: `a[x..y]` (x inclusive,
y exclusive; omit for 0 / len; `^k` means "len − k"), or allocated from a
slice pool (§5.4). An array, or a reference to an array, that reaches a
destination whose type is a slice of its element type implicitly becomes
a whole-array slice of that array itself, as `a[..]` would. This conversion
applies to parameters, though an exact-type overload takes
precedence. It also applies to annotated `let`/`var` declarations and
globals; assignments to slice variables, fields, or elements; struct or
variant literal fields and field defaults; and array literal elements. Other
destinations include `push`, `return`, a body's final expression, and a
branch whose enclosing construct has a slice value (§6.4). The slice is
`const` where the array is read-only (§9.5), and the
lifetime rules treat it as any other slice (§9.2):
`let s: const u8[:] = item.name;` views `item.name`, while
`let s: const u8[:] = make();` binds a view of a temporary and is an
error. (An array literal of variable-size elements is not such an array:
it has no temporary to be sliced, §4.2.)
* Slices of fixed-element arrays index and iterate, bounds-checked against
the slice's own length. Slices never grow; shrinking a slice (re-slicing)
is always safe.
* Slices of variable-element arrays iterate only (no indexing); the count is
an element count.
* Slices obey the same restrictions as interior references (§3.8): a slice
of a grow-shrink array is held only by variables and must no longer be
live when the array next shrinks (§5.2).
* A slice of a grow-only resizable taken before growth remains valid (it
does not include the new elements).
### 3.11 Index and bound types
An *index type* `uN in a` is an unsigned integer that is below `a.len`
wherever it can be observed. A *bound type* `uN upto a` is an unsigned
integer that is at most `a.len`: the end of a range. Indexing `a` with an
index of `a` needs no check, and an index stored in data keeps that
guarantee wherever it is copied to.
This is how an index loaded out of one array (a graph's edge targets, a
mesh's index buffer) is checked once, where it enters the program, rather
than wherever it is used (`docs/design/index_types.md`).
```goose
struct Node { lo: u32 upto edges, hi: u32 upto edges }
var nodes: Node[>..] = [];
type NodeId = u32 in nodes;
var edges: NodeId[>..] = [];
```
* `uN` is `u8`, `u16`, `u32` or `u64`. `a` names a global `var` (or
`reusable var`) of a grow-only type `T[>..]` with fixed-size elements,
resolved where the type is written, as a pool's name is (§3.9). Anything
else is a compile error: a local or parameter has no name where a type
is declared, any other array kind can shrink, and a sequential array
cannot be indexed.
* The array is part of the type's identity: `u32 in nodes` is neither
`u32`, `u16 in nodes`, `u32 upto nodes` nor `u32 in edges`. The value is
the plain integer; the C type is that of `uN`.
* An index type takes no postfix of its own: an array of them is
`(u32 in nodes)[>..]`, or `NodeId[>..]` through an alias. A `for`
binder's written type is followed by the loop's `in`, so an index type
written there is parenthesized too: `for w: (u32 in nodes) in ...`.
* **Making one.** `x as (uN in a)`, for `x` of any integer type, aborts in
every build unless `0 <= x < a.len`, with the error `a[x]` would give. It
is a bounds check, and the bounds-check pass elides it where it would
elide `a[x]` (§10.5). `x as (uN upto a)` is the same with
`x <= a.len`. A value that `uN` cannot hold then converts as any `as`
does (§6.3), and what it wraps to is still in range. `as!` cannot make an
index or bound type. Converting to a wider width of the same array's type
is implicit (`u16 in a` to `u32 in a`), and so is converting an index
type to the bound type of its array; the other directions take an `as`,
which checks. A constant is never of an index type and never adapts to
one, since nothing about `a.len` is known at compile time.
* **Loops.** `for i in lo..hi` whose bounds are both of index or bound types
of one array `a` gives `i` the index type of `a` at their joined width,
since `lo <= i < hi <= a.len`. A count loop `for i in n` with n of an
index or bound type does the same.
* **As a number.** A value converts implicitly to its plain integer type,
and from there as that type does (§6.3). Operators compute with the plain
value, so `w + 1`, `w << 3`, `-w` and `w & m` are plain integers.
Compound assignment and `++`/`--` on a place of an index type are errors.
* **Passing through.** Declarations without a written type, parameters,
results, fields, ADT payloads and elements carry the type, and a store
into any of these takes only a value of that type. A branch, `match` or
block value has an array's index or bound type where every branch has an
index or bound type of that array (at the widest width, and a bound type
if any branch is one). Otherwise it has the join of the plain integers
(`if c { w } else { 0 }` is a `u32`). A reference
or slice to index-typed storage is `(u32 in a)&` or `(u32 in a)[:]`,
never a `u32&` or `u32[:]`, which could write any `u32`. A type argument
may be an index type, and each instantiation is checked with it (§7.7).
* **No default.** An index type has no default value: `default()`, a
literal's `..`, a field default and a global without an initializer all
need one, and an empty array has no index at all.
* **Never shrunk.** An array an index or bound type names is never shrunk:
`pop`, `pop_n`, `resize`, `clear` and a whole assignment of it are errors
anywhere, directly or through a reference parameter, as is a shrink
through a reference that may only bound such an array's storage (§9.5).
Its length only grows, so a value once in range stays in range.
* **What stays out.** `format` and `to_bytes(a, out)` do not write into an
array of `u8 in x`, which is not a byte array. When `from_bytes` verifies
an image, it checks each index in the image against its array's length,
and rejects the image if any index is out of range. The arguments of
`qput`, `qget`, `qpoll`, `qlimit` and `thread_spawn` take no type holding
an index type: each thread program's copy of the array grows on its own
(§11.2). An `extern fn` or `export fn` signature holds none either, since
C code is not checked against the array.
---
## 4. Values, copying, and assignment
### 4.1 Everything is a value; passing by size class
Goose has **value semantics**: a variable, field or element owns its value
inline, and a destination (an assignment, an argument, a return, a push, a
field initializer) receives a value of its own. How a value gets there
depends on its size class (§1.1):
* **Fixed-size values connect by value.** Scalars, structs, fixed arrays,
references and slices are copied. Copying a reference or slice copies the
reference or slice itself, never the pointee. Large fixed values are
copied silently; there is no size threshold. A fixed array whose
destination is a slice of its element type is not copied but viewed, as
any array is (§3.10).
* **Non-fixed values are never copied implicitly.** A variable- or
resizable-class *lvalue* (a variable, field or element) binds by reference
when its destination has a reference or slice type, or is an untyped
parameter (§7.7), an un-annotated `let`/`var`, a `for` binding, or a
result whose type is inferred (§7.1). At a value-typed destination such an
lvalue is an error.
A value-typed destination takes an **rvalue** (a literal, a call's result,
a constructing expression, built in place per §4.3) or an explicit
**`copy(x)`**, which is a fresh copy of the stored value, constructed at
the destination like any other rvalue. A function's own local is *moved*
by `return`, or as its body's final expression (§7.3).
So `f(xs)` passes a `u8[][>..]` variable by reference to each of
`fn f(xs: u8[][:])`, `fn f(xs: u8[][>..]&)` and `fn f(xs)`. `out.push(w)`
needs to be written `out.push(copy(w))` when `w` names storage. `x.f(a)`
(§7.1) reads as `f(x, a)` whatever `x`'s size class. At an untyped
destination, a reference to a *fixed* value is still written `&x`
(`let r = &n;`, or equivalently `let r .= n;`, §3.8; `f(&n)` into an untyped
parameter). Wherever the destination's own type is a reference, the `&` is
redundant, and a redundant `&` is a warning.
These destinations give an `if`, `match`, `block`, `loop` or bare `{ }` no
destination type: an un-annotated `let`/`var`, a result whose type is
inferred (§7.1), an untyped parameter, an operand, or the value a `for`,
`[..]` or builtin works on. There, the construct's value is a copy of what
the branch taken gives, unless its branches join as a slice (§6.4). A
reference to a fixed-size value (`&x` included) is copied as its pointee
there, as it is wherever a reference meets a value (§3.8).
`let r = if c { &a } else { &b };` makes `r` a copy of `a` or `b`, so the
`&`s are redundant and warn. A branch naming non-fixed storage, or a
reference to it, is an error there, as it is at a value-typed destination;
`let x = if c { copy(a) } else { copy(b) };` writes the copies out
explicitly.
At a reference-typed destination (an annotated `let`/`var`, a reference
parameter), each branch binds by reference instead, whatever its size class.
`let r: i64& = if c { a } else { b };` binds `a` or `b` itself.
`f(if c { a } else { b })` passes `a` or `b` itself to
`fn f(xs: i64[>..]&)`, as `g(if c { i } else { j })` passes the `i64` `i` or
`j` to `fn g(x: i64&)`. Given `if c { s } else { t }`, `fn h(p: u8[:]&)`
re-points the slice variable the branch taken names. Calling a function
value is a call, not one of these constructs: its value is a temporary even
where its body's value names a variable (§9.2).
There is no ownership transfer beyond the return move, no destructors, no
`Drop`, no reference counting. The only deallocation is scope exit resetting
stack pointers. A `defer` (§6.6) runs code the program writes at a block's
exit; it is tied to the block, not to a value, and releases no memory
itself. (A `move` operation for resizable arrays, which assigns and leaves
the source empty, is anticipated but not in v1.)
Copying a variable or resizable value costs O(size). The explicit `copy`
marks that cost in the source.
Shadowing: an inner scope may re-declare a name (a distinct variable).
### 4.2 Construction contexts
Variable and resizable values come into existence only at these points, each
of which provides fresh storage in a statically known place:
* `let`/`var` initialization of a local or global;
* a by-value argument slot (§7.2);
* a `return`ed value (§7.3);
* `push`/`append` into a resizable (the new element region);
* a field/element inside a larger value under construction.
Construction writes metadata first, then elements or fields in order, as
required by invariant §1.3(4). Construction of a limited array writes only
its metadata and any provided elements; the remaining capacity is reserved
but **uninitialized**. This is safe because no read path to uninitialized
slots exists (§5.3), and cheap because the runtime commits skipped address
ranges explicitly (Appendix C.4).
While a value is being built in place in an array, nothing may grow that
array, neither the expression being built nor a function it calls (§1.3(4)).
Such a value is an element `push`ed into or allocated in the array that is
variable-size or holds relative references, a call's array result `append`ed
to it, an `append`ed array literal of such elements, or the new contents of
a whole assignment (§4.4). The compiler rejects a growth it cannot show to
be of a different array. A whole assignment builds its new contents over the
old ones, so its right-hand side may not use the array at all (§4.4).
Literal forms usable in any construction context:
* array literals `[1, 2, 3]`. An empty `[]` can be used where the element
type is known from context, or as the whole initializer of a `var` local.
The context is the array type of the variable, parameter, field, element
or result the `[]` constructs, or the slice type of one it is a temporary
for (below). It is never the type of an operand, branch or element beside
the `[]`: `x == []`, `if c { [] } else { x }` with no destination type,
and `[[], [1]]` are errors. As the whole initializer of a `var` local
(`var out = [];`), `[]` makes the local a grow-only `T[>..]` whose `T` is
fixed by the first `push`, `append`, `format` or whole assignment into it
(a string literal pushed into one makes it a `u8[][>..]`). `T` must be
fixed before the local is otherwise used or its scope ends. The `[v; n]`
fill form takes a non-negative compile-time element count (a constant
expression, as a size is: §3.3). `v` is evaluated once, including when `n`
is zero, and its value is repeated. Relative links retain their target at
every destination; `self` in a literal refers to each constructed element.
A literal whose destination names no array type is a `T[k]`, or a `T[]`
when its elements are not fixed-size (§3.3). A `T[k]` literal may also be
a temporary that a slice destination, a `for`, `[..]` or `bytes_of` views
(`f([])` views an empty `i64[0]` for `fn f(xs: const i64[:])`). A `T[]`
literal is a variable value and exists only in a construction context, so
viewing it requires binding a variable to it first. `==` and `!=` compare
a literal of either kind with any array or slice of its element type
(§4.5), since their result holds no view of it.
A literal whose elements, at any depth of nesting, are all integer
constants or all floats of literals adapts to a parameter as its elements
would (§3.1, §6.3). It is a value of an array or slice parameter of its
shape whose element type its elements fit: `take([0, 0, 0, 7])` for
`fn take(c: u8[4])`, and `[1, 2, 250]` for a `const u8[:]`. In overload
resolution this counts as a coercion (§7.1). The same holds for a
construct whose branches are all such literals of one type.
An `append`ed literal is the run of elements it adds. It is a `T[k]` for
the array's element type `T`, or a `T[]` when `T` is not fixed-size, and
its elements are constructed as the array's own (`bytes.append([1, 2])`
for a `u8[>..]`; `[]` adds nothing; a `str()` among them is built in its
element). Elements that are variable-size or hold relative references are
built where they stay, so a relative reference among them may point into
the array. Other elements are evaluated before the run is added, as a
pushed fixed-size element is;
* struct literals `X { a: 1, b: 2 }` (named) or `X { 1, 2 }` (positional, in
declaration order; no mixing). Named initializers may be written out of
declaration order only when every pair reversed by that order is proven
free of observable effects, runtime failures and nonlocal exits. A reversed
pair may not span an effectful omitted default either. Evaluation and
construction always proceed in declaration order, directly into each field;
no temporaries or copies are introduced to preserve source order. The
check is conservative. It admits reads, nontrapping computations and calls
whose checked bodies prove safe. Unknown/native calls, recursion,
mutation, indexing, loops, checked arithmetic/conversions and potentially
checked destination adaptations (capacities, length prefixes, relative
offsets) prevent a pair from being reversed. The check runs before
optimization and includes debug checks in every build, so acceptance does
not depend on optimization or runtime debug mode.
The diagnostic lists the supplied fields in declaration order;
omitted defaults and padding do not need to be added. If side effects
must run in a different order, perform them before constructing the literal.
Fields with declared defaults (§3.2) may be omitted: trailing ones in the
positional form, any of them in the named form. An omitted optional field
without a declared default is null; other omitted fields are errors,
unless the literal ends in `..` (`Entity { kind: k, .. }`). Each of those
fields then takes its type's default value (below), `[]` for an array
field, and a field whose type has no default value is still an error;
* `[..cap]` — an empty limited array `T[..]` with the given construction-time
capacity (`cap` a runtime expression); the reserved slots stay
uninitialized (§5.3, C.4). An array or string literal constructing a
limited array of static capacity must fit it (a compile error otherwise);
* variant literals `Shape.Circle { r: 1.0 }`, whose fields follow a struct
literal's rules, `..` included;
* `self`, inside a struct or variant literal only, as the initializer of a
non-optional relative-reference field pointing at the value being
constructed (`Node { prev: self, next: self }`, §3.9). This is the only
way to give such a field a value before anything else it could point at
exists;
* string literals (§3.7);
* `copy(x)`: a fresh copy of the stored value `x` names (or the pointee of a
reference), constructed at the destination like any rvalue. It is the only
way a non-fixed value can reach a value-typed destination from storage
(§4.1). Copying a temporary is an error, since it is fresh already.
* `default()`, for any fixed-size `T`: the value a `T` has before
anything is written to it. Numbers are 0, bools `false`, optionals null,
slices and limited arrays empty, and an ADT is its variant 0. Declared
field defaults (§3.2) are applied wherever a struct or payload declares
them, so an invariant that a declaration encodes as a default survives
being filled in by generic code (a `sum`'s accumulator, a hash table's
empty slots). A type that contains a non-optional reference without a
declared default has no default value, and `default()` for it is a
compile error. (The zero value a missed `qpoll` or `qget(ms)` yields,
§11.2, is all zeros; the two agree except where a field declares a
non-zero default.) A type that is not fixed-size has a default value too.
A literal's `..` gives that default value to a field, but `default()`
does not provide it. An array other than a fixed one is empty (`[]`; a
`T[..]` then has no capacity), a `varint` is 0, and a struct, a variant or
an ADT's first variant is built from its fields' declared defaults and
default values the same way (`S { .. }`).
### 4.3 The in-place construction guarantee
A constructed nonfixed value is built **directly in its receiving
storage**, including through calls and the selected branch of an
`if`/`match`/case-dispatch. Passing through such an expression is not a
reason to materialize a separate result and then copy it into place.
The same guarantee applies to fresh fixed-size aggregates containing
relative references when the receiver takes their representation. Their
result channels name the receiving storage,
including through multiple and nonlocal returns; correctness does not
depend on inlining or C compiler optimization. Other fixed-size results
may be passed as ordinary C values.
The packed byte layout may require relocation within the receiving
storage: a length prefix whose width becomes known after constructing the
elements can grow or shrink, and a value-form result delivered as an
element run can lose its header and unused capacity. Moving the element
bytes for these layout changes is permitted. The final representation,
reference targets and evaluation order must remain the same; an initializer
must not be re-executed to discover its size.
A limited-capacity receiver must establish that the complete live result
fits before writing outside its available slots. When the result's length
is not available before evaluating it, temporary construction for this
capacity check is permitted. This exception concerns a bounded receiver;
it does not permit staging an ordinary fresh result for a resizable one.
Copying an *already-constructed* value (an explicit `copy`, a by-value
argument from fixed storage, or the repeated value of a fill) is an ordinary
copy. The guarantee concerns fresh construction; §7.3 extends it to
returned locals and specifies the copies associated with different returned
locals and exits that abandon a partial destination.
### 4.4 Assignability
An lvalue may be assigned (`=`) after construction iff its size cannot change
or it can absorb the change (for reference-typed lvalues these rules govern
the *pointee* write that `=` performs; the reference value itself is updated
only by `.=`, §3.8):
* fixed-size lvalues declared `var`: assignable (plain overwrite copy);
* whole resizable arrays (top of their stack), and values holding one as
their tail (§3.4): assignable. Semantically this is clear-then-construct:
the bump pointer resets to the array's element start and the new contents
are built in place (from an rvalue or `copy(x)`, §4.1), so the right-hand
side may not use the array (below);
* limited arrays `[..k]`/`[..]`: assignable if the new length fits capacity;
* fixed-mode ADT lvalues: assignable, including with a different variant;
* **not** assignable: variable-class lvalues (`T[]` locals/fields/elements,
variable-mode ADT lvalues, `varint` fields). These are frozen at
construction; rebuild the container instead.
The right-hand side of a whole assignment of a resizable runs after the old
contents are gone, while the new contents are being built in their place. So
it may not use the array: it may not name the array (including its length),
or a reference to the array or to the value holding it. Neither may a
function it calls, through a global or captured variable that function
names. `x = [x[1], x[0]]`, `x = f(x)` and `g = rebuilt()` for a `rebuilt`
that reads `g` are errors. As with growth (§4.2), a use the compiler cannot
show to be of a different array is an error too. Examples are a use of a
reference parameter that a caller may bind to the global being assigned, and
a use through a call into a recursive cycle still being checked, which
counts as using every global and every variable in scope.
Where the assignment names the array as a field (`t.chars = …`), the
right-hand side may still name those other fields of the holding value that
cannot lead back to the array. These are flat fields (§1.1), and fields
whose references, through any number of steps, lead only to storage of a
type that cannot be the assigned location, cannot be a resizable on the way
from that location to the array, and cannot hold the value the location is a
field of. `t.chars = render(t.font)` is fine. So is `s.path = find(s.start)`
through `s: Walk&` for `struct Walk { start: Cell&, path: Pos[>..] }`, and
so is a reference of such a type that is bound to a variable first. A field
`last: Walk&` or `other: Pos[>..]&` may refer to `s` or to `s.path`, so
naming such a field is an error.
An assignment through a reference field (`d.r = …` for `r: u8[>..<]&`)
writes the array the field refers to. That array may lie anywhere an array
of its type can, including in the holding value's other fields. So a field
that is, or holds by value, an array of that type may be the one assigned
(`d.r .= d.own`), and so may what another field refers to. Naming either is
therefore an error, except for a local whose record shows where the
reference field may point.
Slices and references into the old elements are checked by the shrink rule
(§5.1). The assignment starts with a shrink and the right-hand side runs
after it, so a slice or reference that the right-hand side uses is live at
the shrink. New contents computed from the old are built in a variable of
their own and assigned as `copy()` of it, with the single copy written out
explicitly at the point where it happens (§4.3).
`let` forbids only assigning that name or field as a whole. The *contents*
of a `let` array or struct are as writable as their type says
(`let xs = [1, 2, 3]; xs[0] = 9;` is fine), and so is what a reference to it
reaches. Contents are made read-only by the type, `const T` (§9.5), and
`const x = e;` declares a `let` of type `const T`, which can be neither
reassigned nor written into. A `var` of type `const T` is the converse: it
can be reassigned (`=`, compound assignment, `++`) but never written into.
Definite assignment is enforced: no reads of uninitialized locals; every
declaration either has an initializer or is provably assigned on all paths
before use. A `let` declared without an initializer gets its value once.
Assigning it, or binding it with `.=` (§3.8), is an error if any path may
already have given it a value. That includes a joining branch, an earlier
iteration of a loop outside which it was declared, or a function called along
the way. Calls that compute the assigned value also count (§7.5). A
recursive cycle (§7.8) cannot assign such a `let` declared outside the
cycle, since all the cycle's activations share it. Fixed arrays require full
initialization (every slot is indexable); the `[v; n]` fill literal makes
large ones cheap.
### 4.5 Equality and comparison
`==`/`!=` are **structural** and require both operands to have the same
type. They compare:
* scalars and bools by value;
* structs and fixed arrays memberwise, excluding pad and ADT padding bytes.
Comparison is semantic, per member: memcmp is a valid optimization only
for gap-free layouts without floats, since a float compares by IEEE value
wherever it sits (`-0.0 == 0.0`, and NaN is unequal to itself);
* array-family values and slices by length, then elements (a sequential walk
for variable elements). Two operands of *different* array kinds compare
too, as slices, whenever their slices would have one type (§3.10): a
`u8[>..]` field against a string literal, or a `u8[..32]` against a
`u8[:]`, with no `[..]` needed;
* ADTs by tag, then payload.
References and slices follow a *top-level rule*. As the direct operands of
`==` they have value-like semantics: a reference compares its pointee
(transparency, §3.8), and a slice compares length, then elements. As
*members* of a compared composite they compare by identity (the reference
address, the slice's address+length): recursing through them would turn
`==` into an unbounded pointer traversal. Optionals compare as nullable
references (`o == null` is the null test); `.==`/`.!=` compare two references by
address rather than by pointee (§3.8). Ordering `< <= > >=` exists on the numeric
types only, with operands unified per §6.1.
---
## 5. The resizable/shrink rules
### 5.1 Grow-only `[>..]`
A grow-only array can shrink, but **never while a reference or slice into it can
be live.** Memory below the top never moves and is never reused while the owner
lives. Every interior reference and slice therefore stays valid for as long as
it can be named, and a bound on the length holds across every `push`.
* `push`/`append` bump the stack.
* May contain variable-size elements (build strings/ADTs in place, §7.3).
* Shrinking (`pop`, `pop_n`, `resize` downward, `clear`) is legal exactly
where the compiler can see that nothing is rooted in the array, under the
conditions below. `pop`, `pop_n` and `resize` additionally need fixed-size
elements, since a sequential array cannot find its last element (§3.3).
Use grow-only arrays for arenas, pools, string builders, tree storage, and
scratch buffers that are refilled or popped between phases.
**When a grow-only array may shrink.** The receiver is the array's variable,
a reference variable or parameter bound to the whole array, a global, or the
tail field of a struct reached through any of those (`s.items.pop()` for
`s: Stack&`). Shrinking a tail field shrinks the storage the struct lies in,
as a reference to the field would. The receiver is not a temporary, and not
a `reusable` pool (§5.4: its freelist keeps every slot live). The shrink may
be anywhere in an expression (an operand, an argument, a condition, the
value of a block, `if`, `match` or loop), so `print(a.pop())` and
`if !load(s) { … }` are fine.
At the shrink, nothing *live* may refer into the array. This rules out
anything that the shrink's statement evaluated before the shrink and uses
after it (§2): an earlier argument or operand that is, or holds, a reference
or slice; an earlier field or element of a literal being built; the array an
indexing or slicing reads; the destination of an assignment. So
`use(a[0], a.pop())`, `a[a.pop()]`, `S { r: a[0], n: a.pop() }` and
`a[0] = a.pop()` are errors, while `a[0] + a.pop()` reads its left operand
first and is not. Nor may a value be under construction in the array's
storage (§1.3): an element that `push` builds in place (§4.3), a literal run
that `append` builds, or a call's result being appended.
It also rules out any reference or slice variable rooted at the array. This
includes a `var` reference that the same-depth rebinding rule (§9.2) could
retarget into the array, and one that is not bound yet but is bound further
down a loop body. And it rules out any value that *holds* a reference into
the array: a struct with a reference field, an array of slices, a `let` copy
of an element of such, and the struct whose tail the array is, through its
other fields. Values hold references only where a store put them, and every
store the checker has seen is on record (§9.2). So this half of the test is
exact to the store: the error names the holder and the line where a
reference into the array was stored into it.
A value is never a holder if its type cannot hold a reference to anything
the array's elements contain by value, through any number of references. Nor
is a value linked by relative references alone (a node pool), since relative
references point within their own root. A reference to a whole array or
table is not a reference into its elements, and a shrink leaves it pointing
at the array or table:
`struct Walk { start: Cell&, text: u8[>..]&, path: Pos[>..] }` holds nothing
a shrink of `path` frees, and `w.path.clear()` through `w: Walk&` may be
followed by any use of `w`. A view of the elements taken through such a
reference is stored, if at all, where the holder's type has room for one.
The same holds after a whole assignment: `s.r = [1, 2]` for `r: u8[>..]&`
may be followed by any use of `s`. (The right-hand side of a whole
assignment is different: it runs while the array is rebuilt, and may not
reach the array at all, §4.4. A variable that the right-hand side takes a
reference out of counts as holding what that reference leads to, but a
variable of which it reads only flat fields does not.) A reference to a
slice variable or to a holder (including the storage whose tail shrinks)
counts as what it refers to: while the reference is live, so is whatever
that slice or holder may refer into.
A whole assignment of a value that holds no references (`refs = []`,
`s.refs = []`, a struct literal with none in it) *empties* the part of the
holder it writes: what was stored there before is gone. The part is a
variable's own storage, or the pointee of a `let` reference or of a
reference parameter (which no rebinding moves), together with the fields
named after it. Where each slot of a holder that can hold a view into the
array lies, on every path to the shrink, in a part emptied this way (the
whole holder, or its fields one by one), the holder holds only what was
stored into its storage since then. This stops being true once a store into
that storage is made that the record cannot show, such as one moving the
holder's own references within it. It also stops once a call runs that may
write into the part, since a callee's stores are never on record in their
order. These calls may write into the part:
* a call handed a reference that may point at the holder's storage, with
room for the part or lying in it, or handed anything whose references may
lead to storage of the part's type, whatever it points at;
* a call of a nested function;
* a call handed a block or any other function value;
* a block's own call.
A call handed a struct of the function's own, built from the holder's
reference fields, whose references lead only to what those fields do, is not
such a call. So
`for i in 3 { refs = []; pool = []; refs.push(pool.push(i)); }` passes: the
store at the end of one iteration is gone by the shrink at the start of the
next. Emptying `refs` on some paths only, after a store that reaches the
shrink, or only in a copy of it, does not pass.
`s.refs = []; s.pool = []; s.refs.push(s.pool.push(i));` through `s: S&`,
`struct S { pool: i64[>..]&, refs: (i64&)[>..] }` also passes: the path to
the pool is no reference into it, and a view taken through it is stored, if
at all, in `refs`.
A shrink must also preserve the storage that an enclosing `for` loop walks
(§6.5). This includes every reference or slice on the path to the sequence,
since the loop reloads that path on each iteration. It also includes the
sequence iterated in place, such as a slice, element, or field. A resizable
array is the exception: the loop reads its length again on each iteration.
A variable is live at the shrink when it can be read again afterwards, that
is, when it is named:
* later in the shrink's statement: in an operand evaluated after the shrink,
or, where the shrink is in the condition of an `if` or `guard`, the
scrutinee of a `match` or the sequence of a `for`, in the parts that the
condition, scrutinee or sequence leads to;
* later in its own block or in an enclosing block it was declared in;
* anywhere in a loop that contains the shrink and that the variable was
declared outside of, since the next iteration runs the rest of the body
again.
But suppose a statement of the shrink's own block after the shrink's
statement is a `return` or a `break`, and neither the shrink's statement nor
any statement between them has a `continue` that could lead back into a
loop. Then nothing that the exit leaves runs again from the shrink. The exit
leaves the rest of that block; a `return` also leaves the rest of the
function's body, and a `break` the rest of the loop or `block` it ends,
including that loop's next iteration. So a decoder may
`result.clear(); return result;` inside its loop while a dictionary of views
into `result` that the loop fills is still in scope. The exit's own
statement is read after the shrink like any other.
A function value's body runs in the middle of a statement of the function
calling it (§7.6), so that function's variables count as well. That function
may also call the function value again, so a variable declared outside the
body counts wherever the body names it, as in a loop. Only code that can
name the variable counts (the code of its own function, and of the nested
functions and function values written in it), so the same name in another
function is a different variable. "Named" is syntactic: any mention counts,
including a call of a nested function that mentions the variable, so the
test never depends on what the optimizer proved.
A reference whose last use is before the shrink is dead, even if its block
has not ended: `let w = line[..5]; print(w); line.clear();` is fine. A
scratch buffer refilled per iteration, or a stack popped between phases, can
hand out slices of itself freely, so "reusable scratch" and "structure I can
point into" are the same type. The operations themselves are the grow-shrink
ones: a stack-top move and a length store.
Assigning the array whole (`a = …`), or assigning a value holding it,
replaces its elements. It is a shrink under the same rule wherever its
location could be the receiver of a `clear`: for `s: Stack&`,
`s.items = []`, `s.items = load(f)` and `s = fresh()` are shrinks of the
storage `s` names, as `s.items.clear()` is. The right-hand side runs after
the shrink, so a variable that side names is used after it. `print`, `str`
and `format` evaluate and render their arguments in order (§3.7). So a
variable that a later argument names is used after a shrink made by an
earlier argument, whether by a call in that argument or through a `format`
overload rendering it.
A shrink through a reference or of a global cannot see the callers'
variables, so it is checked at every level. Each function specialization
records which globals, and which parameters' pointees, it shrinks (directly
or through its own callees), and what its calls stored into those pointees.
A call is checked as if it performed those shrinks and stores in the caller,
with each grow-only shrink checked where it stands in the body:
* against the stores the body made before it (in a loop, any the body
makes), not those it makes after;
* with the parts of reference parameters' pointees that the body had emptied
before it on every path counted as empty in the caller's holders too,
where the argument names such a part of storage that no nested function or
function value names. Such a part is the caller's variable, a field path
in it, or the pointee of the caller's own `let` reference or reference
parameter, which the caller's own callers judge the same way.
A function doing `s.refs = []; s.pool = []; s.refs.push(s.pool.push(i));`
may be called again and again on the same holder, in a loop or through a
function taking the holder by reference. The pointees of a parameter that
holds references by value (a struct with a reference field, an array of
slices) are what those references point to: a shrink or a store through one
of them is of the caller's storage, not of the callee's copy of the holder.
When a global receiver shrinks, the other globals are live, and what is
stored into them may come from functions checked after the shrink. So they
are judged once the whole program has been checked. By then every store into
a global, and every binding of a global reference or slice, is on record,
wherever in the program it is made. The shrink is an error where another
global may hold a reference into the array; the error names that global and
the line of the store. A call into a recursive cycle still being checked
counts as shrinking every array a function of the cycle textually shrinks.
A user `format` overload (§3.7) is a call at the `print`, `str` or `format`
that renders a value through it. When a `format` call renders the value, the
text is appended to its receiver, and that receiver is the overload call's
builder. The rest of the argument holding that value is rendered around the
call, so it is still in use there (where the argument lies, the elements of
the arrays and slices it renders, and what its references lead to), unless
the overload takes the whole argument: the value itself or a reference to
it.
A reference whose root is inexact (§9.2), such as a value chosen between
arrays (`if c { a } else { b }`) or one read back out of a container, may
point at any array of its type that the root bounds. A shrink through it
counts as a shrink of each of those arrays: those in variables at the root's
scope or outside it, and the caller's arrays that a parameter's references
lead to, for which the callers check the shrink. The same holds where a call
shrinks what an inexactly rooted argument points at.
A reference read out of the storage behind a reference parameter
(`s.text = …` for `s: S&`) may point at anything the callers or the function
stored there. The function judges its own variables against every array of
the type that outlives the call. Its callers each judge the shrink, by their
argument, as a shrink of an array that the storage leads to. A struct of the
caller's leads to the arrays its record says it refers to that are of the
type shrunk, or that hold or lead to one: `s.text = copy(t)` in a callee
frees no global `u8[>..]` the caller views, where the caller's struct refers
to a local text and a node pool. A struct built from the caller's own struct
parameter's reference fields leads where those fields do, which the caller's
callers judge in turn.
Nor can a shrink inside a function tell what its parameters point into. A
parameter's pointee is whatever each call passes, which may be a global, a
variable of an enclosing function, or what another parameter points into; an
inexactly rooted argument may be any array its root bounds. Where a
reference, slice or holder parameter, or a variable bound from one, is still
used after a shrink of a global, of such a variable or of another
parameter's pointee, the specialization records the pair. It does the same
where a view of a global or of such a variable is still used after a shrink
of a parameter's pointee. Each call judges the pair by the arguments it
passes. A call that may make the two one array is an error, as a still-used
variable of the caller would be, and a call that passes its own parameters
on records the pair for its own callers. A call into a recursive cycle still
being checked is judged again against the pairs the cycle records, once the
whole cycle has been checked.
### 5.2 Grow-shrink `[>..<]`
* Fixed-size elements only.
* `pop()` returns the element by value. A reference or slice element is
returned as the reference or slice it is, pointing where it did and as
writable as its slot, as reading the element would give it (§9.5), since
what a temporary holds is not rooted at the temporary (§9.2).
`pop_n`/`resize`/`clear` are allowed from anywhere: on a local, through a
reference, on a global, on a struct's tail. Assigning the array whole is a
shrink too.
* References and slices into it are created like any other (§3.8, §3.10),
and are **held by variables only**: bound to a local, passed down, or
returned, but never stored into a field, element, or global. Rationale:
after a pop, that stack-top memory can later be reused by *different
types* (other locals, other pushes), so no reference into it may outlive
the next shrink. Keeping such references out of storage means the next
rule only has to scan the variables in scope.
A literal's fields and elements are storage wherever the literal ends up,
so no struct, variant or array literal holds such a reference, even where
the literal itself is passed down or returned. `f(S { a[..] })` is an
error, as `let s = S { a[..] };` is. So is binding a global reference or
slice variable to one, since a global is storage as well.
What counts is where a reference *may* point, whatever its root says. A
value that may be any of several (§9.2) is stored only where each of them
could be. A parameter given a value that may point into the array is not
stored either, nor is a function's result where one of its returns may
point into it, nor a reference to a slice variable whose slice may. Where
a value's root only bounds where it points (§9.5), because the value was
read out of the storage behind a parameter or out of a value holding
references, or a callee returned such a read, the value may point into any
grow-shrink array that the storage leads to through references. With
`t: Top&` and a field `a: Cell[>..<]&` of `Top`, `t` and `t.a` may be
stored, but not `t.a[0]`, nor the bytes of `t.a`.
A reference or slice read out of a field, an element or a global can never
point into a grow-shrink array, since it was stored there, whatever
storage bounds it (§9.5). Neither can a slice that such a slot holds,
loaded through a reference to the slot. Nor does a value holding
references that is read out of a field or an element, popped, or copied
out of one by a `for` or a `match`, hold a reference that may point into
one: what it holds lies in that slot, and it is stored wherever what holds
it outlives the destination. A variable that points into no grow-shrink
array (one bound to such a read, for example) may not be rebound to a
value that may point into one: code that read the variable earlier through
a reference to it may have stored it.
The rule is about references that can point *into* the array. A reference
merely rooted at a value that holds one, whose pointee type the array's
elements cannot contain (a slice key read back out of a dictionary's
slots), can be stored like any other, whether or not it was passed to a
parameter. A `keys: u8[>..]&` read out of a struct with a `u8[>..<]` tail,
and a slice a callee makes of the buffer it refers to, can be stored as
dictionary keys freely.
* **A shrink is an error while any live variable or value may refer into the
array**. This is the test a grow-only shrink applies (§5.1), including the
values its statement still uses, its liveness rule, and its call summaries
for a shrink through a reference or of a global, but without the store
record. References into a grow-shrink array live only in variables and in
the values of the statement being run, so checking those still in use (a
reference to a slice variable among them) is sufficient.
For the same reason, a plain reference or slice read out of a field, an
element or a global never refers into a grow-shrink array, since none is
ever stored there. The shrink does not consider such a reference or slice,
nor a slice of such a slice or a reference into what it views, whatever
storage the read-back rule (§9.5) bounds it by, nor what a value read out
of such a slot holds. This stops at a reference: what a reference read out
of a field leads to may be a whole grow-shrink array, or a variable
holding a view into one. And a `var` bound to such reads still counts
where a rebind could retarget it into the array: a same-depth rebind
(§5.1), or, inside a loop the variable was declared outside of, a rebind
later in the body to a value whose root only bounds the array (§9.2).
The error is at the shrink and names the variable and where it was bound,
so either end can be changed: use the slice for the last time before the
shrink, or move the shrink. A call into a recursive cycle still being
checked counts as shrinking every grow-shrink array it can reach,
including through the references its arguments hold. Function values run
inline, so a shrink inside a block is checked against the block's own
enclosing scopes, and against the function calling the block, since the
block runs in the middle of one of that function's statements.
* **Balanced calls.** A call is *balanced* for an array when the array is
never shorter, while the call runs, than it was when the call began. Such
a call frees nothing that a view taken before it points into. A live view
lies within the array's length at the call, since every shrink after the
view was taken was checked against it. A view that a reference to a slice
variable reaches is no newer, since the callee can only return a view,
never store one where the caller would read it. A balanced call is
therefore not a shrink where it is made: no view is checked against it
there, or recorded with it for the callers to judge (§5.1), since a
parameter's view was also taken before the call. It still counts for the
callers, as a balanced call where the caller is balanced too.
A function is balanced for an array X if every shrink of X is either a
balanced call for X or a qualifying `X.resize(m)`, with or without a fill
value. For the resize to qualify, `m` must be a `let` in the same
activation, initialized to exactly `X.len` on the same path X. The array may
be a reference parameter's pointee, a global, or a captured variable. The same
path means one variable that
cannot be rebound (a `var` reference can be), followed by the same fields,
none of them a reference. `pop`, `pop_n`, `clear`, a resize to anything
else, and assigning X or a value holding it whole are not balanced.
Neither is a resize to an `m` that is bound to a writable reference
(§4.4), by `&`, `.=`, or a reference parameter, field or binding that is
not `const`, in a nested function too, since a write through that
reference can lower `m`. Binding `m` to such a reference after a resize
back to it is an error: that resize has already been judged balanced,
while a loop can run the write before it the next time round. Every length
X has during such an activation is at least its length on entry, so every
mark is too, and so is every resize back to one.
A call is balanced for an array of the caller's when every shrink it may
make, of any array that can be that array, is balanced. The caller's roots
decide which arrays can be that array, and a parameter's array can be a
global, a captured variable, or another parameter's array.
Grow-only
arrays have no balanced calls (§5.1): a callee can store references to its
new elements into the caller's holders before shrinking them away.
A call into a recursive cycle still being checked cannot be judged by the
part of the callee checked so far, so it is taken to be balanced for every
grow-shrink array it can reach until the cycle has been checked. Once it
has, the call is balanced if no function that such a call enters makes an
unbalanced shrink of a grow-shrink array when such calls are counted as
balanced. By induction on how deeply a run nests its calls, no run of the
cycle then shortens an array. Otherwise the call, and every call judged
balanced because of it, counts as a shrink after all: it is checked
against the views live across it, and recorded with them for the callers
to judge.
* `push` returns a reference to the new element, and `index_of` works, as on
grow-only arrays.
* Iterating the array with `for` uses indices under the hood and reads its
length on every iteration, so changing the length inside a by-value
`for x` loop over it is legal and only a logic hazard (bounds checks keep
it safe). The `&x` binding is a variable in scope, so a shrink inside such
a loop is an error. So is a shrink inside a loop over a slice, an element
or a field of the array, which the loop walks in place, or over a sequence
it reaches through a reference or slice stored in the array (§5.1, §6.5).
### 5.3 Limited arrays `[..k]` / `[..]`
Limited arrays grow and shrink freely within a fixed capacity. They are not
bound to a stack top (their capacity is reserved at construction), so they
can live anywhere fixed or variable values live (`[..k]` is fixed-class,
`[..]` variable-class). A `T[..]` constructs either from element contents
(capacity = initial length) or with the `[..cap]` literal (capacity `cap`,
length 0).
Interior references and slices **are allowed**, including surviving pops.
Because capacity and element type are fixed, every slot within capacity,
once first written, remains a valid value of the element type forever;
pop-then-push reuses memory at the same type. Never-written slots are
unreachable: indexing checks `len`, references/slices can only be created
into `[0, len)`, and every growth operation supplies values (§3.3). A stale
reference reads a valid, possibly-different value ("type-safe reuse", §9.4),
never corrupt memory. Indexing through the array checks against current
length; access through retained references does not (they were validly
created).
### 5.4 `reusable` arrays (safe allocation escape hatch)
A grow-only resizable local of fixed-size elements may be declared
`reusable` (the keyword prefixes the declaration:
`reusable var pool: Slot[>..] = [];`, also valid on globals). The compiler
pairs it with a hidden freelist (itself a small resizable of indices, on its
own stack, counted in N):
* `a.alloc_index(v) -> i64` — index of a slot: a freelist slot if available,
else a fresh `push`.
* `a.alloc_ref(v) -> T&` — same, returning `&a[i]`.
* `a.free(i)` — records slot `i` for reuse. The element remains a valid,
live value of its type forever. The index must lie in `[0, a.len)`;
otherwise the operation aborts with the ordinary array-index out-of-bounds
diagnostic, in every build, before changing the freelist. The index
expression is evaluated once. Where the code holds references rather
than indices, `a.free(a.index_of(r))` (§3.3) turns a reference back into
an index.
This is part of the semantics, not just the implementation: *all elements
remain valid at all times*. A reference to a freed-then-reused slot reads a
different (same-typed) value. This is type-safe reuse, never memory
corruption. This is the language's answer to tree-mutation workloads that
would otherwise need an allocator.
**Slice pools.** Declared `reusable[]` (`reusable[] var pool: T[>..] = [];`,
also valid on globals), the array hands out runs of elements instead. Its
hidden freelist holds (index, count) spans sorted by index, and a freed run
merges with the spans it touches:
* `a.alloc_slice(n) -> T[:]` — `n` elements from the front of the first free
span, in index order, that holds them; failing that, from the end of the
array, which grows by `n` less the elements of a free span that reaches
its end (the run then starts with those elements).
* `a.free_slice(s)` — records `s`'s elements for reuse.
* `a.realloc_slice(s, n) -> T[:]` — `s` resized to `n` elements. Shrinking
frees the elements past `n` and keeps the front. Growing a non-empty `s`
keeps its elements in place when `s` ends the array, which grows, or when a
free span starts where `s` ends and either holds the difference or reaches
the end of the array, which grows by the rest. Otherwise, and always for an
empty `s`, the slice's elements are freed and copied to where
`alloc_slice(n)` then places the run, which may be their old place merged
with free elements around it.
Every element an allocation or a growth adds is a default value (§4.2), so
those two need an element type that has one. The length is evaluated once,
and a negative length, or one no data stack could hold, aborts before the
pool changes. The slice handed to `free_slice` or `realloc_slice` must be
one of the pool's runs: one the pool handed out, a re-slice of it, or a
slice of the pool itself. Where the checker sees that, because the slice is
rooted at the pool exactly (§9.2), nothing is checked when the call runs.
Where the pool is a global or a variable of the calling function, or a pool
reference that can only be one of several such (**Pool references** below),
a slice rooted exactly at a global or a variable of the calling function
that the pool cannot be is a compile error. Any other slice, such as one
read out of storage where runs of other arrays could be kept as well, is
checked when the call runs: unless it is empty, its elements must lie inside
the pool's length starting on an element boundary, or the call aborts before
the pool changes. An empty slice frees nothing and grows like a new run,
wherever it points. A grown slice may be copied, which a value holding
self-relative references cannot survive (§3.9), so `realloc_slice` is a
compile error for such an element type; `in pool` references copy fine, and
the other two operations never move an element. The single-slot operations
do not exist on a slice pool, nor these on a `reusable` one; `push` and
`append` add at the end of either without consulting the freelist.
The guarantee is the same: a slice still naming freed elements reads whatever
their next owner wrote, or the default values an allocation put there. Freeing
a run twice is a logic error of the same kind: the freelist can then hand the
same elements out twice, but never anything outside the array.
**Pool references.** A `T[>..]&` variable or parameter bound to a pool is a
*pool reference*: the pool's operations work through it as on the pool,
since it carries the pool's freelist beside the address. So is a variable or
parameter bound to a value that chooses among pools of one kind, or among
pool references to them (an `if`, a `match` or a block giving one wherever
it gives a value). It carries the freelist of the pool chosen. A choice
between the two kinds of pool, or between a pool and another array, binds a
plain reference. A variable is a pool reference from its first binding on,
and `.=` rebinds it only to a pool of the same kind, or to a pool reference
to one; pointing it at any other array needs a new variable. A variable
first bound to anything else stays a plain reference, which `.=` may point
at a pool, as at any array, but which offers none of the pool's operations.
Only a plain reference variable or parameter is a pool reference: an
optional one, a field, an element and a function's result are not, even
where they point at a pool.
---
## 6. Expressions and arithmetic
### 6.1 Operators
The operators follow C and Rust:
* Arithmetic: `+ - * / %`. Integer `%` is Euclidean (§6.2); float `%` is C
`fmod`.
* Comparisons. The logical operators `! && ||` accept `bool` only; `&&` and `||`
short-circuit.
* Bitwise `~ & | ^ << >>` on integers.
* Assignment statements on assignable lvalues: `=`, `+=`, and the compound form
of every other binary operator, including shifts.
* Reference rebinding `.=` and reference identity `.==`/`.!=` (§3.8).
* `++`/`--` as statements on integer lvalues, with no expression form.
Precedence follows Rust (Appendix D). Bitwise operators bind tighter than
comparisons, so `x & mask != 0` means `(x & mask) != 0`. Shifts bind tighter
than `&`. Range expressions `a..b` appear only in slicing brackets, `for`
headers, and match arms.
**Operand unification.** A binary numeric operator's operands must reach
*one common type*, which is also the result type. It is found as follows:
* equal types stay as they are;
* a constant adapts to the other operand's type (a compile error if its
value does not fit);
* otherwise, if exactly one operand implicitly widens into the other's type
(§6.3), the wider type is the common type;
* an integer operand meeting a float one converts to the float's type, so
`i32 * f32` is an `f32` multiply and `i64 + f64` an `f64` add.
A float that takes its type from float literals (the literal itself, or
`n * 0.5` built from literals and integers) adapts to the other operand's
float type as a literal does (`x * (n * 0.5)` with `x: f32` computes in 32
bits throughout), and is an `f64` where neither operand gives it a type.
Anything else (same-width signed/unsigned, `u64` with anything signed) is a
compile error asking for a cast. Nothing here invents a type absent from the
expression: `u8 + i64` is an `i64` add, but `u32 + i32` does not become
64-bit arithmetic implicitly.
Exceptions: shifts take the *left* operand's type as the result (the count
is any integer type, masked per §6.2), and `==`/`!=`/orderings unify the
same way but produce `bool`. So a constant shifted by a count that is not a
constant keeps the constant's freedom to adapt. `1 << k` is not a constant,
but it takes the integer type of its destination or other operand, as a
constant does, where the constant fits that type (`let m: u64 = 1 << k;`,
`m | (1 << k)`, a `u8` field), and is an `i64` where nothing gives it a
type. An integer operation over such shifts and constants does the same
(`(1 << k) - 1`, `~(1 << k)`), and is computed at that type throughout; a
typed operand, unary `-` or a cast gives it a type of its own. So does `~c`
of a constant `c >= 0`, whose value depends on the width it is computed at.
It adapts where `c` fits, so `let a: u8 = ~4;` is 251 and `flags & ~F_ROAD`
clears a flag in a `u8`, while `~4` is -5 at a signed type or where nothing
gives it a type. `-~c` is the constant `c + 1`, and a named constant `~c`
(§3.1) adapts as `~c` does. Unary `-` requires a signed (or float) operand;
`~` any integer, keeping its type.
**Comparisons and `u64`.** A comparison produces `bool`, so it does not need a common numeric result
type. However, comparing signed and unsigned operands must preserve their
values. `u64` is the only unsigned type with no signed supertype
(`u8`–`u32` widen into `i64`, §6.3). A comparison between `u64` and a signed type
is therefore allowed **when the signed operand is known non-negative**: it
converts to `u64` without changing value, and the comparison is a single
unsigned one, no wider than either operand and never a hidden branch.
"Known non-negative" is determined by syntax and is limited to a non-negative
integer literal, a `.len` or `.cap` (non-negative by §10.4), or a `let` bound
to one of those. A `let` is never reassigned, but a writable reference can still
change its
value (§4.4). A comparison therefore cannot rely on a `let` if it, or a `let`
it was bound to, is bound to a writable reference anywhere. This includes
bindings through `&`, `.=`, or a reference parameter, field, or binding that
is not `const`. Bindings in nested functions count too. Either order is an
error. Whether a comparison compiles thus
depends only on what is written, never on how much the optimizer managed to
prove. Other cases require an explicit cast because conversion may change the
value. A cast does not, however, preserve every comparison: `x as! i64`
on a `u64` above `i64.max` silently compares as negative, and the checked
`x as i64` aborts in debug on a value that was perfectly legitimate to
compare. When the rule permits it, use the direct comparison to preserve the values
without conversion overhead.
**Elementwise math**: the arithmetic operators apply memberwise to any two
values of the *same* struct/fixed-array type whose scalar leaves are all
integers or all floats, none of an index or bound type (§3.11); the
result has that same type. The compound forms
`+= -= *= /= %=` apply to these same types, resolving the destination once
and reading its old value before the right-hand side runs (§2). Unary `-`
negates such a value memberwise where no leaf is an unsigned integer
(`-v`, with the same result type; a float member negates exactly, so `0.0`
becomes `-0.0`, and an integer one overflows as a scalar `-` does). This
covers vector math without an operator-overloading feature. The standard
library supplies `float3` and friends; named vector ops (`dot`, `cross`,
`normalize`, …) are ordinary stdlib overloads per math type, not language
builtins.
`*` and `/` also accept one scalar operand, in either order, when every
numeric leaf of the struct/fixed array has exactly the same type. The
scalar converts implicitly to that leaf type (§6.3): `float3 * 2.0`,
`2 * float3`, and `float3 / f32` produce a `float3`; `2.0 / float3` divides
the scalar by each component. The aggregate's type never widens: an `f64`
variable scaling a `float3` needs `as f32`, and an integer scalar must fit
or widen into the integer leaves. Nested structs and fixed arrays work the
same way; aggregates with mixed leaf widths or kinds, or no numeric leaves,
do not support scalar operands. `+`, `-`, and `%` still require two matching
aggregates. The compound forms `aggregate *= scalar` and
`aggregate /= scalar` follow the same rule. Both operands are evaluated
once, left to right, before any result member is written, including when
the scalar is a member of the destination.
### 6.2 Integer semantics
* Every integer operation computes **at its operands' type**: `u8 + u8` is
an 8-bit add, `i32 * i32` a 32-bit multiply. There is no promotion:
operands reach a common type only by the unification rule of §6.1, which
never widens both sides behind the programmer's back.
* Signedness is part of the type: `/`, `>>`, and the ordering comparisons are
unsigned operations on unsigned types (`>>` shifts in zeroes) and signed on
signed ones (`>>` replicates the sign bit). Integer `/` truncates toward
zero: `-7 / 3 == -2`, while `-7 >> 1 == -4`.
* **`%` is Euclidean** at every integer type: the result lies in `[0, |b|)`
and is never negative, whatever the dividend's sign. So `x % n` is a valid
index into a length-`n` array for *any* `x`. The reduction idiom means
what it looks like, and the optimizer can drop the resulting bounds check
(§10.5) instead of requiring a guard the programmer knows is redundant.
On unsigned types this is ordinary remainder; on signed ones it costs one
predictable conditional add over C's truncating `%`, on an operation that
is already a division. Consequently `i64.min % -1` is `0`, not a trap.
Float `%` stays C `fmod`, so `-7.0 % 3.0 == -1.0`, while `-7 % 3 == 2`.
* **Division pairs.** Integer `/` and `%` use different conventions, so
`b * (a / b) + a % b == a` does not hold in general. With `import std;`,
`rem(a, b)` is the truncating remainder (zero or the dividend's sign),
and `div_euclid(a, b)` is the Euclidean quotient. The matching identities
are `b * (a / b) + rem(a, b) == a` and
`b * div_euclid(a, b) + a % b == a`, as mathematical identities when the
quotient is representable; evaluating their intermediate products can
overflow. Both functions accept all eight integer types, returning the
operands' type. `div_euclid` rounds down for a positive divisor and up
for a negative one, and has the same zero-divisor and quotient-overflow
behavior as `/`. `rem` aborts on a zero divisor and returns zero for
`MIN, -1` at every signed width. For unsigned integers `div_euclid` is
ordinary `/` and `rem` is `%`.
* **Signed overflow** (at the operation's width): aborts in debug builds;
wraps two's-complement (defined) in release. The debug abort is per
operation *as it executes*, not per operation as written: the optimizer
may regroup an associative chain or drop an operation altogether, and
each check stays with the operation it belongs to. So a debug build is
a bug-finding tool, not a promise about every intermediate result written
in the source; what a release build computes never depends on any of it.
**Unsigned arithmetic wraps modulo 2^width by definition, in every
build**. Modular arithmetic is what hashing, PRNGs, and bit manipulation
mean by unsigned math, and it is why those kernels are written with
unsigned types. Division/modulo by zero: aborts always. `i64.min / -1`
aborts in every build (it would trap in hardware); at narrower signed
widths the same case is an ordinary overflow: debug abort, release wrap.
Shift counts are masked to `0..width-1` (defined).
* Integer literals above `i64.max` (up to `u64.max`) are `u64` constants
(§2, §3.1); negating one is a compile error (except `-(2^63)`, which is
exactly `i64.min`).
### 6.3 Conversions
Within a kind, a conversion the machine can prove value-preserving is
implicit, and anything that could lose a bit or flip a sign needs a cast.
Between the kinds, an integer becomes a float implicitly, and
a float becomes an integer only through a cast: dropping the fraction is the
real loss of information, while an integer too large for the float's
mantissa rounds as every float result does.
* **Implicit conversions** apply wherever a value meets a differently typed
destination or operand. An integer widens to the same signedness
(`i8→i16/i32/i64`, `u8→u16/u32/u64`), and an unsigned integer converts to a
strictly wider signed type (`u8→i16..i64`, `u32→i64`). Other implicit
conversions are `f32 → f64`, constants to any type that holds their value
(§3.1), and every integer type to either float type. The last rule includes
`i64`, `u64`, and literals: `2` works where a float is expected, just as `2.0`
does. Any integer type except `u64` also converts implicitly into a `varint`
store (§3.6).
* An integer becoming a float takes the float's nearest value where it has
no exact one (beyond 2^24 in magnitude for `f32`, 2^53 for `f64`). No
conversion to a float traps, in a debug build either.
* **Float literal types.** A float literal has no committed type: it adapts
to `f32` or `f64`, whichever its destination or other operand has, and is
an `f64` where nothing gives it a type. So does every float whose
float-ness comes only from float literals and the integers they meet:
`n * 0.5`, `-(i + 0.25)`, `(n - 1) * 2.5` with integer `n` and `i`, and a
construct whose branches are all such (§6.4). Such
an expression is computed at the type it adapts to, throughout (an `f32`
argument, field or operand gets `(n as f32) * 0.5` in 32 bits), and is an
`f64` where nothing gives it a type. A variable, parameter, call result or
explicit `as` commits a type; §7.7 describes the limited case where an
`f32` destination chooses a generic call's computation width first.
A constant part of it is folded at full
precision and rounds once to the type the whole adapts to, as a constant
does anywhere: beyond `f32`'s range (about ±3.4e38) to an infinity, as
`as f32` rounds.
* **Float literal `let`s.** A `let` (or `const`) with no written type, local
or global, whose initializer is such a float
(`let a0 = i * 6.2831853 / n;`, `let K = 0.25;`, std's `PI`) is
literal-like as well. It holds its value as an `f64`, computed once, and
each use adapts as a float literal there would: at an `f32` destination,
beside an `f32` operand (`a0 * r` with `r: f32` is an `f32` multiply), in
a construct's branches, and as a generic argument beside an `f32`. Such a
use takes that `f64` rounded once to `f32`; where nothing gives it a type
it is the `f64`. So `let h = n * 0.1;` stored in an `f32` is `n * 0.1`
computed in 64 bits and then rounded, which can differ in the last bit
from the same expression written at the `f32` destination, computed in 32
bits throughout. A use that adapts it relies on it keeping its value, and
a writable reference to it could change that value to any `f64`. So, as
for an integer named constant (§3.1), such a reference is an error in
either order; a reference to it is an `f64&`. A `var`, and a `let` with a
written type, are values of their type. Unlike for an integer named
constant, this holds for local `let`s too: adapting a float changes only
how it rounds, as writing the literal there would, whereas adapting an
integer local would change the width at which its arithmetic wraps.
* **Never implicit**: narrowing; same-width signedness changes (`i32 ↔ u32`);
anything signed into any unsigned type (a negative value can hide in any
signed operand, so `u32→i64` is silent but `i32→u64` is not); `u64` into
any signed type; `f64 → f32`; and float → int.
* `x as T` — explicit conversion between any two numeric types,
**range-checked in debug** (abort on value change), truncates/wraps/
rounds-toward-zero silently in release. A conversion to a float is never
checked, in any build: from an integer it rounds as the implicit one
does, from an `f64` to the nearest `f32`, as float arithmetic rounds every
result it computes, and a value beyond `f32`'s range (about ±3.4e38)
becomes an infinity. None of these is something a debug abort would
usefully catch.
* `x as! T` — always-unchecked wrap/truncate, for when losing bits is what
is intended, even in debug. To a float it is the same conversion as `as`.
* Into an index or bound type (§3.11), `as` first checks the value against
the array in every build, and `as!` is an error.
* float → int (both forms in release, `as!` always) is defined exactly:
truncate toward zero, then wrap modulo 2^64 into the target's width; NaN
yields 0. The common in-range case is a range test and a hardware
conversion; only the out-of-range tail pays for the defined wrap.
**Redundant casts.** An `as` or `as!` that changes nothing is a warning.
Such a cast is one whose operand has its type already, or converts to it
implicitly where the cast stands, so that deleting the cast leaves every
type, operation, value and call of the program as it is. `n as i32` of an
`i32 n` warns, as do `17 as u32` passed to a `u32` parameter, `b as i64` of
a `u8 b` stored in an `i64`, and `w as f32` in `(w as f32) * 0.5` passed to
an `f32` parameter, where `w * 0.5` is an `f32` multiply too. A cast that
decides anything stays silent: a type nothing else gives
(`let k = 5 as u32;`, `print(b as u8)`), an operation's width or signedness
(`(a as i64) * b` with `i32` operands), a float's rounding
(`(n as f32) * 0.1` stored in an `f64`), a call's overload or a generic's
instantiation. Of two casts that cannot both be removed, as in
`(a as f32) * (b as f32)` of two integers, the right one warns. Of a cast
directly inside another, where both could be removed, the outer one warns.
Casts in a generic function's body, in a function value's body, and in what
is declared in either are exempt from this warning: another instantiation
may need the cast. The warning comes once per cast as written, where every
check of it agrees.
### 6.4 Control expressions
```goose
if c { e } else { e } // expression; both arms required when used as value
while c { s }
for v in e { s } // see §6.5
loop { s } // infinite; exit via break
block { s } // early-out construct: break E exits with value E
{ s } // a plain scope with a value; not a break target
guard c else { s } … // if c { … } else { s }: … is the rest of the block
guard c; … // if c { … }
match e { ... } // §8
return e? (from f)? // §7.3, §7.9
break e? // exits innermost loop/block, optionally with value
continue
```
`block { }` exists to promote early-out style anywhere, not just at function
top level. `break` binds to the innermost `loop`/`while`/`for`/`block`;
labels are not in v1. For definite assignment (§4.4) and narrowing (§3.8),
what holds after one of these is what holds at every way out of it: each
`break`, and the end of a `block`'s body, a `while`'s condition found false
or a `for`'s last iteration; a `loop` has only its breaks. All `break E` of one construct must agree on E's
type: a later break's value constructs into the type an earlier one gave
the construct.
A construct's branches (its arms, its tail and its breaks) join into one
type. A literal branch adapts to the others' type, as at any typed
destination (§3.1). An integer constant adapts to an integer type it fits
(`if c { x } else { 0 }` has `x`'s type, a `u8` for an `x: u8`), and a float
literal, or a float of literals and integers (§6.3), adapts to an `f32`
(`if c { x } else { 0.62 }` is an `f32` for an `x: f32`). An integer branch
beside a float one converts to the float's type (§6.3):
`if c { n } else { 0.5 }` is an `f64` for an integer `n`. A `[]` branch
adapts to no other branch: it takes an element type only from a destination
(§4.2).
A construct whose branches are all literals is itself literal-like wherever
it meets a type, as at a typed destination:
* integer constants adapt to any integer type every one of them fits
(`g(if c { 1 } else { 2 })` passes an `i32` to `fn g(x: i32)`, and
`(if c { 200 } else { 100 }) + b` is a `u8` add for a `b: u8`), and are an
`i64` where nothing gives them a type;
* float literals, and floats of literals and the integers among them, adapt
to either float width (`k(if c { 0.5 } else { 0.25 })` passes an `f32` to
`fn k(x: f32)`), and are an `f64` where nothing gives them a type;
* branches that are all `[]` take the array type they meet
(`h(if c { [] } else { [] })` passes an `i64[]` to `fn h(x: i64[])`), and
are an error where nothing gives them one.
A break with such a number settles no type for the later ones:
`loop { if c { break 1; } break 2.5; }` is an `f64`. A bare `{ … }` in
expression position (a match arm of several statements, for example) is only
a scope: `break` inside it still leaves the enclosing loop.
Arrays and slices of one element type join as a slice. This happens where
one branch's value is a slice (a string literal, for example) and another's
is an array of the same element type, or where the construct's destination
type is such a slice (an annotated variable, a declared result, a parameter
declared as a slice). An array that a branch gives is then a whole-array
slice of that array itself (§3.10) rather than a copy of it (§4.1):
`let name = if item.count == 0 { "none" } else { item.name };` makes `name`
a `const u8[:]`, which views the `u8[..48]` `item.name` where that branch is
taken. The slice is read-only where any branch is: a `const` slice (a string
literal among them), or an array a view of which would be (§9.5). Like any
slice it must not outlive what it views (§9.2): an array declared in the
branch, or a temporary made there (a call's array result, an array literal),
is an error. Arrays of two different types join this way only where a slice
joins them as well, or where the destination is a slice; otherwise they do
not agree. A `break` agrees with the first: after a slice, a break's array
is a slice of itself, while an array that comes first is what a later slice
constructs, where it can.
`guard` guards the rest of the block it is in, whatever kind of block that
is: a function's body, a loop's body, a branch, a `block`, a plain scope, or
a function value's body (§7.6). So `{ s1; guard c; s2 }` is
`{ s1; if c { s2 } }`, and `{ s1; guard c else { s3 } s2 }` is
`{ s1; if c { s2 } else { s3 } }`. The code after a guard runs only where
`c` holds, and is checked on that basis, including flow-narrowing of `T?`
(`guard r else { return; }` leaves `r: T&`). A guard does not jump anywhere:
`guard c;` skips the rest of its block. Directly in a function's body this
returns from the function, in a loop's body it goes on to the next
iteration, and in a function value's body it ends that call of the value,
not the function running it. Leaving more than the block is the `else`
block's job: `guard c else { break; }` leaves the loop,
`guard c else { return; }` the function. An `else` block that does not leave
runs instead of the rest of the block, which then ends. Where the rest of
the block ends in a value, the `if` is the block's value
(`block { guard ok else { break 0; } n * 2 }`); a guard is a statement of
its block, never an operand. What is declared after a guard is declared in
the `if`'s block, so a nested function declared before the guard cannot call
one declared after it (§7.5).
### 6.5 `for`
Built-in iteration only (no iterator protocol):
* `for i in a..b` — integer range, half-open `[a..b)`; the bounds unify per
§6.1 and `i` runs at that type (an `i32` range gives a 32-bit loop variable).
* `for i: T in a..b` — the same range with `i` of the integer type `T`: each
bound is a value of `T`. A constant bound adapts as at any typed
destination (§3.1), and a typed bound converts only by implicit widening
(§6.3), so with an `n: i64`, `for i: i32 in 0..n` is an error asking for
`n as i32`. The end may be one past `T`'s largest value, where `T` is
narrower than 64 bits: `for i: u8 in 0..256` visits every `u8`, and its
counter never overflows. Such an end is written with literals alone
(`256`, `255 + 1`): an end naming a constant is a value of `T` as any
bound is, computed at run time as the same expression is anywhere (§6.2).
* `for i in n` — sugar for `0..n`; `i` has `n`'s type. `for i: T in n` is
`for i: T in 0..n`.
* `for x in arr` — element copies for fixed-size elements, at the element's
type; element references for non-fixed ones (§4.1), whose walk is
sequential. The copy cannot be written to (`x.f = 1` is an error naming
`&x`), since the write would update only the copy. An element that *is* a
relative reference (§3.9) binds as the
loaded plain reference, exactly as indexing it gives; an element with
relative references *inside* it cannot be copied out of its root at all,
and needs the `&x` form.
* `for &x in arr` — element references (the mutation form for fixed-size
elements; redundant, and a warning, for non-fixed ones). Over a `[>..<]`,
the binding is a reference in scope: no shrink inside the loop (§5.2).
An element that is itself a reference binds as the reference it holds
(loaded if relative; there are no references to references), pointing
where the element does by the read-back rule (§9.5), not into the array.
`&x` makes that binding one that writes through, and a varint-width
relative element, which is non-fixed, binds that way without the `&`.
* `for x, i in arr` / `for &x, i in arr` — with index (`i: i64`).
`for x, i: T in arr` gives the index the integer type `T` where `T` holds
every index `arr` can have. A fixed-size or static-capacity array's length
bounds its indices, while any other array's or slice's index can reach
2^48 − 1 (§10.4), which no type narrower than 64 bits holds, so such an
index is converted with `as` in the body. A range's or count's index
counts iterations as an `i64` and takes no type; an element binder has its
element's type and takes none either.
All array-family types and slices are iterable. Custom access patterns are
provided by HOFs taking static function values (§7.6), which compile to
plain loops.
Range/count bounds are evaluated once, before the first iteration; an
empty or reversed range performs no iterations. `continue` advances to the
next index or sequential element just as reaching the end of the body does.
A loop iterates its sequence in place, not a copy of it. Traversal of a
resizable array tests the current length before each iteration, so
permitted growth can add elements to the traversal and a shrink ends it
early; its elements never move. A slice keeps its own length: growing its
source does not extend that view. A slice, and anything else the loop walks
in place (a fixed or limited array, an element or a field of a larger
value), is live for the whole body, as a slice variable would be: a shrink
of a resizable array it lies in or views is an error, however the body
reaches it (a call, a function value; §5.1, §5.2). An index on the path to
the sequence is evaluated once, but the references and slices on it
(`refs[i].items`) are loaded again on every iteration, so the arrays they
lie in may not shrink either. Iterating a copy (`let row = a[i]; for x in
row` over fixed-size rows), or a reference read out first (`let r .=
refs[i]; for x in r`), leaves the array free.
**Specialization-time expansion.** `const for` specializes a fragment once
per entry of a compiler-known sequence. Its v1 sources are `fields` with
two bindings (field name string and field accessor), `variants` with two
bindings (variant name string and variant), `types` with one
type binding, and `[c1, c2, ...]` with one constant binding. `fields`,
`variants` and `types` are contextual identifiers in this header, not
keywords or ordinary function calls. Bindings use required angle brackets:
```goose
fn visit_fields(value: T&) {
const for in fields { F(N, &value.A); }
}
fn transform_fields(value: T&) -> T {
T { const for in fields { A: F(N, value.A), } }
}
fn each_case(value: T) {
match value { const for in variants { V c => F(N, c), } }
}
```
`fields` visits direct, non-padding fields of a concrete struct or
variant type (a variant's payload) in declaration order. Its accessor
identifies the field of that particular struct instantiation: another struct
with the same field name does not match. A variant's accessor reaches a
value of that variant type only, not the ADT before it is matched. It can
appear in a member access or initializer label. `variants` visits every
variant of a concrete ADT in declaration order, with or without payload; its
variant binding is written where a variant's name is: a type, a constructor,
a `match` pattern with or without a binder, and, for a variant without
payload only, a value. It is not stored, passed or turned into a number, and
its name string is not a persistence identifier. Bindings follow lexical
scope and shadowing, including in nested functions. Types and constants may
be heterogeneous; all three sources may be empty. Constants are scalar or
string literals, named global constants, and the integer constant
expressions supported for array sizes, including enclosing expansion
bindings. There is no arbitrary compile-time execution.
A statement fragment is a separate scope for each entry. A constructor
fragment contributes comma-separated initializer entries, flattened before
ordinary default, completeness, ordering and destination checks. A `match`
fragment contributes arms, flattened before the checks of §8.1: every
variant once, `_` last, and the reference-binder and lifetime rules for each
generated arm. A generated arm may bind a variant without payload, to an
empty value of its variant type. An
expansion produces no runtime iterator or reflection metadata. Its body
contains ordinary runtime code, with the usual reference, lifetime and
construction rules. Returns retain their lexical targets; a `break` or
`continue` cannot cross an expansion boundary. The full v1 rules and
implementation limit are in [the `const for` design](design/const_for.md).
### 6.6 `defer`
```goose
defer stmt; // an assignment, an increment, or an expression statement
defer { … } // a block
```
A `defer` statement's body runs when the block the statement is written in
ends, by any exit: the end of the block's text, `break`, `continue`,
`return`, a function value's `return` (§7.6) through the call running it,
or a `return … from` (§7.9) passing through the function. It runs once per
exit of the block, so a `defer` in a loop's body runs at the end of every
iteration. An exit runs the bodies of the `defer` statements it has run
past in the blocks it leaves, innermost block first and latest first within
a block, so a body runs before the storage declared before it is released
and may read it. The body is evaluated at the exit, not where it is
written: it reads variables as they are then. At a `return`, the returned
value is constructed at its destination first (§7.3), then the bodies run,
then the block's storage is released.
Restrictions, all checked statically:
* The body cannot leave: no `return`, `break` or `continue` in it may
target anything outside it, a `return` in a function value written in it
included.
* The body uses no long-distance returns: it holds no `return … from`, and
calls no function that performs or propagates one, catches one, or calls
one that does, transitively. The body may run while such a return is in
flight (§7.9), and a second one, a contained throw and catch included,
would disturb the first's state.
* The body does not name a local the function returns by name: that local
is built at its result destination (§7.3), and is complete when the body
runs.
* The body is checked where it is written: it names what is declared
before it, and a `var` optional narrowed before it is not narrowed in it,
since an assignment before an exit may have undone that. What the body
assigns is not assigned after the statement, since the body runs at the
exits.
* A `defer` is a statement of a function's body, or of a block in one; a
global initializer has no scope exit.
A `defer` body does not run where no exit happens: `abort`, a failed
`assert` or bounds check, `exit`, a contained worker's abort (§11.2), and
workers still running when `main` returns. The process or the worker's end
reclaims what the body would have released.
---
## 7. Functions
### 7.1 Declarations and generics
```goose
fn name(a: i64, var b: f64, xs: i32[:]) -> i64 { ... }
fn generic(a: T, b: T) -> T { ... }
fn also_generic(a, b) { ... } // untyped params are generic
fn scaled(a: i32, b: i32 = 0, c: f32 = 1.0) -> f32 { ... } // scaled(2) is scaled(2, 0, 1.0)
```
* There are only free functions: no methods and no impl blocks. UFCS:
`x.f(a)` passes `x` as `f(x, a)` does. Resolution tries fields and
built-in members first, then functions, which it looks for in the
namespace of `x`'s type first (§11.1).
* Parameters are `let` bindings by default: they are not reassigned, and
their contents are as writable as their type says (§4.4). `var` makes the
(by-value) parameter a reassignable local.
* A parameter with no type annotation is generic (Lobster-style). Explicit
`` parameters express same-type constraints and let signatures name
types. Return types may be omitted where they can be inferred (they are
required across recursive cycles, §7.8). An omitted result type is taken
from the first return checked, the way an un-annotated `let` takes its
type from its initializer (§3.8, §4.1). The result is a reference if that
return gives a reference to a non-fixed value, a non-fixed lvalue other
than the function's own local (which the return moves, §7.3), or an
explicit `&x`. `fn args(c: Ctx&) { c.args }` returns what the field
refers to, just as `c.args` would, and `copy(args(c))` copies it. A
reference to a fixed-size value is returned as its pointee. A part of the
function's own local, or of a temporary, dies with that local or
temporary, so a reference to it cannot be returned in either case: write
`return copy(x)`. An omitted result type cannot be taken from a `[]`,
which has no element type of its own (§4.2).
* A trailing parameter may declare a **default value**, as in
`c: f32 = 1.0`. The parameters after one with a default have defaults
too, and a parameter with a default has a type (an untyped parameter's
type is its argument's). A call may leave out any number of the trailing
parameters that have defaults. Each parameter left out takes its default,
which is evaluated at that call, after the written arguments, in
parameter order, and again on every call: `scaled(2)` above is
`scaled(2, 0, 1.0)`. The default is a value of its parameter's type, as
the written arguments and any explicit type arguments determine that
type, and it adapts as an argument would: `fn scale(x: T, by: T = 2)`
doubles a `u8` and an `f64` alike. A default is an error at a call where
it does not fit: `fn back(x: T, by: T = -1)` does not accept a `u8`.
A default does not bind a type variable itself: to leave `b` out of
`fn f(a: i64, b: T = 0)`, the call is written `f(1)`. Each call
that leaves a default out checks the default there, as if it were the
argument written in its place. So a default may not lead to a call that
takes the same default again, just as a field default may not lead to a
construction that takes it again (§3.2): `fn f(a: i64 = g())` beside
`fn g(b: i64 = f())` is an error. A named function passed as a static
function value keeps its defaults (§7.6). The parameters of a block
literal and of a `thread_fn` (§11.2) have no defaults, and neither does
a tag-dispatched call (§8.2).
* A default can name what a top-level declaration could name, in the
declaration's namespace: globals, constants, top-level functions and
types, and the type parameters that the call binds to types. Those are
the function's own and, for a nested function, those of the functions
around it (§11.1). A default never refers to a local of the code calling
the function, whatever its name. A name that means something else in the
declaration's own scope is an error in a default; it does not refer to
the global of that name instead. Such names are a parameter of the
function (`::x` names the global `x`), a type parameter bound to a
function value (§7.6), and, for a nested function (§7.5), a variable or
nested function around its declaration, or a type parameter bound to a
function value of a function around it.
* Overloading by parameter types is allowed, and resolution is static. The
unique concrete exact match is chosen first, then a generic exact match,
then a match requiring coercion (array→slice §3.10; literal fit §3.1,
which includes an array literal's constants, §4.2; implicit widening
§6.3). A candidate's rank is its worst argument rank. Ties are errors;
there is no further tiebreaking by specificity or declaration order.
A default does not count as an argument: when a call leaves some of a
candidate's parameters to their defaults, the candidate ranks by the
written arguments alone. So `f(a, b = 0)` beside `f(a)` makes every
one-argument call of `f` ambiguous.
Only if no ordinary candidate matches is tag dispatch (§8.2) tried.
A candidate that converts an integer argument to a float (§6.3) is the
last resort, after tag dispatch and after a builtin of the same name:
`f(2)` calls `f(i32)` beside `f(f64)`, and `abs(n)` calls std's
`abs`, not `abs(f64)`. An integer converts to either float type
equally well, so two such candidates are ambiguous: with an integer `n`,
`sqrt(n)` has to be written `sqrt(n as f64)`, while `sqrt(n * 0.5)` calls
`sqrt(f64)`, as `sqrt(0.5)` does.
The expected result type never selects an overload. An `f32` destination
may refine an otherwise unconstrained floating generic computation (§7.7),
provided resolution still selects that same overload.
* Multiple return values are declared as `fn f() -> A, B`. They are
received as `let a, b = f();`, or assigned to existing places as
`x, xs[i] = f();`. In such an assignment the call runs first, then each
target is assigned its result in order, as `x = f()` would assign one
result (a non-fixed result is built at its target; a reference to a
non-fixed value needs a `let` and an explicit `copy`, §4.1). Only `=`
takes several targets.
There is no tuple *type*: multiple returns are a calling convention, and
structs are the way to keep data together. (Function *types* with
multiple returns need parentheses, as in `fn(i64) -> (A, B)`, to
disambiguate the comma; declaration headers do not.) Nonfixed types are
allowed in any return position; each nonfixed result gets its own
destination per §7.3 (possibly on distinct stacks).
An ordinary value context takes only the first result; a call statement
discards them all. Only `return` and calls carry several values: a block
or branch cannot end in `a, b`, so `return if c { 1, 2 } else { 3, 4 };`
is an error, and each branch has to return its values itself. A
multi-name binding requires exactly that many results. `return f()`
forwards all of a multi-result call's results, adapting each to its
corresponding declared return type.
Declarations at top level are order-independent (whole-program compile);
only global initializers have ordered semantics (§11.1).
### 7.2 Parameter passing
Parameter passing works like `=` (§4.1): the parameter's type is the
destination, and the argument connects to it by size class. A fixed-size
argument is copied into a by-value parameter and bound by a reference-typed
one (`fn inc(n: i64&)` is called as `inc(n)`; `inc(&n)` means the same and
draws a warning). A non-fixed lvalue argument binds by reference to a
reference, slice or untyped parameter, and is an error for a by-value one.
`f(copy(xs))` writes the copy out explicitly, and `f(make())` constructs
the result in the parameter's slot (§4.3). Array→slice conversion at call
sites (§3.10) is the only change of shape; it is cheap and never copies.
For a by-value non-fixed parameter, the call site reserves the statically
assigned stack region for the value and constructs the value there, and
the callee owns it like any local. A parameter that the call leaves to its
default (§7.1) receives the default in the same way, as if it were the
argument written in its place: `xs: T[>..] = []` builds an empty array in
the slot, and `r: i64& = counter` binds the global.
### 7.3 Return values and result placement
**Normal completion.** The checker infers whether each function
specialization can resume its immediate caller. A body whose paths all
terminate, loop without an exit, or `return ... from` an outer function
does not resume that caller; this propagates through ordinary calls and
static function values. A variant-dispatched call has this property only
when every case does. A return targeting the function itself, including
one taken by a nested callee, resumes its caller normally.
A function declared to return values needs no result on a path ending in
such a call. For example, `fn fail() { abort("failed"); }` permits
`fn value() -> i64 { fail(); }`. This is a control-flow fact, not a value
type: `fail()` still produces no value to pass as an `i64` argument or to
forward in an explicit `return fail();`.
After a proven exit, the remaining statements and trailing expression in
that block are neither typechecked nor emitted. They still must parse.
This does not make checking depend on constant folding: both branches of
`if false { ... } else { ... }` are checked. Unknown recursive calls are
assumed to return, so inference may miss a recursive function that never
returns; it does not skip a continuation merely because the recursive
callee has not yet been checked.
Fixed-size returns use registers/native stack or a caller-owned data-stack
slot for large values (C.3). A variable or resizable return value is
**constructed directly in its destination** (§4.3). The callee is compiled
knowing the destination stack (statically or as a hidden argument), writes
element data there, and returns the value's metadata (lengths) outside the
element run. Where a result is first built with its packed value header,
the header is removed in place under §4.3's relocation exception
(Appendix C.3):
* `let x = f();` — a fresh stack region (statically assigned to `x`).
* `v.push(f());` / `v.append(f());` — the top of `v`'s stack; on return,
the push only adjusts `v.len` (the data is already in place).
* `g(f())` — the argument slot of `g` (its statically reserved stack).
* `x = f();` (resizable `x`) — `x`'s stack, replacing its contents, which
`f` may therefore not use (§4.4).
**Named results (guaranteed NRVO).** When every `return` of a nonfixed
result returns the same local variable (and those returns are its last
uses), that local is allocated at the return destination from its
declaration, so `return x` costs the same as returning the constructing
expression directly. The same holds where the other returns produce values
whose construction does not read or write that local, such as `return []`
or `return str("none")` beside `return x`. Such a value is an exit's value
as described under **Exits** below: it is built behind the local and moved
down over it. When different locals are returned on different paths, only
one of them can be placed at the destination, and the others are copied on
return. A returned local that another return's value may read or write is
copied as well. The compiler warns where a return keeps a returned local
out of its destination in this way. This is in addition to §4.3's layout
relocation.
**Exits.** A `return` or `break` may be taken while its destination already
holds part of a value: for example inside an element of a literal being
built there, or, for a `return from` (§7.9), below a named result built in
the destination the target handed down. Such an exit abandons that part.
The exit's own value is constructed behind the abandoned part and moved
down over it; only such an exit needs this copy. The `defer` bodies of the
blocks an exit leaves (§6.6) run after the exit's value is constructed and
before the blocks' storage is released.
**Destination requests.** The construct-here information handed to a callee
(or any constructing expression) comes in one of two forms, chosen by the
receiver:
* *one value of type R*, for receivers that store a value (`let`, params,
`=`, `push` of one element). R's nested metadata, if any, is part of the
constructed bytes.
* *a run of elements of type T*, for receivers that splice (`append`). The
callee emits raw elements and returns the count.
The request kind is part of the specialization signature (§10.2); the same
function may be compiled in both forms for different callers. A callee that
merely constructs its result passes the destination through. A callee that
must first operate on its result as a whole array may make one copy into
the requested element run, just before returning. Where its packed value
can occupy the receiving storage, removing the header is instead the
layout relocation of §4.3 and may be performed by the callee or receiver.
A local resizable can use metadata outside the element region
(Appendix C.2), with its elements already in place.
As a result, returning a built-up value costs the same as out-parameter
style, and the idiomatic way to build a variable element "inside" a
container is a function call in argument or push position.
### 7.4 Growing through a reference
Passing `&v` where `v` is resizable lets the callee push or append to it.
For each call site the compiler either specializes on the identity of the
stack that `v` tops (§10.2), or, when one body serves multiple stacks,
passes the stack as a hidden argument or fat reference (Appendix E).
No surface syntax distinguishes these.
An in-scope "current pool" mechanism (allocation without naming the array,
compile-time bound to whichever suitable array is in scope) is deferred
(TODO).
### 7.5 Nested functions and free variables
Functions may be declared inside functions. A nested function may read and
write the enclosing function's locals ("free variables"), subject to those
variables' normal rules (writability §9.5, roots §9.2, shrink rules §5).
For definite assignment (§4.4), a call assigns a free variable only if
every way out of the nested function's body assigns it. The ways out are
each `return` (including those of a function value's body and a callee's
`return … from`, §7.6, §7.9) and the end of the body. Where any of them may
have assigned the variable, a `let` declared without an initializer cannot
be given a value after the call.
A function value's body (§7.6) encloses what is written in it in the same
way: a function declared there, or a function value written there, may also
name the value's parameters and the body's locals.
Names in a nested function resolve where the function is declared, whatever
surrounds a call of it. A free variable is the variable in scope at the
declaration. It is not a variable of the same name that a scope around the
call declares, or that is declared further on, and it is never a variable
that one of the function's own type parameters hides (§11.1). The
functions a nested function can call are those in scope at its declaration
and every function declared in the blocks around its declaration, before or
after it. So nested functions may call each other in either order, as the
functions of a recursive descent parser do. Calling one of them before the
enclosing code has reached its declaration is an error. The enclosing code
can name a nested function from its declaration to the end of its scope,
and may pass the name directly as a static function argument (§7.6).
Nested functions with the same name declared in one block overload each
other as top-level functions do (§7.1), so case functions (§8.2) may be
nested and use free variables. A name refers to the overload set of the
innermost block that declares it. For the enclosing code, that set holds
the members declared so far; for a nested function's body, it holds all of
them, declared before or after the body. A later nested function with the
same parameter types as an earlier one shadows it instead of overloading
it, as a later local of the same name does. The enclosing code then calls
the latest one declared so far, and a nested function's body calls the
latest one declared at or before it, or else the first one after it. A set
in an inner block entirely hides the set of the same name in an outer
block, and the top-level one as well, as a namespace's set hides the
global one (§11.1).
Implementation model: free variables become hidden reference parameters of
the nested function. A nested function's value never outlives the function
that declares it (function values do not escape, §7.6), and a call made
where a variable the function names is out of scope is rejected, so every
call can pass the free variables. A nested function is specialized by how a
call finds its free variables, as it is by its parameters (§10.2): where
each one points, what it holds, whether it is assigned, and, for a `let`,
whether it may already have been assigned. A function that calls the
nested function is specialized in the same way, and so is a function that
calls a function value naming such a variable. A call that shares a
specialization with an earlier call leaves the free variables as the
earlier call's check did: rebound, assigned, and holding what the body
stored.
When a nested function is passed as a static function value and inlined
into its HOF, which is the expected, common case, the hidden parameters
disappear; un-inlined (debug) builds keep them as real arguments.
### 7.6 Static function values
```goose
fn foo(a: i64) { F(a); }
foo(1) { print(it) } // trailing-block sugar, implicit `it`
foo(1) { x => print(x) } // named block params
fn h(v: i64) { print(v) } foo(1, h) // or pass a named function
```
Function values are compile-time entities passed as generic parameters, not
runtime data. The `: fn(...)` bound is optional documentation, and a bare
`` works the same: the call `F(a)` is typechecked per instantiation like
everything else (passing a non-function produces the error at that call,
reported with the instantiation chain). Wherever `F` is in scope it names
the function value, hiding a variable of that name around a nested
function's declaration, and a global or function of that name (§11.1). It
is called as `F(a)`; a member call `a.F()` also names it, and is an error.
A block's value is checked without the destination **type** that its
call's value goes to, so it cannot be a `[]` (nor a construct whose
branches are all `[]`), which would have no element type (§4.2).
Its receiving storage is supplied independently of that type inference:
relative links in a fresh result are checked against where it will be
constructed, including when the block is called from another function.
A fresh block result follows the same construction rules as a named
function's result, including literals containing self-relative references.
Selecting a fresh result through `if`, `match`, `loop`, or `block` preserves
that property. A tail that instead names existing storage (or a reference
to it) is still a copy and must obey §3.9's relative-reference restrictions.
A function argument must be a function name (including a bound generic
function parameter) or a block literal. A name with several overloads
passes all of them, and each call of the value resolves among them as a
call by that name would. So `each(xs, sort)` with
`fn each(xs: i64[:]) { F(xs); }` calls std's one-argument `sort`, and
one value may reach different overloads at different calls. Runtime
expressions producing a function value, such as a call, `if`, `match`,
`block`, or `loop`, are rejected. Evaluate runtime work in ordinary
statements before the call; use locals when its ordering with other
arguments matters. A named function passed as a value keeps its parameter
defaults (§7.1): `F(x)` may leave out the arguments it declares defaults
for. A block's parameters have no defaults. A function value passed as an
argument may come after fewer arguments than the callee has parameters,
when the remaining parameters have defaults: `each_step(5, show)` for
`fn each_step(n: i64, step: i64 = 1)`.
Function values capture enclosing locals per §7.5, and cannot escape:
storing them, returning them, or putting them in data is a compile error.
Every call is direct and inlinable; HOFs compile to hardcoded loops
(Lobster-style guarantee). There are no closures-as-objects and no runtime
function pointers in v1.
A plain `return` inside a function value returns from the **lexically
enclosing named function**, not from the HOF that calls the value, so a
HOF-based iterator has the same structuring power as `for`. This is
implemented with the `return from` mechanism (§7.9), which unwinds the
HOF's frames transparently.
### 7.7 Generics and monomorphization
Generics use monomorphic specialization, C++/Lobster-style. Type
parameters are unconstrained, and the body is re-typechecked per unique
combination of (type arguments, reference roots (§9.2), writability
provenances (§9.5), static function values, destination stacks). Errors are
reported at the offending instantiation **with the compile-time call
chain**: because the compiler sees the whole program and checks it in
call-graph order, it can always show which call path produced the failing
instantiation. Every instantiation the program reaches is checked, so there
must be finitely many: a recursive call may not keep making new ones
(§7.8), nor may a generic type's fields (§3.2).
Type variables are never checked abstractly. A generic body is checked only
at an instantiation where every type is concrete. That includes the result
of every call the body makes on a function-value parameter, since that
value's body is checked inline against the concrete argument types at that
point (§7.6). So a HOF never needs to state what its function value
returns: `map`'s result element type is the type of `F(x)` in that
instantiation, even when the block is `{ generic(it) }` and that type
depends on the instantiation.
**Call-site type arguments.** Type arguments are inferred from the argument
types whenever they appear in the parameter list: `fn foo(x: T)` can
be called as `foo(1)` without an explicit type argument. The typechecker
must support inference for both ``-style and untyped (implicitly generic)
parameters. Where several arguments mention one type variable, the typed
arguments bind it and a literal then adapts (`max(n, 0)` with `n: u32` is
the `u32` instantiation). So an `i64` literal never fixes the type by
coming first, and neither does an array literal of constants (§4.2). Among
literals, a float literal binds before an integer literal, which converts
to the float type (§6.3): `max(1, 0.5)` is the `f64` instantiation. A float
expression of literals and integers (`n * 0.5`) binds as a float literal
does. A `[]` or `null` binds nothing. It takes the type that the other
arguments, literals included, give its parameter, wherever it stands among
them: `both([], xs)` for `fn both(x: T[], y: T[])` is the same
instantiation as `both(xs, [])`. An explicit list `f(x)` is allowed.
It is *needed* when no argument mentions the parameter (e.g. `qget()`,
or `zero()` for `fn zero() -> T`), or when only a `[]` or `null`
does (`g([])` for `fn g(xs: T[])`). It binds the leading type
parameters in order, and the rest are inferred. Syntactically, `f<` commits
to a type argument list only when the `<…>` is immediately followed by `(`,
by `{` for a struct literal (`Pair { … }`), or by `.ident {` for a
variant literal (`Opt.Some { … }`); otherwise `<` is the comparison
operator.
**Floating computation context.** After ordinary argument inference and
overload resolution, an `f32` destination can refine a call's inferred
`f64` type parameter to `f32`. This applies when the declaration returns
that bare type parameter (`-> T`) and every parameter mentioning it is a
written, bare value parameter (`x: T`). Its arguments must be adaptable
float literals or expressions (§6.3), adaptable integer constants, or directly
nested calls satisfying this same rule. Matching those arguments as `f32`
must select the same overload with all other bindings unchanged; ambiguity
or a different selected overload prevents the refinement. Normal resolution
must succeed first: result context never rescues a failed or ambiguous call.
With integer `width` and `height`, the destination below therefore chooses
`f32` for both calls and their argument arithmetic:
```goose
let scale: f32 = max(0.5, min(width / 960.0, height / 600.0));
```
An explicit type argument for `T`, an already typed numeric argument, a reference or
container parameter mentioning `T`, or a default supplying such a parameter
commits its type. The rule does not infer a type parameter from the result
alone, change an integer computation into floating arithmetic, or extend
through an intervening operator or stored call result. Overload selection
still sees the call's ordinary inferred type. Without an `f32` destination,
calls continue to default to `f64`; an inferred binding such as
`let saved = min(width / 960.0, height / 600.0);` stores an `f64` result.
This is not a general change to mixed-argument unification. With
`var count: i32 = 1;`, `let value: f32 = min(max(0.25, 0.5), count);`
still errs: `count` is a typed integer argument, so result context does
not refine that call. Reversing the arguments does not make it valid.
Write `min(max(0.25, 0.5), count)` to choose floating arithmetic
explicitly for that mixture. In contrast, `count * 0.5` is an adaptable
floating expression and can participate in the contextual rule above.
An explicit inner `min(...)` also chooses `f32`. Alternatively,
`max(0.5, min(width / 960.0, height / 600.0)) as f32` computes in `f64` and
then rounds the result once: a cast does not pass its target type into its
operand. These are different choices:
`min(n * 0.1, 1.0)` with integer `n = 9` computes `0.90000004`, while
`min(n * 0.1, 1.0) as f32` computes `0.9`. A typed intermediate is also
valid, but is not needed just to choose a generic call's type.
**Literal arguments.** Where a literal is the only thing binding a type
variable (the parameter is untyped, or its type is a bare `T` that no typed
argument mentions), the variable takes the literal's default type (§3.1).
Inside the specialization the parameter is then a *literal parameter*: a
constant of unknown value that adapts wherever the bare parameter meets a
type, as the literal would have. So `push_n(flags, 1, n)` with
`fn push_n(xs: A&, v: T, n: i64)` pushes a `u8` into a `u8[>..]`,
and `fill(small, -7, 2)` pushes an `i16` into an `i16[..4]`, with no
suffix or cast at the call. The value is not part of the specialization:
every call with a literal shares one body, which is checked once with the
value unknown. The body records each type the parameter adapted to: in the
body itself, in a callee it passed the parameter on to as a literal, and
in a block it handed the parameter to. Once the whole program is checked
and the records are complete, each call's literal is checked against every
recorded type: `push_n(flags, 300, n)` is an error naming the `push` that
takes a `u8`. What the body cannot do with an unknown value, it cannot do
with a literal parameter either: `[0; n]` needs a constant, and `v + 1` is
an ordinary `i64` expression, just as `let y = v;` commits `y` to `i64`,
so a recursive call on `v + 1` passes a typed value and terminates. `var`
parameters and `extern` functions take literals as ordinary values.
Passing arrays by reference is the generic way to write mutating range
algorithms (each array-family type instantiates its own copy); slices are
the uniform non-mutating way.
### 7.8 Recursion
Non-recursive call graphs are the default and have no special
requirements. Recursion is opt-in and annotated:
* The entry function of every recursive cycle is marked `recursive fn`.
The keyword is all that is written: roots and destinations follow from
the entry call as for any specialization (§7.7).
* All functions in the cycle need fully explicit signatures, since there is
no inference across the cycle's back edge. A result inferred as a
reference (§7.1) is an error in any of them, since the back edges need
its roots before its returns are checked.
* Most importantly, **no function in the cycle may call into the cycle
while one of its nonfixed locals is in scope**.
A call into the cycle is any call that can lead back to the caller: the
back edge, and every other call reaching a member of the cycle. A nonfixed
local is a variable- or resizable-class `let` or `var`, or a `match` arm's
by-value copy of such a payload. It holds a data stack (§10.3) from its
declaration to the end of its block. Its initializer builds it in place
(§4.3), so it also counts at a call in its own initializer. If a nonfixed
local were in scope at a call into the cycle, every activation would hold a
stack of its own. So a scratch buffer declared in a block or a loop body
that ends before the recursive call adds no stacks to the recursion: its
stack is free again before any deeper activation starts. A scratch buffer
declared before a loop whose body recurses is still in scope at that call,
and is an error there, reported with the local. A by-value nonfixed
*parameter* is in scope for the whole body, so no function in a cycle can
have one: it takes a reference or a slice instead. A function value's body
is part of the function it is written in, so the locals the body declares
count where it calls into the cycle, as do the locals of the function
calling it. A nested function in the cycle is a member like any other,
while the locals of an enclosing function outside the cycle are owned
outside the cycle (see below). Growable data that must outlive a recursive
call is owned outside the cycle and passed in (references, slices, reusable
pools, or one struct of references to several tables, A.6). The compiler
checks every call into a cycle in call-graph order; recursion depth then
only consumes native call stack.
**Temporaries.** The same holds for the unnamed nonfixed values of the
statement that makes the call: **at a call into the cycle, no nonfixed
value of the enclosing statement may be live**. Each such value lives on a
data stack of the activation (§10.3), a temporary until its statement ends
(§9.2) and a value under construction until it is complete, so it would
otherwise need a stack per activation, as a local in scope does. That
forbids:
* (a) a nonfixed temporary as an argument of the call, which the callee's
view keeps alive across the call: a call's result, a `copy`, a `str`, a
literal of nonfixed class, or the copy a by-value nonfixed parameter
takes of its argument (§4.1);
* (b) the call nested inside a nonfixed value under construction: in an
element or field initializer of an array or struct literal of nonfixed
class, or in an argument of `print` or `str`, whose text is being
rendered (`format` is not such a value: its text goes into the storage it
is given);
* (c) a nonfixed temporary evaluated earlier in the same statement, whether
or not something has consumed it since. A temporary lives to the end of
its statement. So does a temporary built by the head of a construct the
call is inside (an `if` condition, a `match` scrutinee, a `while`
condition, a `for` sequence), since the head runs in the scope around the
construct;
* (d) the call's own nonfixed result anywhere except in these two
positions:
* as the whole value of a `return` of the same result type (including
when it reaches the `return` through the branches of an `if` or
`match` and a block's tail, or is returned by `return … from`), where
it is built in the caller's destination;
* pushed or appended in place into resizable storage owned outside the
cycle, where it is built at that storage's top. §1.3(4) then rejects
this wherever a function of the cycle grows that storage, as every
function appending into it does.
Anywhere else, such as an argument, an operand, a dropped statement
value, an element of a literal, or a value assigned (which §4.4 rejects
as well), the result needs a destination temporary taken before the call
and held across it.
The compiler reports each case with the call and the value. The
alternatives are to evaluate the value in a statement of its own before the
call (`let width = str(n).len;`, where the binding is fixed-size), to pass
scratch in from outside the cycle (the `mark` / `resize(mark)` pattern
below), or to take an output buffer by reference and build into it in place
instead of returning a growable value. Fixed-size values and temporaries
are never affected: a slice of a fixed literal, a struct of fixed fields
and a large fixed value (§10.3) all use no data stack at the call. So a
program's stack count is a compile-time constant, and the number of worker
threads (§11.2) is the only dynamic factor.
Scratch passed down this way can be viewed across the recursive calls. An
activation that takes `let mark = scratch.len` of a grow-shrink scratch
array, pushes onto it, views its own part and ends with
`scratch.resize(mark)` leaves the array as long as it found it, so its calls
back into the cycle are balanced (§5.2) and the view stays in use across
them:
```goose
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;
}
```
**Polymorphic recursion.** A recursive call may instantiate its callee with
different type arguments from those of the call it is in (§7.7):
`flip` calling `flip` is back at `flip` one round later,
and that call is the cycle's back edge. The instantiations a recursion
reaches must still be finite. Every branch of every specialization is
checked, whatever values reach it at run time, so a call whose types grow
with each round (`g(a, n - 1)` with `let a: T[1] = [x]` inside `g`, or
`grow(n - 1)` inside `grow`) would instantiate without end, even
where `n` bounds the recursion at run time. The compiler rejects such a
call once it would put more than 16 specializations of one function in
progress on one compile-time call path, similar to how C++ bounds template
instantiation depth, and reports it with the chain of instantiations that
led there. A finite recursion has one specialization in progress per
instantiation on its cycle, or per level of a nested type it descends
through, so only an unusually deep recursion reaches the limit.
**Cycle store rule.** Within a recursive cycle, distinct activations of the
same local are statically indistinguishable, so the §9.2 depth check is not
sufficient there. Therefore a reference whose root is a local of a cycle
function may be passed *down* as an argument, but may not be stored into
any location, nor returned. A function is in the cycle for its whole body,
not just from its call into the cycle on: the stores it makes before that
call are inside the cycle too. Rebinding one of the activation's own
reference variables (`cur .= cur.next`) is not such a store: the variable
dies with the activation, and what it is bound to came from an activation
that outlives it.
References rooted outside the cycle are unrestricted. In particular, this
covers a pool handed to the cycle by reference: a parameter whose pointee
is resizable-class (no cycle function can hand such a value on to the
cycle, since it holds none across a call into the cycle). So a recursive
builder can push into a caller's local pool and link what it pushed. A
local of an enclosing function outside the cycle (a free variable, §7.5,
of a nested `recursive fn`) also outlives every activation. So a parser
keeps its pool, its key table and its input as locals of a non-recursive
`parse`, and the nested recursive functions freely store, link and return
references rooted at them. Because the cycle's functions are checked
against the entry call's roots, every recursive call must pass such a pool
by the same reference the entry call did: swapping two pools, or passing a
different one, at a back edge is a compile error.
A reference or slice parameter that the cycle passes on is rooted outside
the cycle in the same way, whatever the size of its pointee. Suppose that:
* the call that first reached the function rooted the parameter's argument
exactly, outside every recursive cycle (at a local of a function that is
neither recursive nor in a cycle, or at a global), and
* every call back into the cycle passes the parameter on: the parameter
itself, a subslice or field of it, or a parameter of the calling
activation passed on in the same way.
The parameter then points at the same storage in every activation. References
rooted at it or read through it may be stored inside the cycle, provided the
destination storage is also the same in every activation. This can be a local, a
pool, or storage pointed to or reached through another parameter with the same
guarantee. It cannot be storage pointed to or reached through a parameter that
the cycle passes a different value. For example, a recursive walk over a context
of borrowed tables (A.6) can store views read through the context into tables
reached through that context.
A parameter whose argument on every call is a reference or slice read
through one such parameter, or through a pool, also points outside the
cycle in every activation, though not at the same storage. So what is
rooted at that parameter may be stored in the same way, but the parameter
is not itself storage to store into. For example, a builder hands the node
read through the context to the function that links it under a new node
(`rest(c, c.nodes.push(…))`). Nothing requires a recursive call to pass
such a parameter on, so the parameter is restricted only once a store
relies on it. A later call into the cycle that passes it anything else is a
compile error reported at the store, and such a call before any store makes
the store the error. (Further refinements are future work, TODO.)
**Cycle return roots.** A back edge reaches a function whose own returns may
not have been checked yet, so the roots of its result cannot be taken from
those returns right away. Instead, **a cycle is checked in rounds**:
* In the first round, a back edge's result points nowhere yet. Nothing
checks or stores it, and the back edge does not shrink, grow, store or
rebind anything.
* Each later round gives every back edge what the round before recorded of
its callee: the union of the roots its returns give, one root for each
way a return can go, mapped through the back edge's own arguments, and
read-only where any return is. A back edge reuses the body whatever it
passes, so it maps a parameter's root through the arguments it gives the
parameters that the entry call gave that root (it may give two of them
two arrays where the entry call gave one), and merges the lot as a call
merges its callee's returns (§9.2).
* The errors that stand are those of the round after the last round that
changed anything. That includes a variable outside the cycle, which is
one variable at every level of a nested function's recursion (§7.5): a
round that rebinds it, or stores into it, has read it for the next level
as it was before.
A cycle function may therefore `return` the result of a back-edge call, and
a parenthesised subexpression in a recursive-descent parser needs no
wrapper node (`bench/goose/calc_noparen.goose`). A cycle function that
returns either of two pools it is handed can store the result where both
pools outlive the store. A cycle function that returns a view of a
grow-shrink array on any path has a result that no store may keep (§5.2). A
result that *holds* references, such as a struct with slice fields, is
checked the same way, by the roots of what it holds (§9.2). So a recursive
builder may return views into an arena that outlives the cycle, built from
its children's views, with the recursive call either before or after the
base case's return:
```goose
struct Fragment { text: const u8[:] }
var arena: u8[>..] = [];
recursive fn nest(n: i64) -> Fragment {
if n == 0 { arena.append("x"); return Fragment { text: arena[..] }; }
let child = nest(n - 1);
return Fragment { text: child.text };
}
```
### 7.9 `return … from` (long-distance return)
```goose
return E from f // f = a named function on every compile-time call path
// from f to this statement
```
`f` is resolved where the returning function is written: it names a nested
function in scope, or else the name's overload set by the rules of §11.1
(`from image::parse` is explicit). The target is the innermost enclosing
call of a function in that set. An unrelated function that merely shares the
leaf name never catches the return.
`return E from f` returns `E` as the result of the innermost active call of
`f`, unwinding all frames in between. It is the language's opt-in,
lightweight "exception" mechanism (e.g. a deep parser/loader error doing
`return err from load_level`), and the mechanism behind function-value
`return` (§7.6).
Semantics and implementation:
* Validity is checked statically in call-graph order: every call site of the
returning function must lie within the dynamic extent of an `f` call.
* Each function on a path between `f` and the `return from` gains a hidden
return discriminant: "result is for my caller" vs "propagate for target
`f`" (an enum when multiple targets are possible). Callers check it and
immediately return in the propagate case. Normal epilogues run, so all
intermediate data-stack watermarks restore correctly, and the `defer`
bodies of the frames left run in them (§6.6); a body uses no
long-distance return itself, so the one in flight is undisturbed. No
unwinder or unwind tables are needed, and there are no destructors to
run.
* `E`'s destination follows §7.3 for `f`'s call site. No special stack rule
is needed: a pending nonfixed return's destination stack is in the in-use
watermark from `f`'s call site through `f`'s entire dynamic extent
(§10.3), so no function between `f` and the `return from` can own data on
it, and unwinding intermediate frames cannot disturb the in-flight value.
What they had built there toward `f`'s result is abandoned, as for any
exit (§7.3).
* Multiple `return from` sites and multiple targets compose; agreement with
`f`'s return type applies as usual.
Error handling has **no required error-value convention**: an application
may use a bool, enum, string, i64, or another type. By convention, the error
is the last of multiple return values. Use ordinary multiple returns for
local propagation and `return from` to return across several calls.
---
### 7.10 `extern fn`
```goose
extern fn sqrt(x: f64) -> f64;
extern fn sqrt(x: f32) -> f32 = "sqrtf";
extern fn read_file(path: u8[:], out: u8[>..]&) -> bool = "gs_os_read_file";
extern fn gl_vertex3(v: float3&);
```
A declaration without a body binds a Goose signature to a C function. A
string after `=`, where a body would go, is the C symbol (by default, the
symbol is the Goose name). The older syntax with the string after `extern`,
`extern "sqrtf" fn sqrt(x: f32) -> f32;`, is deprecated. It binds the same
way and draws a warning naming the `= "sqrtf"` form (except in a package
fetched from a git source, since it is up to that package's authors to
change it). A declaration giving both strings is an error. An `extern fn`
is top-level only, fully typed, not generic, and joins overload resolution
like any function. A call compiles to a direct C call with no
calling-convention extras, and a prototype is emitted from the declaration
unless the runtime declares the symbol (the `os` library's `gs_os_*`
functions, declared in `src/runtime/runtime_ext.h` and defined in
`src/runtime/runtime_os.h`). User C code is added to the program with
`--include `, which is emitted after the generated type
declarations so that a header can implement shims against the generated
typedefs. The types in every `extern fn` signature are emitted even when
the function is unused, including the complete types behind reference and
slice parameters. An unused extern does not require a C implementation to
link.
Every name that comes from the program appears in the C with a `_g`
suffix: a struct `Stats` with a field `lo` becomes `Stats_g` with `lo_g`.
This keeps the generated file clear of the C keywords and of whatever the
platform's headers declare, without depending on a list of names to avoid.
A namespaced declaration `ns::x` (§11.1) becomes `ns_x_g` followed by the
namespace's length (`image::Pixel` is `image_Pixel_g5`), so `a::b_c` and
`a_b::c` stay distinct and no namespaced name coincides with a global one.
The exception is an `extern fn`'s own symbol, which is the C name the
declaration gives, unchanged.
An `extern fn`'s trailing parameters may have defaults (§7.1). Each call
passes them, so the C function always receives every argument.
These values can cross the boundary (with layouts per C.2):
* the integer and float scalars and `bool`;
* any flat fixed-size value (including structs, fixed and static-capacity
limited arrays, and fixed-mode ADTs), by value as its packed C type;
* `T&` to such a `T`, as a pointer;
* `T[:]` of such a `T`, as the slice struct (`{ data, len }`);
* `u8[>..]&`, as the resizable's header plus its stack (`gs_rref`), which
C appends to through `gs_bld_append(ref, p, n)`. C compiled on its own,
such as a package's C, finds both in `goose.h` (docs/packages.md).
A return is one scalar or flat fixed-size value by value, or nothing;
references, slices and builders can only be parameters. Everything else
(varints, references inside structs, variable and resizable values by
value, slices of variable elements, optionals) is rejected at the
declaration. A parameter of reference or slice type is as writable as its
type says (§9.5): passing a read-only argument requires the parameter to be
declared `const`.
C must preserve the types and lifetimes of borrowed storage, must not
retain a borrowed argument beyond the call, and may only append to a
builder. The compiler assumes an extern may write through its arguments
but does not mutate unrelated Goose globals; C code is responsible for
honoring those obligations.
**Binding from a shared library.** An `extern` block names a shared
library and declares functions that are bound from it by name when the
program starts, without any C code of the program's own and without
linking:
```goose
extern "SDL2" {
fn SDL_Init(flags: u32) -> i32;
fn SDL_Quit();
fn SDL_GetTicks64() -> u64;
fn delay(ms: u32) = "SDL_Delay";
}
```
A member is an `extern fn` in every respect above. It is fully typed,
non-generic, and has no body. It accepts the same shapes,
with one exception: a struct holding a `pad` by value must cross by reference.
The library's struct has padding where Goose's generated C has a byte array,
and the calling convention may pass these differently. A string after `=` gives
its C symbol, which is otherwise its Goose leaf name. Its name is a
`declname`, so a namespaced member is written as it would be outside the
block. A member is written without `extern`, since the block's `extern`
covers it. `export`, `thread_fn` and the qualifiers an `extern fn` cannot
carry are errors inside the block, with the same messages as on an
`extern fn`. A block is top-level only, may not nest, and may be empty.
Two declarations may bind one symbol under different Goose signatures,
each through a pointer of its own. A member that no call reaches is never
looked up, and a library none of whose members is called is never opened.
The string is a bare name or a file name, interpreted for the platform the
program runs on:
| string | Windows | macOS | Linux |
|---|---|---|---|
| `"SDL2"` (no separator, no dot) | `SDL2.dll` | `libSDL2.dylib` | `libSDL2.so` |
| `"libfoo.so.3"`, `"foo.dll"` (a dot) | as written | as written | as written |
| `"bin/steam_api64"`, `"./x.so"` (a separator) | relative to the program's resource directory (`resource_dir()`, docs/stdlib.md) unless absolute; the bare-name rule applies to the last component |
A bare name is passed to the platform's loader as a bare name, and the
loader searches for it in its usual way: the DLL search order on Windows,
`DYLD_LIBRARY_PATH` and the usual places on macOS, and `LD_LIBRARY_PATH`,
the rpath and the cache on Linux. A dotted file name is taken literally
and is therefore specific to one platform; it belongs in the sidecar
rather than the source, unless the program targets only one platform. The
sidecar's `dylibs` list (which takes the `_windows`, `_linux` and `_macos`
suffixes, as every list does) maps a name that a block uses to the
spelling to open, as `name=spelling`. A package's sidecar maps its own
names as defaults, and the program's sidecar can map a name for any package
it uses. For a platform that the program's entry says anything about, the
program's entry takes precedence. A name that no block declares is an
error at the sidecar (docs/packages.md).
The libraries are opened and the symbols resolved on the main thread when
the program starts, after the runtime starts and before the globals'
initializers run. The pointers are written once and only read after that.
A library that cannot be opened, or a symbol it lacks, is a runtime error
naming the spelling tried, and the program does not start:
```
cannot load SDL2 (libSDL2.so): libSDL2.so: cannot open shared object file: No such file or directory
SDL2 (libSDL2.so) has no symbol SDL_Foo
```
A library is never closed. A package's `main_thread_only` restriction
(docs/packages.md) applies to the members of its blocks as it does to its
other externs. A variadic C function cannot be bound: the declaration has
no way to say so, and calling one through a fixed-arity pointer is
undefined. The generated C is the same on every platform, since the
runtime chooses the spelling, and a program whose only C is such libraries
runs under the in-process backend with no C toolchain installed.
Conventions for declaring a real library's functions under these shapes
are in docs/reference.md §24.
### 7.11 Exporting Goose functions to C
```goose
export fn hello_world() -> const u8[:] {
return "Hello, world!"
}
export "c_sum" fn sum(a: i32, b: i32) -> i32 { a + b }
```
An `export fn` defines a normal Goose function and emits a C-callable wrapper.
The optional string names its C symbol; otherwise the function's leaf name is
used. Exported functions must be top-level, non-generic, have explicit
parameter types, and have at most one explicitly typed return value. Default
arguments, `return ... from`, reusable-pool parameters, and string-builder
parameters are not supported at the C boundary. Parameters otherwise use the
same fixed-size C shapes as §7.10.
Scalars and flat fixed-size returns are returned by value. A read-only
`const u8[:]` return is exposed as `const uint8_t *` and an additional
`int64_t *out_len` parameter; the returned bytes are not NUL-terminated, and
their storage remains borrowed according to the Goose function's lifetime.
Other references, slices, and variable-size returns are rejected.
Generated C retains its normal `main` unless the program is compiled with
`--header out.h`, which builds it for a C host: the generated C has no `main`
and a source without a Goose `fn main()` is accepted. The header declares
`goose_init(argc, argv)`, which a C host must call before any exported
function. It initializes the runtime and the globals and then runs the Goose
`fn main()`, if there is one, so `main` is where a library keeps its own
start-up code; exported functions may be called from it. A second call does
nothing. Initialization is idempotent on the initializing thread; exported
calls and mutable Goose globals are not thread-safe and must remain on that
thread. `--header` requires generated C output and at least one export, and
writes declarations and the required packed C type definitions for the
exported API.
An exported C symbol is emitted as written, as an `extern` one is, and must be a
C identifier that is not a C keyword and that the program does not otherwise
reserve (`main`, `goose_init`, or a name beginning with `gs_`).
The wrapper calls the normal generated Goose specialization, not its hidden
internal C signature. This keeps Goose's stack argument and other compiler
implementation details out of the function's public C signature.
### 7.12 `simd fn`: versions per instruction set
```goose
simd fn encode(out: u8[>..]&, src: u8[:]) { ... }
```
`simd` asks the implementation to build the function once for the
baseline instruction set of its target and once more for each wider one it
supports (AVX2 and AVX-512 on x86-64), and, every time the function is
called, to run the widest version that the CPU running the program
supports. It is meant for a function containing a hot loop that the C
compiler can vectorize, but only poorly or not at all with the baseline's
instruction set, such as byte transforms, codecs and kernels over arrays.
`simd` has no meaning of its own: every version computes exactly what the
baseline does, so a program's output, aborts and exit status never depend
on which version runs, or on whether there are versions at all. Integers
already compute at their exact widths (§6.2), checks abort where they
would, and float operations round as the baseline's do, one rounding per
operation: no version fuses a multiply and an add into one rounding where
the baseline would not. An implementation may ignore `simd` entirely, as
the C backend does under compilers other than clang on x86-64 and under
the JIT.
The one exception is the standard library's `mul_add(x, y, z)` (`math`),
which asks for `x * y + z` rounded once where the hardware can do so and
accepts it rounded twice where it cannot. Which one a call gets is up to
the implementation. It may differ between targets, between builds for one
target, and between a function's versions. Because the product is not
rounded on its own, the two can differ by much more than the last bit:
where `x * y` and `z` nearly cancel, including a zero against a nonzero,
and where `x * y` overflows but the sum does not. The C backend rounds once
on arm64, in x86-64 code built for AVX2 or later, and in a `simd`
function's AVX2 and AVX-512 versions. It rounds twice everywhere else,
under MSVC and under the JIT. A `*` followed by a `+` is never fused, `mul_add` or not.
Versions are per function: each version contains everything the function
contains, including the callees inlined into it. The function itself stays
out of line, since a caller it was inlined into would run the baseline's
code. Each call to it performs one test of the CPU's level, so `simd` suits
a function that runs a loop rather than a function called from inside a
loop. The cost is code size: the function's body appears once per version.
`simd` comes after `export` and before `recursive`. It applies to
functions with bodies, including nested and generic ones (each
specialization has its own versions), and `extern simd fn` is an error.
### 7.13 `inline fn`: inlined whatever its size
```goose
inline fn project(cam: Camera, p: double3) -> f64, f64, bool { ... }
```
`inline` asks for the function to be inlined at every call, however big
it is and however many calls there are. Without it, the implementation
decides from the function's size and its number of calls, and a function
called from a hot loop can stay a call because it is called from several
places.
`inline` has no meaning of its own: a program computes the same with or
without it. It is a request the implementation may not be able to grant
at every call. Where Goose cannot inline a function itself (one returning
several values, §7.3), the C backend asks the C compiler to inline it
instead. Where inlining is off (`-O0`), every call stays a call.
`inline` is a keyword only directly before `fn`, `recursive` or
`thread_fn`, so it stays usable as a name. It is an error together with
`extern` (no body to copy), `export` (its C wrapper calls it), `simd` (a
caller would run the baseline only), `recursive` and `thread_fn`.
### 7.14 `checkfree fn`: no bounds check left
```goose
checkfree fn bfs(dist: i32[>..]&, q: NodeId[>..<]&, src: NodeId) -> i64 { ... }
```
`checkfree` asks the compiler to reject the program if a bounds check is
left anywhere in the function's text after bounds-check elimination
(§10.5): in each specialization of it, each `simd` version and each copy
inlined into a caller. It changes nothing about what the program computes
or how it is compiled. A check belongs to the text it is written in, so a
caller's argument expression is the caller's, and a nested function or
block written inside a `checkfree` function is checked as part of it.
* **What counts.** Only the index and slice checks of §9.3 and conversions
into index and bound types (§3.11) count. The other aborts of §9.3 are
still allowed, `assert` in particular, which is how such a function
states its preconditions once, outside its loops.
* **What it calls.** Every call in a `checkfree` function is to a
`checkfree` function (directly, through a case function's dispatch or a
function value naming one, or a `format` overload a `print` reaches), a
builtin or an `extern fn`; a block it calls must be written in a
`checkfree` function. `inline` does not exempt a callee: a call that
stays a call runs the callee's own checks.
* **What it may rely on.** It may rely on the analysis's facts, including
those from call sites (§10.5). So a new caller that does not establish
what an entry relied on can make a `checkfree` function fail to compile;
the error names the check and why it remained. Index types in the
signature and `assert`s at entry make the function independent of its
callers. Inside a `checkfree` function, an implementation must prove at
least the following:
* a constant index into a fixed array in range;
* an index into a fixed array whose type's range fits the array;
* an index of `a`'s index type into `a`;
* an index of `a`'s index type into `b` where `b.len >= a.len` was
asserted, with no growth of `a` and no shrink of `b` since;
* a `for` loop's index into the array whose length is the loop's count
or range end;
* `a[i]` where a condition established `0 <= i < a.len` and neither `i`
nor `a` has changed since;
* a repeat of a check already passed on the same path.
Anything else it proves is implementation-defined.
* `checkfree` is a keyword only directly before `inline`, `simd`,
`recursive`, `fn` or `thread_fn`. It comes after `export`, and is an
error with `extern`. A nested function inherits it. Under `--no-bce`
nothing is verified, and the compiler warns about this.
## 8. ADTs in use
### 8.1 `match`
```goose
match shape {
Circle c => c.r * c.r * 3.14159, // payload by value (a copy)
Rect r => r.w * r.h,
Point => 0.0,
}
match sexp {
Sym &s => use(&s.name), // payload by reference (&-binder)
List &l => walk(l.kids),
}
match n { 0 => "zero", 1..10 => "small", _ => "big" }
match t { GROUND => "ground", world::GATE => "gate", LO..HI => "mid", _ => "other" }
match d { 0, 6 => "weekend", 1..6 => "weekday", _ => "no day" }
match dir { North, South => "vertical", East, West => "horizontal" }
```
* Over ADTs: a match is exhaustive over the variants. A `_ =>` wildcard is
allowed, but never in a list. An arm names a variant and may bind its
payload (`c: Shape.Circle` above), or lists several variants
(`North, South =>`) and binds no payload, since their payloads differ in
type. No variant is named twice.
* Over integers (any integer type): an arm is a constant, a half-open
range, or a comma-separated list of them (`0, 6`, `1, 3..5, LAST`) that
matches when any one of them does. `_` is required, and never in a
list. Every pattern value must fit the scrutinee's type. The first arm
that matches is taken. A constant or a range bound is an integer literal
or a named constant, either one optionally negated: `GROUND`,
`world::GATE`, `::GATE`, `LO..HI`, `-LIMIT..0`. A named constant is a
`let` or `const` global whose initializer is a constant expression,
taken at that value; an array size may name the same kind of constant
(§11.1). The name resolves as any other does (§11.1): a local, type
parameter or nested function of that name hides the global and is not a
constant, and a `var` global is not a constant either. A bare name is a
variant when the scrutinee is an ADT, and a constant when it is an
integer; as a constant it binds nothing.
* Every pattern must be able to select its arm: `_` is the last arm, and a
value or range that earlier arms already match completely is an error
(`1..5 => a, 3 => b`). Overlap is fine as long as each pattern still has
something left to match: `0 => "none", 0..10 => "few"` sends 1 to 9 to
its second arm. Within one arm, a pattern listed twice and literals that
overlap are errors, but differently named constants may overlap, since
they may be aliases of one value (`EAGAIN, EWOULDBLOCK`, which have the
same number on Linux).
* Arm binders are explicit about copy vs reference, like the rest of the
language. `Circle c =>` binds the payload *by value*: a copy, potentially
a large one for variable-size payloads, which may not be written to, just
as a `for` copy may not (§6.5). `Circle &c =>` binds it *by reference*,
in the same form as `for &x` (`Circle& c` is the same tokens).
* `&`-binders are legal only on **variable-mode** payloads (whether the
scrutinee is a value or a reference). A fixed-mode value may be
overwritten with another variant, including inside the arm, so a
reference into its payload is exactly what §3.5 forbids, and matching a
fixed-mode ADT (even through a reference) offers by-value binding only.
So does matching a resizable ADT, since a whole assignment can give it
another variant in the same way (§4.4). The variant can never be reassigned
through a `&`-binder. A payload holding self-relative references binds
by reference only (§3.9), which is why an ADT with such payloads is used
in variable mode, and a resizable ADT holds them only in its resizable
payloads (§3.5).
* An arm list may hold `const for` expansions (§6.5) beside written arms;
their arms are flattened first, and every rule above applies to the result.
* `T?` narrows via `if r { … }` / `guard` / `assert(r)` (flow typing, §3.8).
### 8.2 Case functions (match as an overload set)
The cases of a `match` may instead be written as separate functions, each
taking one *variant type* in the same parameter position:
```goose
fn area(c: Shape.Circle) -> f64 { c.r * c.r * 3.14159 }
fn area(r: Shape.Rect) -> f64 { r.w * r.h }
fn area(p: Shape.Point) -> f64 { 0.0 }
let a = area(s); // s: Shape — dispatches on the tag, like a match
```
* Variant types (`Shape.Circle`) are types in their own right, usable
directly and behind references (`Shape.Circle&`).
* Calling the overload set with the ADT type (or a reference to it)
performs tag dispatch (a jump table). Exhaustiveness is checked as for
`match`: every variant must have exactly one applicable overload, and the
return types must agree. All arms construct any nonfixed result in the
same destination (§4.3). The overloads' parameter types choose copy or
reference as match binders do (`Shape.Circle` vs `Shape.Circle&`), with
the same rules: a fixed-mode or resizable scrutinee, even behind a
reference, dispatches only to by-value variant parameters (§3.5), and a
payload holding self-relative references dispatches only to by-reference
ones (§3.9).
* Dispatch is on one parameter position (v1 rule: multi-position dispatch is
an error). Other parameters pass through unchanged.
* A dispatched call writes out every argument. It evaluates the arguments
once, before the tag picks the case, so no case's defaults (§7.1) are
passed, and a call that could dispatch only by leaving parameters to
their defaults is an error.
This is the language's "virtual function" idiom, without vtables or
inheritance, and it keeps the closed-world exhaustiveness guarantee.
---
## 9. References, lifetimes, and safety
### 9.1 The outlives rule
**A reference/slice must not outlive the variable that owns its target, and
must never observe its target at a wrong type.** Everything below enforces
this rule statically. The surface language has no lifetime annotations and
no aliasing or exclusivity restrictions. Aliased references are allowed:
without shared-memory concurrency, and given the layout rules of §3/§5,
aliasing alone cannot break type safety. This outlives-rule is the only
"borrow checker" the language has.
### 9.2 Roots and the depth check
Every reference/slice value has one or more static **roots**. Each root is a
local or global variable, or a temporary (below), that bounds the scope the
target lives in. A value that may point at several places has one root per
place (merged values, below), and every rule is checked against each of
them. A root is **exact** when that variable's own storage contains the
target, and inexact when it only bounds the target's lifetime; the owner is
then that variable or a variable further out.
Every root that a `&lvalue` creates is exact; inexact roots come from the
reads out of containers described in §9.5. Because functions are compiled in
call-graph order and specialized per instantiation, roots are always
statically known: a parameter's roots come from each call site, and a
function is specialized per distinct root. This gives the precision of Rust
lifetimes through monomorphization, with no syntax.
The rules below are the *scope* rules, and they apply to exact and inexact
roots alike. Exactness matters only to the rules that need the target's
**identity** rather than its lifetime:
* storing a reference into a relative-reference location (§3.9);
* converting a reference to an index (`index_of`, §3.3);
* which array a shrink through a reference frees (§5.1);
* where a store through a reference lands (the store rule below);
* the compiler's proof that two references name different arrays.
Without an exact root, each of these takes its conservative answer. A
relative reference that names a pool (§3.9) is another source of exact
roots: a load out of one is rooted exactly at that pool, whatever container
it was read out of.
Rules (scopes ordered by nesting; globals are the outermost scope, §11.1):
* **Store**: `r` may be stored into a location owned by root `L` only if
`scope(root(r)) ⊇ scope(L)`, that is, if the pointee provably outlives the
container. A location may be reached through a reference whose root is
inexact. The location then lies in what the reference points at (for a
slice, in an element), and its owner is that root or anything further out
that can hold that pointee: every read-back candidate for that type at the
root's scope or outside it (§9.5), including the caller's storage behind
a parameter. `r` must outlive each of these owners, and the store is on
record for each of them. In `let n: Node& = if c { a } else { b }; n.p .=
x;` the field is `a`'s or `b`'s, so `x` must outlive both. It need not
outlive storage that could hold `p`'s type but no `Node`, since the field
is never in such storage. A call checks in the same way what its callee
stores through an argument with such a root, or into storage that an
argument's references lead to, since the callee has only a root for that
storage.
A value that *holds* references (a struct with a reference field, an
array of slices, an ADT payload with a reference field) is stored under
the same rule for what it holds: its root is that of the references
stored into it, and each such store is on record for the shrink rules
(§5.1). The same applies to the elements `append` copies: an array's
elements are stored as the whole array would be, and a slice's elements as
each element read out of it would be (§9.5). A declaration stores its
value into the variable it declares, with or without a type annotation:
`let s = { let t: u8[] = "abc"; t[..] };` is an error, since `t` ends with
its block.
* **Branch values**: whatever receives the value of an `if`, `match`,
`block`, `loop` or bare `{ }` (a call's argument as well as a variable)
receives it after the scopes that its branch opened have ended. So a
reference or slice that the value is, or holds, must not be rooted at a
variable declared in those scopes (including a match arm's binder), or at
a temporary made there:
`f(if c { var t: i64[>..] = [1, 2]; t[..] } else { a[..] })` is an error,
as is the declaration above. So is `f(if c { var t: i64[>..] = [1,
2]; t } else { a })` for a slice parameter, where the arrays join as a
slice (§6.4).
* **Return**: a returned reference's root must be visible to the caller (a
caller-supplied root, a global, or the function's own in-place-constructed
return value). This applies to every return, whatever the other returns
give. A returned value that holds references is checked by the references
it holds; for a local, those are the references stored into it (§9.5). A
value whose reference fields were only ever null can be returned as
freely as a value holding no references.
* **Merged values**: a merged value is a value that may be any of several:
the branches of an `if` or `match`, the `break`s of a `block` or `loop`, a
reference variable's bindings, or a function's `return`s (which each call
maps to its own arguments). It has every one of their roots, and it names
one array exactly only where every one of them names that same root
exactly (a `null` names none). What a merged value holding references
holds is rooted the same way. A merged value must outlive a destination
by every root, and it is writable only where all of them are (§9.5). It
is stored only where each of them could be: not at all where one of them
may point into a grow-shrink array (§5.2), and not inside a recursive
cycle where one of them is rooted where the cycle stores nothing (§7.8).
A shrink frees what any of them points into (§5.1). A parameter given a
merged value stands for all of its places, and so does a call's result
where the argument behind a return may be a merged value. So a function
may return a view of its input on one path and of a global arena or a
string literal on another. For a `longer` that returns whichever of its
two slice parameters is longer, `longer(x, y)` may point into either `x`
or `y` (whichever it returns at run time), and into nothing else.
* Struct types with reference fields are implicitly generic over those
fields' roots. Struct instances with different root bindings are distinct
types for checking purposes, with the same layout. An instance whose
references all point into one variable is bound to that root exactly. An
instance whose references point into several variables has each of their
roots, so a function given both kinds of instance is checked for each
kind. A `recursive fn`'s back edges reuse its body whatever they pass
(§7.8), so its by-value parameters are always taken to be of the second
kind.
* References into a value being copied by value do not transfer to the copy;
they keep referring to the source (plain value semantics).
* A reference *variable* commits to the root of its first binding. `.=` may
rebind it only within the same root, or to a root at the same scope
depth. The common case is retargeting it in a loop to another element of
the same container or of a sibling container. Any other rebind needs a new
variable. What a callee reads out of storage it was given is rooted at
that argument (§9.5). So a key that a dictionary's `each` passes to a
function value is rooted at the dictionary passed, at that dictionary's
depth, whatever text the keys view. A slice variable commits the same
way. A store through a reference to its slot (§3.8) assigns it as `s = v`
does, even where the variable is a `let` (§4.4). This holds for a store
through a reference that may name the slot (one read out of storage, a
merged one, or a parameter given one of those) as well as for a store
through a reference that does name it. A rebind to a different root
leaves the variable inexact, since it no longer names one array. It also
makes the variable a merged value (above), since it may still hold its
earlier binding.
A loop body is checked in the state that its earlier iterations leave it
in, including a rebind in a nested function or a function value that the
body calls. So a read earlier in the body than such a rebind sees both
roots, and it cannot be used where the root is needed exactly (such as a
relative link's identity, §3.9). A variable declared before a loop and
bound only inside it (`var last: Node? = null;` before `loop { if last {
last.next .= child; } … last .= child; }`) has, at a use earlier in the
body than the rebind, the root that the rebind gives it, exact where the
target lies in that root's own storage. A variable that the loop never
binds is null throughout the loop, and a use of it is that null (§9.5).
Any function may rebind a global `var`, a function compiled after a
function that uses it as well as one compiled before. So a use of a
global `var` in a function's body has every root it can be given (the
read-back rule's answer for a global, §9.5), rather than only the roots
bound so far.
* Inside recursive cycles the stricter §7.8 cycle store rule applies.
* The following expressions create a **temporary** when viewed in place instead
of built into a destination: array, struct, and variant literals; call
results; `copy(x)`; `default()`; and values of `if`, `match`, `block`,
`loop`, or bare `{ }` constructs whose branches do not join as a slice
(§6.4). A view can come from a slice destination, a `for`, `[..]`,
`bytes_of`, or a path into the value (§4.2). The value of such a construct is a copy of what the branch taken
produced, even where that branch names a variable. A temporary lasts for
the rest of its statement, or for the rest of the block whose final
expression made it. Its scope is that
statement's: the variables the statement declares outlive it, and the
variables of the scopes the statement opens (the body of a `for` over it,
the arms of a `match` on it) do not. So a reference or slice into a
temporary may be passed down, and may be stored or bound only in those
inner scopes: `let s = f()[..];` is an error, while
`let t = f(); let s = t[..];` is not, and neither is a view into `x`
bound inside `for x in f() { … }`. Such a reference or slice is never
returned. A function it is passed to may keep it in its own locals, which
end first. What a temporary *holds* is not rooted at the temporary
(§9.5). So, for a slice parameter,
`take(if c { str("a", n) } else { str("b") })` is an error, because each
branch's string ends with its branch, while `take((if c { str("a",
n) } else { str("b") })[..])` views the `if`'s own value, an array that
lasts for the rest of the statement. A temporary is not storage that `.=`
or a reference parameter can bind (§4.1); they bind a construct's
branches instead. A view of a temporary is read-only (§9.5).
Violations are compile errors. There is no escape hatch in v1.
### 9.3 What the runtime still checks
The runtime aborts (prints a message and exits; an abort cannot be caught)
on each of the following. In a contained worker (§11.2), each of them except
the last two ends only that worker:
* array/slice indexing out of bounds (elided wherever the compiler proves it
cannot fire, §10.5; can be disabled wholesale in a designated unsafe-fast
build), and a conversion into an index or bound type outside its array
(§3.11), elided the same way;
* a reusable pool's `free(i)` with an index outside `[0, pool.len)`;
* a slice pool's `alloc_slice`/`realloc_slice` with a negative length, or
one no data stack could hold (§5.4);
* a slice pool's `free_slice`/`realloc_slice` with a non-empty slice that is
not one of its runs, where the checker could not tell (§5.4);
* limited-array capacity overflow;
* shrinking below empty (`pop` on an empty array, `pop_n` of a negative
count or of more elements than the array holds, `resize` to a negative
length);
* relative-reference offset overflow at store (only where a root array, or
a named pool, can span more than the width holds, §3.9);
* debug only: integer overflow (per operation as it executes, §6.2), `as`
conversions that change the value (§6.3; conversions to a float
excepted);
* division by zero (always);
* `assert` failures, and the program's own `abort(msg)` (`msg` any `u8`
array or slice; printed as `goose runtime error: `);
* guard-page hits (stack budget exceeded), which abort safely and never
corrupt memory;
* native call stack exhaustion (recursion deeper than a thread's stack
holds, §7.8), which aborts safely in the same way, with a diagnostic of
its own.
`exit(code)` ends the program normally with the given process exit code.
Neither `abort` nor `exit` returns, and the checker takes this into
account: code after them is unreachable, so a branch ending in one of them
gives no value (§6.4).
### 9.4 Type-safe reuse and stale references
With `reusable` arrays and limited arrays, a stale reference can read a
*different value of the correct type* (type-safe reuse). This can cause a logic
error, but not memory corruption, type confusion, or out-of-bounds access. Slot
reuse allows these errors while avoiding a general-purpose allocator.
External bytes must be validated before they become a Goose value.
`from_bytes` (§12) checks an image's framing, tags, lengths, and
self-relative links. Each link must point to an element start in the same
image. Invalid input returns `false` and an empty array, so it cannot
introduce an unchecked reference. This verifies *safety*, not data
integrity: edited bytes may still describe a valid structure. Detecting
changes to the original data requires a separate check, such as a checksum.
### 9.5 Writability
Everything is writable unless its type says otherwise. Any type may be
qualified `const` (§2). A `const i64[3]` or `const Point` is a value whose
contents cannot be written into (its elements, its fields, and whatever a
reference to it reaches). `const T&` and `const T[:]` are a reference and a
slice through which the pointee, or the elements, cannot be written: no
assignment, compound assignment, `++`, or growing/shrinking operation
through them compiles. Whether a `const` value can be assigned as a whole
is decided by its binding (`let`, §4.4), and a copy of a `const` value is a
fresh, writable value: a cursor `var s: const u8[:] = text;` advances with
`s = s[1..]` and never writes a byte of `text`. `const` is shallow: a
reference or slice read out of a field of a `const Box&` is as writable as
the field's own type says, and so is a path through it (`b.r.n = 7`,
`b.s[0] = 7`). The exception is a self-relative reference (§3.9). It points
within the value or array holding it, so it is no more writable than the
path that reaches it. Every value of reference or slice type is either
read-only or writable, and where that property comes from is *inferred*
per instantiation, in the same way as roots, with no annotation needed:
* Read-only values include string literals (`const u8[:]`, §3.7). A view made
with `&x`, `x[..]`, or reference binding (§4.1) is also read-only when `x` is
a `const` value, a field of one, or a by-value `for`/`match` binding (§6.5).
These views have types `const T&` or `const T[:]`. Views into temporaries are
read-only because a write would change only the temporary (§9.2). So are
`bytes_of` views (§12), values read from slots declared `const`, and
self-relative references read along a read-only path. A slice loaded through a
reference-to-slice is the slice in the referenced slot. It is writable only if
both the reference and the slice are writable. A reference to a variable
holding a read-only slice is therefore read-only too, whether made with `&s`
or by binding `s` by reference (§4.1). A `let` binding of a reference or slice
prevents rebinding; writes through it follow the value's own constness. For
example, `let r .= xs[i]; r = 0;` writes an element of a `var` array (§3.8).
* Writable: everything else, that is, `&x` and `x[..]` of a `var` or of a
plain `let`, and whatever else is read out of a slot that is not `const`.
* **Parameters and results are generic over constness.** A parameter
declared `u8[:]` or `T&` takes a read-only or a writable argument, and the
specialization is checked with the argument's constness (a write through
a parameter given a literal is an error at that instantiation). A
parameter declared `const u8[:]` is read-only whatever it is given;
declaring it so documents that the function only reads its input. A
declared result type likewise names the shape. The constness of a call's
result is that of what the function returns, and the result is read-only
where any return is (a `u8[:]` result of `return s` for a `const u8[:]`
parameter is read-only at that call). `-> const u8[:]` makes the result
always read-only. Adding `const` is always implicit (`u8[:]` fits
`const u8[:]`); dropping it is never implicit.
* **Slots say what they hold.** A field, an element, a global, an annotated
variable and an assignment target are *slots*, and a read-only reference
or slice is stored in a slot only if the slot's type is `const`: `struct
Named { name: const u8[:] }` holds a literal, `struct Buf { bytes: u8[:] }`
does not, and `views: const u8[:][>..]` takes either kind of view. So
what is read out of a slot is exactly as writable as the slot's type says
(a self-relative reference is at most that writable, above), and storing
a value never makes it writable: a `u8[:]` field that could be written
through can only ever have been given a writable slice. An un-annotated
variable's type is its initializer's, made `const` where that value is
read-only (at this instantiation, for example). So `var t = trim(line);`
for a read-only `line` is a `const u8[:]` cursor, which
`t = trim(t[..n]);` can advance. It stays read-only whatever is assigned
to it later, as if it had been declared with that type.
* An `extern fn` (§7.10) is a C function, and its parameters have exactly
the constness they declare: a read-only argument needs the parameter
declared `const`.
**Read-back roots.** A container names a scope, not the storage its contents
point into, so the root of a reference or slice of pointee type `T` read out
of a container `C` is re-derived. A **candidate** is a variable whose own
storage can hold a `T` by value: an array of `T` in any array kind, a struct
or ADT payload with a `T` field, a `T` itself, and so on through by-value
nesting. A variable that only holds *references* to `T` is not a candidate,
and a reference or slice field ends the search. Static data is a candidate
for the element types a literal can supply. For a writable reference or
slice, static data is a candidate only when nothing else is, since a
writable slot is never given a literal (above) and so holds static data
only as a null or an empty slice. For a read out of a field, an element or a
global, a variable holding a grow-shrink array whose elements can hold a
`T` is not a candidate either: a reference rooted at that variable is never
stored (§5.2), whatever else of the variable could hold the `T`. A
reference to the whole array, or to what holds the array, is stored like
any other: its pointee is not an element, and the variable stays a
candidate for it. Then, depending on where `C`'s own root lies:
1. **A global.** Only globals outlive globals (§11.1), so the owner is a
global candidate, whatever local scope is open. If there is one
candidate, the root is that variable, exact. Otherwise the root is the
global scope, inexact.
2. **A local of the function being checked**, at any block depth,
including by-value parameters. Everything stored into `C` was reachable
from this frame and had to outlive `C`. So the owner is a local declared
at or outside `C`'s block, a reference parameter's pointee, a global,
static data, or storage of the caller's that a parameter leads to through
references. Those references are the ones a by-value parameter holds
(which its argument filled), and the ones in what a reference parameter
points at. This function cannot enumerate that caller storage, but all
of it outlives the parameter's root (for a by-value parameter, the root
of what its argument held: §9.2's generic roots of reference fields), so
that root is its candidate. Every candidate is a root of the value: a
local or a global as an exact root, and such a parameter's root as a
bound on the storage behind the parameter. The value names one array
only when there is exactly one candidate in all and that candidate is
not such a parameter's root. The candidates are the variables in scope
at the read: an array that comes into scope after the read cannot be the
owner and is not a root of the value.
Where `C` is a local that holds
values rather than a reference or slice, everything stored into `C`
since it was made is on record: stores through a reference to it, by a
function it was passed to, or later in a loop around the read. Where
each of those stores was rooted at one variable exactly, at static data,
or at the storage a parameter's argument holds, the value's roots are the
roots of those stores instead. A variable whose storage cannot hold the
value's pointee type is not one of these roots, since it owns none of
what was stored there of that type: an `i64&` read out of a holder that
also views a `u8` buffer is not rooted at the buffer. A word of
`words(buf)` for a local `buf` points exactly into `buf`, whatever else
in scope could hold a `u8`. A by-value parameter holds what its argument
held, so a slice of `g.edges` for a `g: Graph` parameter is bounded by
that argument's root. It is not bounded by the callee's other
parameters, which can hold its element type but none of the caller's
views.
A copy of a field or an element of such a local (a `let` of one,
a `for` or `match` binder's copy, a popped element, or a value loaded
whole through a reference to it) holds no more than was stored into the
local, and that record then applies to the copy too: out of
`let hs = [H { r: a }]`, `hs[0].r` points exactly into `a`. A copy out
of any other container has only its container's scope on record, and
its roots are the candidates.
A nested function's or a function value's
body reads a local of the function it is written in as one of its own
locals here, with that local's stores on record in the same way: the
body is checked for the state each call finds the local in (§7.5,
§7.6). A `for` loop also reads its binder in this way in each of its
checks, so that what the loop body stores into the holder it walks
reaches the elements that later iterations read.
3. **A reference parameter's pointee, or itself inexact.** The owner may be
caller storage that this function cannot enumerate, so the root is
`C`'s root, inexact.
A non-recursive function can retain the caller's more precise record when
its argument names a local holder exactly, all the holder's recorded
contents point into one root exactly, and only parameters can reach that
holder during the call: neither the function nor a function value it runs
can name it directly. Reads then have that contents root, including any
roots stored through possibly aliased parameters while the body runs.
A nested function or callback's stores keep the distinction that the
resulting references came from that holder's contents; call sites check
them against the actual stores into its storage, including aliased
stores.
Stores feed back around loops, and calls are specialized for the contents
they receive; a call after a rebind is checked for the new contents. The
same rule applies when a parameter forwards such a holder to another
function. It does not separate fields whose contents have different
roots: those retain the conservative bound. For example, a `Cache&`
naming a local `Cache { bytes: buffer }` keeps `buffer` as the root of
`cache.bytes`, so an unrelated reusable byte pool cannot be that buffer.
Shrinking it still checks every live reference and stored view into
`buffer`, including those in the caller.
4. **A temporary** (§9.2). Everything in it came from the literal's
initializers, from the call whose result it is, or from the value it is
a copy of, so its root is theirs: the innermost root among the
initializers, the root of the result's contents, or the root of the
copied value's contents. The root is exact when it is one variable
exactly.
Any of the program's globals may be a candidate. A function that a global
initializer calls is compiled before the globals declared after that
initializer, but that compilation also serves the calls made once those
globals exist. So inside that function the global scope stands for those
globals as well, as an inexact root.
A global `var` of reference or slice type is a slot that any function may
bind (§9.2). The compilation of a function that uses it serves every call,
including calls made after a function compiled later has bound it. So a
use of such a global in a function's body is a read out of a global (case
1): its roots are each global whose storage can hold its pointee, exact
where there is only one. They never include a grow-shrink array's
elements, since no binding of a global points there (§5.2). Such a global
that names a pool (§3.9) points into that pool, and a global `let` keeps
its binding. A global initializer runs once, when the bindings made so far
are all there are, so a use in it has the roots of those bindings.
An optional variable bound only to `null` so far (`let none: Node? =
null;`, or `var best: Node? = null;` before anything binds it) holds null.
Where every binding that can come before a use of it has been checked, the
use is a null, which has no root (§3.9). For a local, that is the case
wherever it is used. In the body of the function declaring it, this
includes a loop's later bindings (§9.2). It also holds in a nested
function's or a function value's body. Such a body is checked for how a
call finds the variable (§7.5), so a call after a binding has a check of
its own, and the body's own bindings reach a loop or a recursion around
the call as the declaring body's bindings do. For a global, it is the case
in the global initializers, and anywhere for a global `let`, which only its
initializer binds. A global `var` used in a function's body is read as
described above.
A relative reference `T&` read out of `C` always points within `C`'s own
root array (§3.9), so it takes `C`'s root and `C`'s exactness, whichever
case applies. A relative reference that names a pool needs no candidates:
it points into that pool, so it is rooted there exactly. This keeps a
container of links usable where a container of plain references would be
ambiguous.
When a diagnostic depends on an inexact read-back, it names the source container
and the possible roots. For example: "`n` was read out of `slots` and may point
into `pool` or `spare`", or "`g` is a global var, which any function may bind,
and may point into `pool` or `spare`". If only the caller knows the candidates,
the diagnostic instead names the parameter whose pointee bounds the value.
Writability checks depend on the callee's operations. A function that mutates
its slice argument can compile for a writable argument and fail for a
read-only one. The compiler reports the full compile-time call chain,
including the source of the read-only data. `const` is required on slots
that must accept read-only references or slices; parameters and results
may also use it to state that they are always read-only.
---
## 10. Compilation model
### 10.1 Whole program, call-graph order
Goose is a closed-world, whole-program compiler. Functions are typechecked
and specialized in call-graph order (callers before callees), so argument
types, reference roots, writability, destination stacks, and static function
values are always concrete. Recursion is the annotated exception (§7.8).
### 10.2 Monomorphization
Each function is compiled once per unique combination of (argument types,
reference roots, writability provenances, destination/target stacks and
request kinds, static fn values), with a nested function's free variables
counting as arguments (§7.5). Roots and stacks are compile-time constants
inside each specialization, so pushes through references compile to direct
bumps of a known global. The exception is where the compiler chooses a
hidden stack argument to share one body across contexts (§4.3, §7.4).
### 10.3 Stack assignment (deterministic recipe)
Traverse specializations DFS from `main` (and global initializers). Each
specialization carries a compile-time *stack environment*: the watermark of
stack indices in use on entry, and a map from each of its nonfixed locals /
by-value params / in-flight returns to an index. Assign each new
simultaneously-live resizable (and each data-stack-resident variable value,
which shares by nesting per §1.3(4)) the lowest index not in use at that
point. Indices become free again when the owning scope exits (sequential
reuse), so a function called after a local's block has ended may use that
local's index again. This is what lets a recursive cycle's functions own
scratch (§7.8).
A pending nonfixed return's destination stack is part of the in-use set
from the call site until the value is received. This is also what makes
`return from` safe (§7.9). N = the maximum index + 1 reached anywhere.
Threads run the same algorithm per thread program (§11.2).
Implementations may replace per-context specialization with hidden runtime
stack arguments where profitable; semantics are identical.
Fixed locals normally go to the native stack / registers. The C backend
places fixed locals and temporaries larger than 4096 bytes on data stacks,
using the same scope watermarks and hidden stack indices as nonfixed values.
This threshold is a backend policy, not a language limit or a change to a
type's size class, packed layout, value semantics or reference lifetime.
It prevents large arrays and records, including temporaries introduced by
inlining, from exhausting a platform's much smaller native call stack.
Inside a function of a recursive cycle (§7.8) such values stay on the native
stack instead. A data stack held across a call into the cycle would need one
stack per activation, which §7.8 rules out for every value. The recursion's
depth is already bounded by the native stack, and the native stack is where
fixed values normally go. So a large fixed parameter copy, local or
temporary of a cycle function is a native value; deep recursion over large
values avoids this by taking them by reference. Rejecting such parameters
and locals in cycle functions instead would have made the backend's
threshold a language rule.
Variable-class locals may be placed on the native stack (`alloca`) instead
of a data stack when the compiler chooses; the current backend places them
on data stacks. Resizable frame objects (C.2) still keep their fixed prefix
and tail header in the native frame.
### 10.4 Runtime environment
At startup, reserve N address regions (target: multiple GB each; commit-on-
touch via guard pages, prototyped at github.com/aardappel/stackalloc),
plus guard gaps between regions so runaway growth aborts cleanly. An
implementation sets one budget for the address space that regions use
across all thread programs together (32 TB by default here), and this
budget bounds `hardware_threads()` (§11.2). A region that the platform
refuses may be reserved smaller; nothing that the implementation elides
depends on a region's reserved size. Platforms without address-space
reservation (wasm today) fall back to index-based references and
bounds-checked growth, with reduced performance.
**The 48-bit size limit.** A data stack reserves **at most 2^48 bytes**.
Consequently, in every conforming implementation:
* no value's byte size exceeds 2^48;
* no array's element count exceeds 2^48 (stated separately because an
element type may be zero-size, and because most array types are already
far tighter: `T[]`'s length field defaults to `u32`, §3.3);
* therefore every length, capacity, index, and byte offset lies in
`[0, 2^48]`, and `.len` and `.cap` are non-negative by construction.
This limit allows up to 256 TB in one value while leaving headroom for
arithmetic the compiler can reason about. The guard region after each
reservation already turns an attempt to exceed it into a safe abort (§9.3),
so no growth operation needs a separate check for this limit. A fixed
array's size is a constant checked at compile time. Implementations may
impose a *smaller* limit (wasm32 is inherently capped at 2^32); they may not
raise it, so a program's meaning never depends on the target having more.
Making the limit part of the specification provides three guarantees:
* **Size arithmetic cannot overflow.** With 15 bits of headroom below `i64`,
`len - 1`, `i + 1` for `i < len`, `len + len`, `len * 2`, and
`i * element size` are all in range. The optimizer may assume this rather
than prove it, and the bounds-check analysis (§10.5) relies on it directly.
* **Signed is the right default for sizes.** `.len` returns `i64` (§3.1),
and since the top bit is provably unused, the sign bit loses no usable
range, while subtraction and differences behave naturally. Signed sizes
avoid unsigned wraparound in ordinary differences.
* **Non-negativity is a type-level fact, not an inferred one**, so §6.1 can
admit the one mixed-signedness comparison that matters without any
analysis.
Compilation target: C/C++ first, LLVM later. Representation and calling
convention: Appendix C.
### 10.5 Bounds-check elimination
The index and slice checks of §9.3 are the only abort that the compiler
routinely proves unnecessary, so the analysis that removes them is part of
the compilation model rather than an implementation detail. It runs over
each specialization after optimization, and it is required only to be
*sound*: every check it removes is one that could not have aborted, and a
check it fails to prove unnecessary stays in the program. Nothing about a
program's meaning depends on how much it proves, except whether a
`checkfree` function compiles (§7.14), for which the specification fixes
what every implementation must prove.
Beyond ordinary flow facts (loop headers, conditions and their negations,
`assert`, match arms), the language gives the analysis the following:
* **Index types.** A value of `uN in a` is below `a.len` at any later point,
since `a` never shrinks (§3.11). So an index of `a` used on `a` needs no
check, and one used on another array `b` needs only `a.len <= b.len`.
That is a relation between two lengths, which the analysis states and
keeps like any other relation. It is not a fact about every element of an
array, which the analysis cannot state.
* **Types.** An array is of one type, and a reference reaches only an array
of its own type, so a growth or shrink through a reference, or a callee's
write through an argument that is an array of scalars, leaves every
array of another type as it was.
* **Monotonicity in the type.** A grow-only `[>..]` shrinks only at a `pop`,
`pop_n`, `resize` or `clear` on the array itself (§5.1), which the analysis
sees, so a bound established before a `push` still holds after it. This is a
guarantee a resizable-array type without the grow-only/grow-shrink split
cannot offer.
* **Roots.** Every reference's root is static per specialization (§9.2), so
a call cannot invalidate a length the callee has no path to, since
storage is reachable only through a global, a capture, or an explicit `&`
(§3.8).
* **Static extents.** `T[k]`'s length and `[..k]`'s capacity are constants.
* **Total operations.** `%` is Euclidean (§6.2), so a reduction is always in
range; a completed `pop` proves that the array was non-empty, and a
completed `pop_n(n)` proves that `0 <= n <= len`, because the other cases
abort.
Known gaps are listed in TODO 0f. Whole-program compilation lets the
analysis reach across calls: each specialization has concrete argument
roots, a caller's facts reach an inlined callee body directly, and a call
that is not inlined carries them too. Every call site records what it
proves about the arguments it passes (constant bounds on an array's length,
and how the passed lengths and integers relate). A specialization enters
with the meet of those facts once all of its call sites have been analyzed.
Callers are analyzed first, and a call site inside a recursive cycle gives
its callee no facts. A thread's entry point and an `export fn`, which C may
call with any arguments, enter with no facts. In the other direction, a
callee is summarized rather than assumed hostile. The storage it may resize
or overwrite (through its reference parameters, in globals, or in captured
outer locals) is computed as a fixpoint over the call graph, and a call
kills only the facts that those effects can name. So a kernel
`fn blur(src: u8[>..]&, dst: u8[>..]&)` that only reads and writes elements
can use `src.len == W * W` when every caller established it, and a call to
it leaves the caller's own facts about `src` intact.
---
## 11. Program structure
### 11.1 Modules and globals
* A program is compiled as a whole. `import a.b.c;` includes the file
`a/b/c.goose` once. The file is first resolved relative to the root of
the *importing file's package*: for the program's own files that is the
directory of the root file being compiled, and for a library's modules
the library's directory. Next, where `a` names a package the root file
declared (below), it is resolved as that package's module `b/c.goose`
(`import a;` is its entry `a.goose`). Last, it is resolved in the
standard library's directory (`--stdlib `, else `GOOSE_STDLIB`, else
the `stdlib/` of the source tree the compiler was built in). A file of
the importing package with the name of a declared package is an error.
The form `import .a.b;` (leading dot) resolves relative to the
*importing file* instead.
* **Packages** (`docs/design/packages.md`). `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 path relative
to the root file (`"./lib"`, `"../lib"`) or a git repository at a ref
(`"owner/repo@v1"` on GitHub, `"host/owner/repo@commit"`, or a URL),
fetched by the compiler. Only the root file declares packages, so a
program's dependencies are flat and in one place. The same form in any
other file is a *requirement*: the root must declare a package of that
name, from the same source, or compilation fails naming the line to add;
a different ref is a warning. One version of a package exists in a
program. All declarations are public in v1; a name collision within one
namespace is an error, except that a global variable and functions may
share a name (below). Top-level declarations are order-independent.
* **Namespaces** (docs/design/namespaces.md). `namespace image;`, written at
most once and before a file's declarations, places those declarations in
namespace `image`. A file without it declares into the global namespace,
and several files may declare the same namespace. An import loads a file;
it neither creates a namespace nor brings a namespace's names into scope.
From elsewhere a declaration is named `image::Pixel`,
`image::brightness(p)`, `image::Shape.Circle { r: 1.0 }`, `image::pool`;
`::name` selects a global declaration (or builtin) that a namespaced one
shadows. A declaration may write its namespace itself
(`fn image::brightness(…)`, `fn ::main()` inside a namespaced file). The
qualifier alone then decides the namespace of the declaration and of the
names inside it. An unqualified name resolves lexically first, to
what the scopes around its use declare. It resolves to the innermost
variable or type parameter of that name (a function's parameters and
locals, then its type parameters, then what is around its declaration,
§7.5), or else to the nested functions in scope (the innermost block's
set, §7.5). Only a name that no scope declares resolves in the current
declaration's namespace, and then in the global namespace and the
builtins. So a type parameter or a nested function hides a global and a
function of its name, as a local does. A namespace keeps its global
variables apart from its functions, and no variable can be called
(function values are not data, §7.6). So the name in a call skips the
global variables and resolves to the functions, in each namespace of
this order. A global `var last: i32` in any module leaves std's
`last(xs)` callable from every module, including its own, while any
other use of `last` (`last = 7`, an argument) is the variable. A
variable in scope still hides the functions of its name: a local `count`
makes `count(xs)` an error. A type parameter bound to a type is not a
value (in `fn f(x: N)`, `N` as an expression is an error, and `::N`
names a global `N`). No type parameter or nested function is a constant
that a size or a match pattern may name (§3.3, §8.1). A function name's
overload set is the set of the first namespace in this order that
declares the name at all; sets never merge across namespaces (a
namespaced `hash` overload reaches the global integer overloads as
`::hash(x)`). A generic body resolves names where it is defined. A UFCS
call `x.f(...)` resolves `f` by the same rule, except that when `x`'s
struct or enum type is declared in a named namespace that declares
`f`, that namespace's set is the one used, ahead of the calling code's
namespace and the global one (a nested function in scope still hides
it). So a type's functions are its methods wherever it is used, in a
generic body too, while a plain call `f(x)` looks only where the calling
code does. The `format` hook is likewise looked up by type (§3.7).
Namespaces affect only name resolution, type identity and generated C
names (§7.10); they have no runtime representation, and they provide no
privacy and no re-exports.
* Globals are declared like locals (`let`/`var`, any type including
resizable). Semantically, the program runs inside an implicit outermost
scope that owns the globals. They take part in the depth check (§9.2) as
the outermost roots. They are initialized before `main`, in declaration
order within a file and imported files first across files, so a global
initializer may name a global from a module it imports; their
initializers may call functions. A global resizable owns the bottom of a
stack for the program's lifetime (a natural whole-program arena). A
global has no value until its initializer has run, and nothing else can
give it one. Naming it earlier is an error (§4.4), whether to read or to
write it, and whether in an earlier global's initializer, in its own
initializer, or in a function that one of those calls. A compile-time
size or match pattern may still name a later named constant (below).
That outermost scope is a frame like any other: a global's storage
belongs to the program instance running it and is never shared with
another (§11.2).
Every global has an initializer, except that `var g: T;` leaves its value
to its type: `g` starts as `T`'s default value (§4.2), set where an
initializer would run. So `var log: i64[>..];` is `var log: i64[>..] =
[];`, and a `T` without a default value (a reference) is an error. A
`let` or `const` global must be given its value, since it can never take
another.
`const` globals (`let` globals of a `const` type; a `var` of such a type
is still assigned as a whole, §4.4) of flat fixed type with
compile-time-evaluable initializers are stored in static data. The
initializer of any `let` or `const` global is a named constant that a
compile-time size may use (`i64[N]`) wherever no local of its name hides
it (§3.3). An untyped integer or float named constant also adapts
wherever it is used, as a literal does (§3.1, §6.3).
A size, a fill count (`[v; N]`), a match pattern (§8.1), or a use that
adapts an untyped named constant to another type, takes a named constant
at its initializer's value. A size also takes a `.len` that an
initializer gives and the type does not fix (§3.3) at that value's
length. So a `let` that such a use names, directly or through another
named constant's initializer, may not also be bound anywhere to a
writable reference, through which its value could change (§4.4). This
covers binding by `&`, by `.=`, or by a reference parameter, field or
binding that is not `const`. As for a comparison that relies on a `let`
being non-negative (§6.1), this is an error in either order; declare it
`const`, or bind the reference to a copy.
* Entry point: `fn main() { }`, in the global namespace. Only the *root*
file's `main` is the entry; a `fn main` in an imported file is ignored:
it is not an entry, it cannot be called, and it causes no name
collision. A runnable file can therefore also serve as an importable
library: give it `fn main_x() { ... }` plus a `fn main() { main_x(); }`
wrapper, and importers call `main_x` directly. A namespaced file does the
same with `fn ::main() { ns::main_x(); }`.
### 11.2 Concurrency
Threads never share mutable memory. The model is a statically typed
relative of the Linda tuple-space / coordination style.
* A worker entry point is declared `thread_fn worker(a: i64, ...) { }`.
The compiler compiles a `thread_fn` and everything it calls **as a
separate program** with its own stack assignment (its own N′). Because
many instances of a thread program can run at once, a thread program's
stack references are inherently dynamic: each thread carries a pointer to
its own block of N′ stacks, and stack accesses in thread-program code are
indexed from it (one hidden base kept in a register). A function is
shared between thread programs and the main program only when all its
stack references arrive through (hidden) arguments; any static stack use
specializes it again per program, like a template instantiation.
* The number of workers is decided **at runtime** (there is no static
maximum):
`thread_spawn(worker, args…) -> i64` reserves a fresh stack block, copies
the args, starts the worker, and returns its id. `worker` names a
`thread_fn`, directly or as a function parameter bound to one (§7.6), so
`fn spawn(n: i32) -> i64 { thread_spawn(W, n) }` spawns whichever
worker its caller passes. IDs increase monotonically
and are never reused. `hardware_threads() -> i64` exists for sizing, and
reports no more workers than the implementation's address space for data
stacks (§10.4) holds beside the main program, so a worker pool sized by it
never exhausts that address space.
`thread_wait(id) -> bool` blocks until that worker has ended and all of
its Goose storage has been released. This supports both scoped fork/join
parallelism and orderly shutdown (send quit messages, then wait). Repeated
waits on a completed id return immediately; multiple workers may wait for
the same id. Every wait returns `false` for a contained worker that
aborted, and `true` otherwise. `thread_wait(id, msg) -> bool` waits the
same way and appends the abort's message to `msg`, a growable `u8` array.
The message is what the abort would print after `goose runtime error: `,
including its location. The first such wait for the worker takes the
message; later waits, and a worker that returned, append nothing. A
program that wants the message asks for it once and keeps it. A negative
or never-issued id, or a worker waiting for itself, aborts with a runtime
diagnostic.
* `thread_spawn_contained(worker, args…) -> i64` starts a **contained**
worker, and otherwise behaves like `thread_spawn`. An abort in it (§9.3)
ends only that worker, which releases what a return would, and leaves its
message for `thread_wait` instead of printing it. The program and other
workers keep running. Resources it opened through `extern fn` stay open.
Guard-page hits, native stack exhaustion, `exit(code)`, and the runtime's
own failures still end the program, as does any abort on main's thread.
A worker it starts with `thread_spawn` is not contained.
* A worker's return releases its copied arguments, stack block, every reserved
and committed data-stack region, and active-worker record, whether or not
anyone waits for it. The runtime retains no per-completed-worker record or
native handle, except the id of each contained worker that aborted, so
that waits can answer, and its message until a wait takes it. Native
threads are detached. Their final native stack/TLS teardown happens when
the runtime entry wrapper returns, immediately after it publishes that
the Goose cleanup has completed. A child worker owns its storage
independently and may outlive the worker that spawned it. Queued messages
belong to their queues and remain available to receivers after the sender
exits. Workers still running when `main` returns are killed.
* A thread program is an instance of the program with globals of its own.
At `thread_spawn`, every global the worker's program uses is copied from
the spawning instance, in the same way as the arguments, and the worker
reads and writes its copies from then on. No global is shared: a worker
that increments a global increments its own copy, and main's copy keeps
its value. A global whose type is not **flat** (§1.1) cannot be copied
and is an error in any function a `thread_fn` reaches, unless it is a
`let` or `const` global whose references all lead to static data, as a
string literal's do (`let NAMES = ["a", "b"];`). The copy of such a
global views the same read-only bytes, and no program can tell this
apart from a copy of its own. `const` globals with compile-time
initializers are static data (§1.2), read-only and therefore shared as
they are.
* Args and queue elements must be **flat** types (§1.1); values are copied
in and out, which is cheap because Goose values are contiguous. A
`thread_fn`'s parameters have no defaults (§7.1): `thread_spawn` passes
every argument.
* **Typed queues**: conceptually there is one queue per flat element type.
`qput(v) -> bool`, `qget() -> T` (blocking), `qpoll() -> T, bool`
(non-blocking; the bool says whether a value was received; on a miss the
value is T's zero value: zeroed scalars, empty arrays, variant 0),
`qget(ms: i64) -> T, bool` (like `qpoll`, after waiting at most `ms`
milliseconds; zero or less does not wait) and `qlimit(n: i64)` select
the queue by type. Queues are stored outside stack memory
(runtime-internal allocation is permitted here).
* **Bounded queues**: `qput` never blocks. It returns `true` once the value
is queued, and returns `false` immediately, queueing nothing, when the
queue holds its limit of messages. A call statement drops the result
(§7.1), so `qput(v);` ignores a refusal. A queue has no limit until
`qlimit(n)` gives it one of `n` messages; lowering a limit below what
the queue holds keeps the messages queued, and puts fail until fewer than
`n` remain. A limit of zero or less aborts with a runtime diagnostic.
### 11.3 Dynamic stacks (future)
This future feature is a resizable-of-resizables local in which each
element owns its own dynamically created stack and is accessed through fat
references. It is not in v1. (The thread mechanism of §11.2 already
introduces the dynamic-stack-block machinery this needs.)
---
## 12. Builtins, the standard library, and what v1 leaves out
The language's own functions:
| Builtin | Meaning |
|---|---|
| `print(a, b, …)` | the arguments' text and a newline to standard output (§3.7) |
| `str(a, b, …) -> u8[>..]` | the arguments' text as a fresh string, built at its destination (§3.7) |
| `format(out, a, b, …)` | the arguments' text appended to `out`, any growable `u8` array (§3.7) |
| `assert(c)` | aborts unless `c`, a `bool` or an optional, which it narrows (§9.3, §3.8) |
| `abort(msg: u8[:])`, `exit(code: i64)` | end the program; both diverge (§9.3) |
| `copy(x) -> T` | a fresh copy of the stored value `x` names (§4.1) |
| `default() -> T` | the value a fixed-size `T` has before anything is written to it (§4.2) |
| `hardware_threads() -> i64`, `thread_spawn(worker, args…) -> i64`, `thread_spawn_contained(worker, args…) -> i64`, `thread_wait(id) -> bool`, `thread_wait(id, msg) -> bool` | workers (§11.2) |
| `qput(v) -> bool`, `qget() -> T`, `qget(ms) -> T, bool`, `qpoll() -> T, bool` | the typed queues (§11.2) |
| `qlimit(n)` | a queue's limit of messages (§11.2) |
| `to_bytes(a) -> u8[>..]`, `to_bytes(a, out)` | an array's image (a varint byte count, then its element region), either fresh or appended to a growable `u8` array (§3.9, `design/serialization.md`) |
| `bytes_of(a) -> u8[:]` | the element region alone, as a view: no copy, and never writable |
| `from_bytes..]>(b: u8[:]) -> T[>..], bool` | a *verified* array from an untrusted image; empty and `false` if it is not one. Also builds `T[>..<]` and the `T[]` family |
| `embed_shader(path)` or `embed_shader(stage, source, …) -> const u8[:]` | compile-time GLSL compilation to a static shader blob; graphics extension, detailed in the implementation notes, section 3.14 |
| `embed_file(path) -> const u8[:]` | the bytes of a file, read when the program is compiled, as a view of static data; `path` is a string literal (or a `let`/`const` global holding one), relative to the file with the call; each distinct file is read once |
And the array members, ordinary functions of their receiver per UFCS
(`a.push(v)` is `push(a, v)`):
| Member | On | Meaning |
|---|---|---|
| `.len -> i64` | every array kind and slices | the element count (§3.3) |
| `.cap -> i64` | limited arrays | the capacity (§3.3) |
| `.push(v) -> T&` | limited, grow-only, grow-shrink | one element, constructed in place (§3.3) |
| `.append(src)` | limited, grow-only, grow-shrink | an array or slice of elements (§3.3) |
| `.pop() -> T`, `.pop_n(n)`, `.resize(n, v?)`, `.clear()` | limited, grow-shrink; grow-only where §5.1 allows a shrink | shrinking, and growing with a fill value (§3.3, §5) |
| `.index_of(r) -> i64` | fixed, limited, grow-only, grow-shrink | the index of the element `r` refers to (§3.3) |
| `.alloc_index(v) -> i64`, `.alloc_ref(v) -> T&`, `.free(i)` | `reusable` pools | slot reuse (§5.4) |
| `.alloc_slice(n) -> T[:]`, `.realloc_slice(s, n) -> T[:]`, `.free_slice(s)` | `reusable[]` pools | slice reuse (§5.4) |
For serialization, the byte-count prefix must account for exactly the
remaining input. Validation establishes complete elements, representable
lengths, in-range tags, booleans encoded as 0 or 1, canonical varints, and
valid self-relative targets before exposing a value. A link must name an
outer element or the correctly tagged payload of one; interior-field
targets are currently unsupported. Failure returns an empty array and
`false`, not a partially verified value. Success owns a copy of the
payload. The implementation notes, section 6.9, give the byte contract and
current resource limits; section 11 identifies violations still to fix.
Everything else is the standard library: Goose source under `stdlib/`,
reached by `import` (§11.1) and documented in `stdlib.md` (the design it
came from is `design/stdlib_design.md`). The math types of §6.1 are its
`vec` module; the C behind `math` and `os` enters through `extern fn`
(§7.10).
Deliberately out of scope for v1: error-value conventions (§7.9); move
operations for resizables; multiple resizables per struct; two-way growth
arrays; inline compaction / copying GC for pools; mixed-type pools;
alignment annotations and explicit vector types (`simd fn`, §7.12, only
widens the instruction set a function is built for); dynamic stacks;
labeled break; namespace privacy, re-exports and nesting (§11.1).
---
## Appendix A. Worked examples (informative)
### A.1 A list of strings, flat
```goose
fn read_words(text: u8[:]) -> u8[][>..] { // grow-only array of variable strings
var words: u8[][>..] = [];
var i = 0;
while i < text.len {
let start = i;
while i < text.len && text[i] != ' ' { i++; }
words.push(text[start..i]); // copies the slice into place, flat
while i < text.len && text[i] == ' ' { i++; }
}
return words; // NRVO: built on the caller's stack
}
```
The result is one contiguous block: `len | (len | bytes)*`. Iteration is
sequential, and no allocation happens anywhere.
### A.2 A compact tree with relative references
```goose
enum Sexp {
Sym { name: u8[varint] },
List { kids: (Sexp..&)[varint] }, // tiny self-relative links
}
var pool: Sexp..[>..] = []; // grow-only pool of variable ADTs
```
Children are built first (pushed earlier into `pool`), and parents store
tiny backward offsets: `(a (b c d))` takes about 21 bytes in total and can
be serialized by memcpy.
### A.3 A growable hash table (open addressing), in-model
```goose
struct Map { slots: Slot[>..] } // Slot fixed-size; grow-only
```
To grow the table, `slots.append(…)` doubles it in place (a bump, with no
move) and then rehashes in place by cycle-walking. Alternatively, rebuild
into a fresh local and copy back with whole-resizable assignment (§4.4).
Deletions use tombstones or the `reusable` mechanism. No allocator is
involved, and references are not invalidated during lookup.
### A.4 Case functions as virtuals
```goose
enum Node { Num { v: f64 }, Add { l: Node..&, r: Node..& } }
recursive fn eval(n: Node.Num&) -> f64 { n.v }
recursive fn eval(n: Node.Add&) -> f64 { eval(n.l) + eval(n.r) }
```
### A.5 Long-distance errors
```goose
fn parse_expr(l: Lexer&) -> Expr.. {
guard l.tokens_remain() else { return Expr.Nothing {}, "eof" from parse; }
...
}
// All deep failures land here; the error is the last return value (§7.9).
fn parse(src: u8[:]) -> Expr.., u8[] { ...; return parse_expr(&l), ""; }
```
### A.6 A context of borrowed tables
```goose
struct Ctx { // a slice and three references: fixed-size
source: const u8[:],
nodes: Node[>..]&,
text: u8[>..]&,
scratch: i64[>..<]&, // the recursion's working stack
}
recursive fn walk(c: Ctx&, n: i64, depth: i64) -> i64 {
let mark = c.scratch.len;
c.scratch.push(depth); // above its callers' entries
...
c.scratch.resize(mark);
return total;
}
fn compile(source: const u8[:]) -> i64 {
var nodes: Node[>..] = []; // one local per table
var text: u8[>..] = [];
var scratch: i64[>..<] = [];
...
var c = Ctx { source: source, nodes: nodes, text: text, scratch: scratch };
return walk(c, 0, 1);
}
```
A struct owns at most one resizable (§3.4), and a function in a recursive
cycle owns none that stays in scope across a call back into the cycle
(§7.8). So a program whose state is several growable tables owns each table
as a local of a driver function, and passes the rest of the program one
struct of references to them. The tables are rooted outside the cycle,
which grows and shrinks them through `c` like any pool passed to it (§7.4,
§7.8). Since every call back into the cycle passes `c` on, the views read
through `c` (a node's name copied into `text`, for example) may also be
stored into the other tables inside the cycle. The tables last for one call
of the driver: each compilation starts from fresh tables, and no state is
global.
---
## Appendix B. TODO / open items
These items were collected from the design discussion, and each still needs
work to resolve. The newest, highest-priority items come first. Items that
have since been resolved are kept at the end, each with a pointer to where
its resolution is described.
0a. **Mixed-signedness ergonomics in practice** — §6.1's unification
deliberately rejects `u32 + i32`, and `u64` against anything signed
outside the comparison rule now in §6.1. Watch whether real code (hash
kernels, size math against `.len`'s `i64`) still accumulates casts.
This problem only affects `u64`: `u8`/`u16`/`u32` all widen implicitly
into `i64` (§6.3), so the same code on a `u32` hash already needs no
casts.
What remains is a problem for the *analysis*, not the language: the
bounds-check pass (§10.5) follows a `u64` value's range through
`%`, `&` and casts, but drops it as soon as the value is bound to a
`u64` local, because in general a `u64` need not fit `i64`. A `let` has
one value, so it could carry its initializer's range as §6.1's
non-negativity already does. That would be the narrow, checkable
version of "track that this `u64` fits `i64`". Until then, the
open-addressed map in `bench/goose/words.goose` keeps the four checks
around `slots[idx]`.
0f. **Bounds-check analysis, known gaps** (§10.5) — a loop's exit condition
is a disjunction (`!(i < n && p)`), which a difference-constraint domain
cannot represent, so post-loop bounds rely on the inferred invariants
instead. LLVM has the same limitation and works around it by
duplicating blocks so the conditions never merge; that technique is
available here too. A value loaded from an array has no known range, so
indirect indexing (`dist[out[k]]`, `bench/goose/graph_csr.goose`) keeps
its check unless what is stored has an index type (§3.11). Release-mode
wrapping (§6.2) is deliberate and stays, so the analysis proves the
absence of wrapping explicitly where it needs to.
0c. **Pointee writes through optionals** — writing through a narrowed
optional works, but there is no way to write through an optional
without narrowing it; rebinding to a plain reference first
(`let r: T& = o;`) is the only workaround. This may be acceptable;
revisit it with more usage.
0d. **Lifetime precision, remaining cases** — the checker is still more
conservative than the spec in two places. First, long-distance returns
(§7.9) written in a named function only carry references to
globals/static data. The precise rule, which a function value's return
follows, is that the references are rooted at or above the target's
frame.
Second, the recursive-cycle store rule (§7.8) admits as roots of stored
references only globals, free variables, pool parameters and parameters
that every call back into the cycle passes on. A reference to a
caller's fixed-size local that such a call replaces is still
pass-down-only inside a cycle, and so is a merged value that may be one
rooted at a parameter the cycle passes on.
(One-root-per-reference-variable is a language rule, §9.2; a function's
result is rooted as a branch's value is, at the innermost of its
returns' roots. Writability follows `const` types; storage no longer
removes read-only restrictions, §9.5.)
2. **Error propagation sugar** — `return from` is the mechanism; revisit
whether a convention/sugar layer (a `try`-alike) is wanted once idioms
emerge.
3. **Move operation for resizables** — an assignment that leaves the source
empty, as the only sanctioned "move".
5. **Cycle store rule refinement** (§7.8) — the pass-down-only rule is
conservative. Parameters that the cycle passes on as they were given
are exempt. A parameter that a recursive call gives other storage stays
pass-down-only, even where every activation's store would outlive what
it is given. So does a parameter whose first argument is a local of a
recursive function outside the cycle. Explore per-activation reasoning.
6. **"Current pool" implicit destinations** — allocation without naming the
array, bound at compile time to the in-scope array of the right type.
7. **Mixed-type pools & pool GC** — internal vs external references, inline
compaction, copying; interacts with 6.
8. **Multiple resizables per struct** (memmove-on-insert, opt-in) and
**two-way growth** arrays.
9. **Concurrency surface finalization** — queue fairness/capacity, select,
worker-local init, flat-type relaxation for self-contained
relative-reference values (send whole trees through queues).
10. **SIMD/alignment** — measure whether packed layouts cost real SIMD
performance; consider opt-in aligned types if so. Related: narrow-lane
elementwise ops (§6.1) should vectorize now that arithmetic runs at the
element width; verify with the particle/sum benchmarks. `simd fn`
(§7.12) gives a function versions for the wider instruction sets; what
remains open is alignment, and whether a function should be able to
say which versions it wants (an AVX-512 version is not faster for
every loop).
13. **Labels for `break`** — if early-out patterns demand them.
15. **Wasm fallback** — index-based reference representation details
(`docs/design/wasm.md` compares the options).
16. **Relative-reference region tracking** — copies of values containing
*self-relative* references are currently rejected outright (§3.9; the
`in pool` form already copies); track the region an offset ranges over so
whole-region copies (and serialization moves) can be proven safe.
### Resolved
0e. **Per-array index types** — DONE, see §3.11 and §7.14
(`docs/design/index_types.md`): `u32 in a` is an index validated once
against a grow-only global `a`, which then never shrinks. So indexing
`a` with it needs no check, and neither does indexing another array
once their lengths are related. `checkfree fn` makes any check left in
a function an error.
0b. **Reference address identity** — DONE, see §3.8: `r1 .== r2` /
`r1 .!= r2` compare references by address; `==` stays the pointee
comparison.
0h. **Passing by size class** — DONE, see §4.1 (and §7.2, §3.8, §6.5):
fixed values connect to destinations by value; non-fixed lvalues bind
by reference at reference-typed, untyped and un-annotated destinations
and are an error at value-typed ones, which take an rvalue or an
explicit `copy(x)`; a redundant `&` is a warning.
0i. **Resizable tails get their own frame header** — DONE, see C.2: a
resizable-tailed struct with an all-fixed prefix is a frame object, so
`&s.tail` is an ordinary reference; variable-size prefixes and
resizable-class ADTs keep the bytes-on-stack shape.
0j. **Slices of grow-shrink arrays** — DONE, see §5.2 (superseding TODO 4):
a slice or reference into a `[>..<]` is created like any other and held
by variables only. A shrink while one is live is an error, which names
the slice or reference and where it was bound. Specializations record
what they shrink so that calls are checked the same way.
0g. **Recursive results' roots at back edges** — DONE (§7.8, cycle return
roots): a cycle is checked in rounds. A back edge's result carries the
merge of the roots the cycle's returns give, mapped through that back
edge's own arguments. In the first round the result points nowhere
yet, and the next round repeats that round's checks with the result. A
result holding references is checked by the roots of what it
holds, in the same way. One case remains: a back edge that passes
storage of its own for a parameter the entry call gave static data gets
a holder result that may only be passed down.
1. **varint format benchmark** — DONE: ULEB128 was adopted (§3.6). The
break-even point against the best branchless format is at ~70–75%
single-byte values (a cliff, not a slope); above it ULEB is ~3x faster,
below it up to ~3x slower. Revisit only if a per-field format choice is
ever wanted for unpredictable-length data.
4. **`[>..<]` interior-reference relaxation** — DONE, see §5.2: the
liveness test of §5.1, plus per-specialization shrink summaries for
the shrinks it cannot see directly (through a reference, or of a global).
11. **FFI** — DONE, see §§7.10–7.11: `extern fn` binds a Goose signature to a C
function, an `extern "lib" { ... }` block binds its members from a shared
library by name when the program starts, and `export fn` exposes a Goose
function through a C-callable wrapper. The flat fixed-size types by
value, references and slices to them, and `u8[>..]&` builders can cross
the boundary where supported; other shapes are rejected at the
declaration.
12. **Open syntax details** — DONE: both trailing-block parameter forms
exist (the implicit `it` and `x =>`, §7.6), and `recursive fn` is the
keyword alone on a cycle's entry function (§7.8).
14. **Stdlib math types** — DONE, see `stdlib.md` (`vec`): `vec2/3/4`
with `float3` and friends as aliases; the named ops are overloads per
size, and elementwise arithmetic is the language's (§6.1).
17. **Serialization of relative-reference structures** — DONE, see §12 and
`docs/design/serialization.md`: `to_bytes(a)` copies an array's element
region into a fresh `u8[>..]`, `from_bytes..]>(bytes)` verifies an
image and returns the array plus a bool. The verifier is generated per
element type next to the size and equality walkers, and a rejected image
yields an empty array, never an unproven reference (§9.4). An image is
little-endian by definition and carries a varint byte count in front, so
a reader of a stream can size its read; `bytes_of(a)` is the payload as a
view, for a save that copies nothing. v1 does not support references
into a *field* of an element, `from_bytes` into a fixed or limited array,
or a big-endian host (which aborts rather than writing bytes only it can
read).
---
## Appendix C. Representation and calling convention (normative for the C backend)
### C.1 Data stacks
Each data stack `S` has a control block `{ top: byte* }`. For the main
program the control block is a global; threads use an array of control
blocks reached from a hidden per-thread base pointer (§11.2). Allocation is
`p = S.top; S.top += size`. Scope exit restores the watermark recorded at
scope entry. No other runtime state exists.
### C.2 Value layouts
* Scalars: natural little-endian storage of their declared width; `bool` is
1 byte, 0 or 1. All loads/stores may be unaligned.
* Fixed structs/arrays: packed concatenation in declaration order. A
zero-length array (`T[0]`, or `T[..0]` after its length field) has one
element slot holding no element, since C has no empty arrays (§3.4). A
struct with no bytes of its own (no fields, and no `pad n`) takes up one
byte, since C has no empty structs. A variant type with no bytes of its
own takes that byte only as a value of its own, and takes no byte behind
its ADT's tag (§3.4).
* `T[]` family: length field (of the declared storage type) then elements.
* `T[..k]`: length field (smallest unsigned type fitting `k`), then `k`
element slots (uninitialized until first written).
* `T[..]`: capacity then length (both `u32` default), then capacity slots.
* ADT tags are zero-based variant indices in declaration order, stored in
`u8` for up to 256 variants, otherwise `u16` in the current C backend.
Fixed mode: tag, then payload area of max size (trailing padding
uninitialized, never read). ADT variable mode: tag, then the actual
variant's payload.
* varint: §3.6 (ULEB128; zigzag where signed).
* Reference: one pointer, except the fat form for resizables below.
Optional: 0 = null. Self-relative: signed offset
of the declared width from the offset field's own address; 0 = null
only in the optional form (§3.9).
Pool-relative (`in pool`, §3.9): unsigned offset of the declared width,
`(target − base(pool)) + 1`, so 0 stays null and a `uN` width spans
2^N − 1 bytes of the pool. `base(pool)` is the global's element region,
fixed when its stack is reserved before any initializer runs; a function
that loads or stores such a reference reads it once into a local at entry.
* Slice: `{ data: T*, len: int64 }` (len = element count).
**Resizable values.** A resizable's element region always tops its data
stack (§1.3(2)). A resizable array is represented as a header in the owning
frame (`{ base: byte*, len: int64 }`) plus the element region on the data
stack. `&v` yields the header's address (a fat reference is that pointer
plus the stack identity). With the stack identity known statically (or
carried as a fat reference or hidden argument), push compiles to
`*(T*)S.top = e; S.top += size; h.len++`.
A resizable-tailed struct whose other fields are fixed-size (and hold no
relative reference) is a **frame object**: a C struct of those fields
followed by the tail's own header (or, for a tail that is itself such a
struct, its frame object). It is held in the owning frame as a fixed value
would be, with only the innermost tail's elements on the data stack.
`&s` is the object's address plus the stack identity, `&s.tail` the tail
header's, so a tail can be referenced, sliced and grown through a
reference like any standalone resizable; `s.f` through either is a plain
member access. Copying, passing by value and returning such a value move
the frame object where an array moves its header (C.3) and the elements
where an array moves its elements. Nesting composes: a struct whose tail is
a frame object embeds it.
The two shapes that are not frame objects keep the bytes-on-stack form
behind a base pointer: a struct with a *variable-size* prefix (a `u8[]`
field before the tail) and a resizable-class ADT, whose payload shape is
per variant. Their header's `base` is the value's start on the data stack
and `len` the tail's count. A reference to such a nested tail is a compile
error; reference the owning variable instead. A tail of such a shape whose
type is a frame object takes the same form: its fixed fields are bytes of
the value, where a bare `pad` inserts nothing (§3.2), and its own tail's
count is that header's `len`, so its tail cannot be referenced either.
### C.3 Calling convention
* Fixed-size parameters and returns up to 4096 bytes: native C values
(structs as packed C structs). The first such return uses the C return
value; additional fixed returns use out-pointers. A slice, a reference to
a resizable value and a reusable pool's reference parameter are passed as
their members, each a C parameter of its own.
A fixed aggregate result containing relative references instead uses a
typed out-pointer at every size, and constructs directly through it.
Ordinary C-return selection skips these results. A nonlocal return uses
a saved pointer to the same receiver, rather than a copied result value.
* Larger fixed-size parameters: the caller makes a private value copy on a
scoped data stack and passes its typed pointer. The callee can mutate
that copy without affecting the argument's source. Larger fixed returns
use typed out-pointers to caller-owned storage; any return temporaries
also live on data stacks. Captures and `return from` keep their ordinary
lifetime and value rules.
* `extern fn` retains its declared native C ABI (§7.10), including large
packed structs passed and returned by value. C ABI copies can still use
the native stack; pass a reference or slice when crossing C with a large
value to avoid those copies.
* Nonfixed by-value parameters: caller allocates/constructs on the
parameter's assigned stack (§4.3), passes the header (base, len, or
metadata appropriate to the type) by value; callee owns and may grow it if
it is resizable (its stack identity is part of the specialization or a
hidden argument).
* Nonfixed return values: the destination stack is part of the callee's
specialization or a hidden argument, as is the destination request's kind
(one value vs element run, §7.3). The callee writes data at the
destination's top and returns the outermost metadata (lengths/counts) in
registers; in the element-run form it emits only raw elements. Because
in-flight return data carries no inline header above the caller's live
data, `v.append(f())` is always contiguous.
* Hidden arguments follow the declared arguments in this order: free-variable
references (§7.5), then destination/target stack identities. The `return from`
discriminant is a single thread-local int, not a signature argument: 0 means
normal execution, and k means propagate to target k. It is nonzero only
between a `return … from` and the catch in its target frame. An ordinary
return therefore writes nothing. A call on a propagation path costs a load and
a never-taken branch.
### C.4 Guard pages and commit order
Construction rules (§4.2) write stacks front-to-back. Windows commits on
fault: the runtime's handler commits a chunk from whichever page
was touched, so an allocation that skips ahead without writing (a large
limited-array capacity) needs no explicit commit, and nothing depends on
touch order. macOS and Linux map the usable region read/write and let the
kernel supply physical pages on touch. Fresh pages, including pages
discarded by `trim_memory`, arrive zeroed from the OS. Reused stack memory
is not zeroed. This is harmless, since uninitialized slots are unreachable
(§5.3). A final gap after each region, never committed, turns runaway
growth into a safe abort.
---
## Appendix D. Grammar sketch (informative)
```
program := namespace? topdecl* // imports may precede namespace
namespace := "namespace" ident ";"
topdecl := import | struct | enum | typealias | fndecl | externblock | globaldecl
import := "import" "."? ident ("." ident)* ";" | "import" ident "from" strlit ";"
declname := ident | ident "::" ident | "::" ident // the namespace, else the file's
qname := ident | ident "::" ident | "::" ident // a declaration reference
struct := "struct" declname generics? "{" fieldlist "}"
enum := "enum" declname generics? "{" variant ("," variant)* ","? "}"
variant := ident ( "{" fieldlist "}" )?
fieldlist := field ("," field)* ","?
field := ("let" | "const")? ident ":" type ("=" expr)? | "pad" intlit?
typealias := "type" declname "=" type ";"
generics := "<" ident (":" type)? ("," ident (":" type)?)* ">"
fndecl := "checkfree"? ("export" strlit? | "extern" strlit?)? "checkfree"? "simd"?
"inline"? "recursive"? ("fn" | "thread_fn")
declname generics? "(" params? ")" ("->" rettypes)?
(blockexpr | ("=" strlit)? ";")
// "=" strlit: extern only, its C symbol
// "extern" strlit: deprecated, the same
// checkfree at most once (§7.14)
// nested: ident only
externblock := "extern" strlit "{" externmember* "}" // §7.10: the library, then its functions
externmember := "fn" declname "(" params? ")" ("->" rettypes)? ("=" strlit)? ";"
params := param ("," param)* ","?
param := "var"? ident (":" type ("=" expr)?)?
// untyped => generic; defaults
// trailing, not in a block (§7.1)
rettypes := type ("," type)* // no parens in declarations
globaldecl := ("reusable" ("[" "]")?)? ("let" | "var" | "const") declname (":" type)? ("=" expr)? ";"
// no initializer: var with a type only
type := "const"? (prim | qname tyargs? | "(" type ")") postfix*
| "const"? indextype // no postfix (§3.11)
// const: the first & or [:] (§9.5)
indextype := ("u8"|"u16"|"u32"|"u64") ("in" | "upto") qname
// parenthesized as a for binder's type
prim := "bool"|"varint"|"i8"|…|"u64"|"f32"|"f64"
| "fn" ( "(" types? ")" ("->" (type | "(" types ")"))? )?
tyargs := "<" type ("," type)* ","? ">"
uint := "u8"|"u16"|"u32"|"u64"|"varint"
postfix := "[" expr "]" // fixed array (const expr)
| "[" uint? "]" // variable array
| "[" ".." expr? "]" // limited (const expr)
| "[" ">" ".." "<"? "]" // resizable
| "[" ":" "]" // slice
| "&" ("<" uint ("in" qname)? ">")? // reference / relative
| "?" // optional (any type)
| ".." // variable-mode ADT
| "." ident // variant type
stmt := decl | assign | incdec | guardstmt | deferstmt | exprstmt
decl := ("reusable" ("[" "]")?)? ("let" | "var" | "const") identlist (":" type)?
(("=" | ".=") exprlist)? ";" // .= binds by reference (§3.8)
assign := lvalue assignop expr ";" // = .= += -= *= /= %= &= |= ^= <<= >>=
incdec := lvalue ("++" | "--") ";"
exprstmt := expr ";"
guardstmt := "guard" expr ("else" blockexpr | ";") // guards the rest of its block (§6.4)
deferstmt := "defer" (blockexpr | assign | incdec | exprstmt) // runs at the block's exits (§6.6)
expr := control | binary
control := ifexpr | matchexpr | blockexpr | loops | jumps
ifexpr := "if" expr blockexpr ("else" (ifexpr | blockexpr))?
loops := ("while" expr | "for" forbind "in" iter | "loop") blockexpr
forbind := "&"? ident (":" type)? ("," ident (":" type)?)?
iter := expr | expr ".." expr
jumps := "return" exprlist? ("from" qname)? | "break" expr? | "continue"
matchexpr := "match" expr "{" arm ("," arm)* ","? "}"
arm := pattern "=>" expr
pattern := ident "&"? ident | item ("," item)* | "_" // a variant and its binder, ...
item := bound (".." bound)?
bound := "-"? (intlit | qname) // qname: a variant or a constant (§8.1)
blockexpr := "{" stmt* expr? "}"
binary := (precedence climbing; tightest → loosest)
postfixe := primary ( "(" args ")" trailingblock?
| "[" sliceargs "]" | "." ident | "as" "!"? type )*
unary := ("-" | "!" | "~" | "&") unary
levels := * / % → + - → << >> → & → ^ → |
→ < <= > >= → == != .== .!= → && → ||
primary := literal | qname | "null" | "self" | "(" expr ")"
| arraylit | structlit | genericcall
genericcall := qname tyargs "(" args ")" trailingblock?
trailingblock := "{" (params "=>")? stmt* expr? "}"
sliceargs := expr | bound? ".." bound? // bound := "^"? expr
args := (expr ("," expr)* ","?)?
arraylit := "[" (exprlist ","? | expr ";" expr)? "]"
structlit := (qname tyargs? | varianttype) "{" (fieldinits | fieldinits? "..")? "}"
fieldinits := fieldinit ("," fieldinit)* ","? // before "..": the "," required
fieldinit := (ident ":")? expr
```
Known parse notes:
* The `structlit` vs `trailingblock` vs `blockexpr` ambiguity is resolved
as in Rust (no struct literals or trailing blocks directly in
`if`/`while`/`for`/`match`/`guard` scrutinee position).
* A statement that is a block-ended construct needs no `;` and ends at its
`}` (§2).
* `f<` commits to a type argument list only when followed by `(`, `{`, or
`.ident {` (§7.7); otherwise `<` is comparison.
* `T&` tokenizes as `&` `<` in type context only.
* `a..b` ranges exist only in `for` headers, slice brackets, and match arms.
* Multiple return types in a function *type* (not a declaration header)
require parens around the list.
* `T&?` parses to the same type as `T?`, and `?` on an already-optional
type is an error.
* `from` is not reserved: `return E from f` is recognized by the identifier
that follows it, so `from` remains usable as a name.
---
## Appendix E. Implementation notes (informative)
What the current compiler does where the text above leaves it a choice. The
compiler's own description, pass by pass and analysis by analysis, is
`implementation.md`.
* **Two C backends.** The generated C is normally written to a file for a C
compiler to build (`-o out.c`). Where the compiler was built with TinyCC
(the `third_party/tinycc` submodule), giving no output file compiles that
same C inside the compiler's own process instead and calls its `main`, so
a program can run without a C toolchain and without writing a file. The
program shares the compiler's process: its exit status becomes the
compiler's, and the compiler's own progress lines go to stderr so that
stdout is left to the program. It runs on the compiler's main thread. The
compiler reserves that thread's stack at 64 MB on Windows and macOS,
where an executable's main thread gets 1 MB and 8 MB respectively by
default; on Linux it is the stack limit the process started with
(`ulimit -s`), as for an executable. The program's own C is the same for
both backends, and `-D` is written into that C rather than passed to a
backend so that this stays true. What differs is the runtime around it.
The in-process build compiles the whole runtime with the program. The
file holds only the parts of the runtime that have to be inlined into the
program and declares the rest; `goose --emit-runtime` writes that rest as
a C file of its own, built once and linked with every program
(`--standalone` writes a file that holds the whole runtime, as the
in-process build does). TinyCC does not optimize and cannot place
thread-local storage in an in-memory run, so a program that uses workers
(§11.2) is refused there (docs/design/jit_backend.md).
* **Fat references.** A callee that grows a resizable through a reference
(`push` through a `f64[>..]&`) must know which data stack to bump. The
compiler does not pin that stack per call site (§10.2): every reference
to a resizable-class value carries the stack identity beside the header
address (C.2), and a pool reference (§5.4) carries the freelist as well.
There is no surface syntax; the form is per instantiation, and a variable
keeps the one its first binding gives it (§5.4).
* **Stack assignment** uses the hidden-argument strategy permitted by §10.3.
Every function that uses data stacks receives a base index as a hidden
argument and addresses its nonfixed locals at constant offsets from that
base. Callees start above the function's in-use watermark. All stacks
reachable by the program are reserved when its thread program starts; their
count is known at compile time (§7.8).
The globals of a program instance, together with the dedicated stacks of
the resizable globals, form one struct reached through a single
thread-local pointer (a plain pointer in a program without workers).
Main's struct is a static instance; a worker's is allocated when the
worker starts and filled from the spawn image, which carries the used
globals after the arguments. The only C statics are string literals and
`const` globals with compile-time initializers. Every generated function
is shared between thread programs and the main program.
* **Element-run results** (§7.3) exist for variable *array* results
(`T[]`): an element-run receiver compiles the callee a second time in run
form (raw elements plus a count out-parameter), so `v.append(f())` is
contiguous with no copy, and a named result incurs the specified single
callee-side copy. Callees with no run form (builtins, tag dispatch) fall
back to the value form, and the receiver slides the length prefix out
with one memmove. A runtime-capacity limited result is built on the
destination stack, then compacted there to discard its header and unused
slots. Non-array variable results have only the value form.
* **Queue images** (§11.2) are one contiguous byte image per value; a
resizable's is its count, its fixed fields and its tail's elements, built
on a scratch stack at `qput` and unpacked at the receiver's destination.
* **C locals at function scope.** Every aggregate-typed C local (a struct,
array, slice or header) is declared at the top of its C function rather
than in the block that uses it, and a literal temporary with unused
limited-array slots is zero-initialized. Both work around MSVC
19.44–19.51. At any optimization level it miscompiles a read through a
copy of a struct that holds the address of a block-scoped local (for
example a slice of a fixed array declared in a loop body, passed to an
inlined callee), and it exploits the indeterminate bytes of a copied,
partially written struct. Neither workaround changes what the program
means.
* **The store record.** Every store of a reference, slice, or value holding
one, into a variable, field, element or global, is logged program-wide
with the stored value's root, exactness, and source container. A
specialization also summarizes the stores into its parameters' pointees,
and these summaries are replayed onto the caller's containers at each
call. A grow-only shrink (§5.1) checks this record for any live value
that holds a reference into the array, following source links (a copy
holds what its source holds; a global source is checked by its type). A
shrink inside a loop is checked again at the loop's end against the
stores that the rest of the body made.
* **Liveness at a shrink.** The checker keeps the statement index of every
open block; a holder is live when its name occurs in a later statement
of an open block at or inside its scope, in that block's tail, or
anywhere in an enclosing loop it outlives. Nested functions in scope are
followed by name from the calls in that code. What a `for` walks in
place, and the references and slices on the path to it that the loop
loads again, are live throughout its body (§6.5).
* **Literal parameters** (§7.7). Which parameters are literals is part of
the specialization key; their values are not. A read of a literal
parameter is a value that is not constant-folded but keeps the literal's
adaptability. Every adaptation of the bare parameter is recorded on the
specialization with its line, and a literal parameter passed on as a
literal records a link to the callee's parameter. After the whole program
is checked, each call site's literal is verified against the closure of
those records. The parameter is passed at its nominal type (`int64_t`,
`double`), and each use casts it to the type it adapted to; the
verification proved that cast exact.