6. Bindings and initialisation
6.1 let, var, and zero values#
let width = 1920
let title: str = "Preview"
var frame = 0
frame += 1let binds once. var may be reassigned and never changes type. Parameters are let. A let requires an initialiser. A var requires an initialiser unless it has a written type that has a zero value, which it then holds. A type has a zero value when it is zeroable, or when it is a struct with no init of its own whose every field has a default or a zero value: that struct's zero value is S(), its defaults filled and every other field holding its own zero value.
A type is zeroable when the all-zero bit pattern is a valid value of it:
- integers, floats,
bool,char,usize,isize; - nullable pointers, nullable functions, nullable interface views;
@Tof a zeroableT;- spans and
str: the zero value is the empty span with the dangling pointer of §5.4; T?of any type: the zero value isnone;- integer-backed enums with a case whose value is
0; - payload enums whose first declared case carries no payload: that case is the zero;
- arrays and unions of zeroable types;
- structs whose fields are all zeroable and that declare neither a custom
initnor a field default.
Zeroable is the narrower property: the all-zero bit pattern itself is the value, so it is what new T[count], a zeroed array and a global without an initialiser need. A struct whose zero value comes from its defaults is not zeroable, and holds its zero value only where it is constructed: a var, a field omitted from a construction, or S().
The following have no zero value and always require an initialiser: T*, const T*, volatile T*, func(...) -> R, a bare interface view, a struct with its own init, and every aggregate that contains one of them, except a struct field given a default.
var count: u32 # 0
var buffer: u8[4096] # zeroed
var next: Node*? # none
var origin: Point # every field zero; rejected if Point holds a bare pointer
var style: Style # Style(): its defaults, every other field zeroWhy. C's char buf[4096] = {0}; is the most common initialiser in systems code, and = {0} is easy to forget. Zero by default is what Go and Odin do, and it is safe precisely because the never-null types are excluded: a zeroed Node* would be a null pointer with a non-null type, so it is not allowed to exist.
6.2 Explicit non-initialisation#
var buffer: u8[4096] = ---
var nodes: Node[64] = ------ allocates the storage without writing it, for a var of any type. Reading before writing is undefined, as in C. The linter warns on every --- by default; a diagnostic build fills the storage with the byte 0xAA (§19.4).
Why. A large buffer that a read call will fill should not be zeroed twice, and a pool of structs that hold bare pointers has no other spelling: the type is not zeroable, and the pool will be filled by the program before use. The spelling is deliberately conspicuous so that it can be searched for.
6.3 Module globals#
var next_id: u32
var seed: u32 = 0x9E3779B9
local var current_arena: Arena*?
pub let max_header: usize = 16 * 1024A module may declare var at top level. It is zero before main runs, or holds the constant its initialiser names; the initialiser must be a constant expression (§6.4), so there is no initialisation order. local var is one such variable per thread, C11's _Thread_local: ordinary zero-initialised or constant-initialised storage in each thread, with no per-access guard; every thread's copy starts with the initialiser, a spawned thread's as the initial thread's. A global's initialiser also runs nothing: a construction through a type's own init, or one that leaves out a field default that calls, is a constant where it is made at its use (a default argument, a top-level let) but not a value storage can hold before anything runs, on every thread for a local var; the program keeps an optional and sets it where it is first needed. A top-level let is a constant. C's function-scope static local is a module-level var.
Why. Full Luce forbids mutable globals because their initialisation order and their effect on test isolation are unmanageable. Base cannot forbid them, because a C replacement without globals is not a C replacement, but it removes the two hazards: a constant initialiser is static data with nothing to order, and the test runner is to report which globals a test wrote (§16.5, planned).
6.4 Constant expressions#
A constant expression is built from literals; memory.size_of, memory.align_of, and memory.offset_of; luce.location, luce.file, luce.line, and luce.function; arithmetic, bit, comparison, and cast operators on scalar constants (a lane-wise operator of §5.12 is not one); array and tuple literals of constants; enum cases and | on integer-backed enums; struct construction from constants; the address of a global or a function, or of a global's element or field reached by constant steps; and top-level let names. It may appear as a top-level initialiser, an array length, a default parameter value, and the condition of a module-level assert. luce.location (and its pieces) expand to the file, line, and function of the use site; when used as a default argument they expand at the call site.
6.5 Assignment#
The right-hand side is fully evaluated before the destination changes. Values copy. A field or index path is one lvalue, evaluated once. Assignment is a statement; there are no ++, --, or assignment expressions. A tuple binding let (a, b) = pair destructures once, left to right.
var cursor = Cursor(position = 1)
cursor.position = 4
items[index].selected = true
*p = value
p.field = value6.6 Address-of, mutability, and the escape rule#
&x yields the address of an lvalue. The pointer's qualifier comes from the nearest root of the path: a var binding or a dereference of a T* yields T*; a let binding, a dereference of a const T*, or a read-only span yields const T*. So &task.link is Link* when task is a let holding a Task*, because the path is rooted at the pointee, not the binding. Stores follow the pointee: *p = v requires T*; p.field = v requires T* and a var field.
The address of a local, a span or str of a local array, and a span memory.frame yields (§12.7) may be passed down but not up. The compiler rejects three uses of such a pointer or view: returning it; storing it in a global; and storing it through a pointer parameter or into a struct that is returned. Passing it as the message to error(...) is allowed, as error copies the message (§11.3); a caught failure's message has a lifetime of its own, its handler's, under the same kind of rule. The check follows let aliases within one function and stops at calls; a value read through a pointer or a view is not the pointer, so let row = &rows[n] taints row and not row.next, and a global of an imported module is a global. It catches the common mistakes and promises nothing about the rest, which remain the programmer's responsibility, as in C.
Why. C has &x and no way to say whether the result may be written through. Deriving the qualifier from the path gives the same information with no annotation, and it is the same rule Luce already uses to decide whether a field path may be assigned.