Unit-level storage: the var declaration¶
Status: DECIDED (#546). A var is one storage
per program, in the data segment, with an address.
The rule¶
public var File stdout = File(fd: STDOUT_FD, owned: false) # another unit may name it
var i32 next_id = 0 # this unit only
A var declaration stands at the top level of a unit, beside a const. It has the same
shape -- a marker, a type, a name, an initializer -- and a different kind:
const |
var |
|
|---|---|---|
| storage | .rodata, one copy per module that reads it |
the data segment, ONE per program |
| address | none: a read copies the value out | yes: a rebind, a poke, a field write and a mutating method reach it |
| initializer | a constant expression | a constant expression, plus an EMPTY container |
| lifetime | none | initialized before main, never destroyed at exit |
| moved out of | never (CE2436); a plain value copies out | the same |
| visibility | public explicit, private by default |
the same |
What it is for¶
The console handles are the main case. stdout, stderr and stdin are public var
File, because the Writer contract takes poke self (so that a BufWriter@(W) can
implement it), and nothing writes a constant (CE2400). A var gives the console handle
storage the contract can write.
A private var is the common case in Go and Zig code, and it is what keeps public var
honest:
- A flag set once, read everywhere in the unit.
var bool verbose = false, set fromargvinmain, read by every printer in the file, with no parameter threaded through twenty signatures. - A counter or an id generator.
var i32 next_id = 0behindfn fresh_id() i32. - State a module keeps for itself. A random generator's seed.
- A cache or a registry, filled on first use:
var List@(Entry) cache = List.new().
The initializer¶
The initializer is a constant expression, evaluated by the same evaluator a const uses:
a literal, a constant, an operator, as, an interpolation, a struct or an enum variant
built from constants.
There is no run-before-main initializer (Go's), because that brings initialization ORDER
with it. Two consequences:
- A
varcannot name anothervarin its initializer, and aconstcannot name avarat all: the value is read at run time (CE0108 either way). - An empty container qualifies:
List.new(),from([])andnew()are the literal descriptor{0, 0, null}and allocate nothing, so the backend emits them as the zero value of the type.HashMap.new()does not qualify: it mallocs its buckets on the spot.from([1, 2])does not either: the elements need a buffer. Both are CE0108. One predicate,allocates_nothinginsemantics/const_eval.py, is read by the typecheck pass and the backend alike, so the two cannot disagree about what qualifies. - An enum variant qualifies: a payload-free variant is a tag over a zero
payload, so
var Maybe@(HashMap@(K, V)) cache = Maybe.Noneis the cache-filled-on- first-use shape the ruling names, and the first call rebinds it withcache := Maybe.Some(map). The variant is built against the DECLARED type -- the internedMaybe<HashMap<K, V>>, not the bareMaybe-- which is why the declaration is resolved before the initializer is read.
The borrow checker: one storage class beside "local"¶
A var gets a BorrowState at every function's entry (is_unit_var), so the rules that
already exist apply to it with one addition:
- Borrowable like a local.
peek vandpoke vtake its address; onepokeat a time (CE2403); apokebeside apeekis CE2407.foreach(poke r in v.iter())points into its element storage. - Never moved out of (CE2436).
f(nom v),let T x = v,return vand anom selfmethod such asclose()would hand storage nothing re-initializes to a callee or a binding that frees it. The rule is CE2410's, the one that fencesmain's argv view, and it applies to an OWNING type only: a plainvar i32copies out freely. It reads the same for aconst, which has no owner either: a take of aconst stringis CE2436 and.clone()is the escape, while aconst i32copies out. - A rebind is the one way to change what it holds.
stdout := fconsumesf, drops the old value the way a local's rebind does, and stores the new one. Alet-borrow out of avar(let string first = names[0]) freezes it exactly as it freezes a local: a mutation of thevarwhile the binding is live is CE2412 at the binding's next use. - Never frozen across a call, because no function owns it. A callee may rebind a
varthe caller is reading; that is what storage means, and it is the caller's to order.
The scope pass owns "what kind of name is this". It and the typecheck pass ask one gate
for every borrow position: reject_borrow_of_constant in semantics/constant_borrow.py. A var passes every
position a constant fails there -- a poke/peek of it, a poke foreach over it, a
let poke/let peek bound from it, a poke self call on it, a poke pattern binding
into it, and the same through an alias -- and it passes the CE1002 gate on a rebind
target. The typecheck pass's CE2096 gate (a write into a constant) asks the record's
is_var and lets a var through -- behind an alias too, so geo.count := 3 writes and
geo.SIZE := 3 is refused.
Every one of those readers asks the SCOPED lookup, never ConstantTable.by_name. The
flat view holds one record per NAME over the whole program and is first-wins, so it
cannot tell the var of one unit from the const of the same name in another unit.
Never destroyed at exit¶
A var is registered with no scope, so nothing frees it when main returns. That is
deliberate: the process ends, the operating system reclaims the pages, and an exit-time
destructor pass would need an order between units that nothing else needs. A program whose
var holds heap at exit therefore shows those bytes to a leak checker; a test over such a
program carries no EXPECT_NO_LEAKS.
The backend: one storage, external linkage¶
A constant is emitted internal into every module that reads it, which is harmless for
a value. A var is ONE storage, so:
- the declaring unit's module DEFINES it (
@io$fs$stdout = global %File {...}), with external linkage under the unit-mangled symbol (docs/design/unit-namespaces.mdsection 9); - every other module DECLARES it (
@io$fs$stdout = external global %File); - a consumer of a BINARY library declares it under the manifest's
link_symbol, and the library's bitcode carries the definition.
The reads and writes reach it through the seams a constant uses:
resolve_name_slot answers the global where it answered a local's slot, and
namespaced_storage (backend/expressions/names.py) is the ONE reader of an alias that
reaches storage -- a rebind, a poke, a field write and a mutating method all ask it.
Library manifest¶
public_variables mirrors public_constants -- name, unit, type, the declaration as
source, an optional doc -- plus link_symbol. A source library needs none of it: its
units are recompiled and the per-unit rule above applies. A private var is named in
not_exported with kind variable, so a consumer naming it hears CE3005 and not CE1001.
A private var an exported generic's body names ships in the export closure's constants
with its link_symbol, and the consumer declares it.
What was refused¶
static: in Sushi "static" already means a function called on a TYPE name (List.new(),f64.from_bits(b);Static(ty, name)indocs/design/ir.md), "static dispatch" is everywhere, and in Cstaticmeans internal linkage, close to the opposite of an exported unit-level value. The word is the surface marker for a receiver-less method --extend Vec static at(i32 x, i32 y) Vec:-- so it means exactly what it means internally, and Sushi has no static STORAGE at all. The record isdocs/design/method-resolution.md.letat the top level:letnames a block-scoped binding with RAII drop, and one word would carry two lifetimes.global: the runner-up.varis the Go, Zig, Swift, Nim and Pascal word for unit-level storage, and it reads againstconstthe way the language needs.- A kind public by nature: a
varthat could only be public would be the one place where the visibility rule bends, and a unit's own counter, cache or flag has every reason to stay private. - Two contracts (a
BufRead-style perk for the buffered types) and relaxing CE2400 so a constant could satisfy apoke selfcontract: both were the routes the ruling did not take. A buffered-direction contract forlines(),read_line()andfill()is a separate, later question.