6. Bindings and initialisation
6.1 let, var, and zero values#
let width = 1920
let title: str = "Preview"
var frame = 0
frame += 1
let 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 is zeroable, in which case it holds that type's 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, functions, and interface views; @T of a zeroable T; spans and str (the empty span with the dangling pointer of §5.4); T? of any type (none); 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; and structs whose fields are all zeroable and that declare neither a custom init nor a field default.
The following have no zero value and always require an initialiser: T*, const T*, volatile T*, func(...) -> R, a bare interface view, and every aggregate that contains one of them.
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
Why. 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 wearing a non-null type, so it is not allowed to exist.
6.2 Explicit non-initialisation#
var buffer: u8[4096] = ---
--- allocates the storage without writing it. It is admitted only for arrays and structs whose fields are integers, floats, bool, or such arrays and structs, never for anything holding a pointer, an enum, a str, or an optional. Reading before writing is undefined, as in C. The linter warns on every --- by default; a diagnostic build fills the storage with a pattern.
Why. A large buffer that a read call will fill should not be zeroed twice, and C programmers will not accept a language that makes them pay for it. The spelling is deliberately ugly so that it is findable.
6.3 Module globals#
var next_id: u32
thread_local var current_arena: Arena*?
pub let max_header: usize = 16 * 1024
A module may declare var at top level. Its type must be zeroable; it is zero before main runs; there is no initialisation code and no initialisation order. thread_local var is one such variable per thread, C11's _Thread_local, zeroed on each thread's first use. A top-level let is a compile-time constant: literals, arithmetic, arrays and tuples of constants, enum cases, and struct construction from constants. 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: there is no initialiser to order, and the test runner reports which globals a test wrote (§16.5).
6.4 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 = value
6.5 Address-of and mutability#
&x yields the address of an lvalue. A path rooted in a var yields T*; a path rooted in a let, a const pointer, or a read-only span yields const T*. There is no second spelling. Stores follow the pointee: *p = v requires T*; p.field = v requires T* and a var field.
Why. C has &x and no way to say whether the result may be written through. Deriving the qualifier from the binding 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.
6.6 Escape rule#
The address of a local, and a span or str of a local array, may be passed down but not up. The compiler rejects returning such a pointer or view, passing it to error(...) as the message, storing it in a global, and storing it through a pointer parameter or into a struct that is returned, when it can see the flow within one function. The analysis is local and conservative. It catches the common mistakes and promises nothing about the rest, which remain the programmer's responsibility, as in C.