5. Variables and Bindings
Binding forms#
A binding takes one of four forms. The annotation : T gives a type inference cannot decide (buffer: u8[] = []), or makes it visible.
| Form | Mutability | Type | Example |
|---|---|---|---|
name :: value |
immutable | inferred | pi :: 3.14 |
name := value |
mutable | inferred | total := 0 |
name: T: value |
immutable | typed | cap: i64: 1024 |
name: T = value |
mutable | typed | count: u64 = 0 |
n := 0 // mutable, inferred i32
n = n + 1 // OK
k :: 0 // immutable
k = 1 // error: cannot reassign immutable binding
Only a mutable local, a parameter or a name a pattern binds may be rebound. A declaration cannot be rebound, even renamed by an import or reached through a namespace (P.SEP = 'x'), and neither can a for ... in variable (§7 for (universal loop)) or a match header name (§4 Pattern matching).
The compiler warns about a mutable local never rebound: only an assignment to the bare name rebinds it, compound assignments included; x.f = 1 and x[0] = 1 do not. A destructuring := draws no warning when one of its names is rebound.
At module level, only the immutable forms declare; module state lives in a @singleton (§12 @singleton).
The top level is not ordered. A constant or a function may read a module constant declared below it: INNERS :: [ONE] above ONE :: Inner { .n = 7 } is an Inner[].
- A constant whose value reads itself, directly or through others (
A :: B + 1,B :: A + 1), is an error. A@comptimeconstant in the cycle reports it when evaluated. - A read inside a lambda waits until the lambda runs, so an annotated constant may name itself there; an unannotated one may not.
fact: fn(i32) -> i32 : (n: i32) -> if n <= 1 { 1 } else { n * fact(n - 1) }
Mutability: two independent axes#
- The binding.
::forbids rebinding the name,:=allows it (Binding forms, above). An assignment targets a name, a field or an element; any other target (get() = 3,xs.len() = 5,Direction.North = .South) is an error. - The referent. A value may be modified through any name that reaches it: a parameter, a local, a field, an element. No signature says whether a function modifies what it is lent (What a function does with what it is lent, below).
Only two kinds of value are frozen, which the compiler forbids modifying in place: the value of a module constant, at every level, and a view of a string's bytes (s.bytes()).
CONFIG :: Config { .retries = 3 }
main :: fn() -> i32 {
p :: Point { .x = 1, .y = 2 }
p.x = 5 // OK: `::` froze the name, not the contents
// p = other_point // error: that is the binding
// CONFIG.retries = 5 // error: a module constant is frozen
c :: CONFIG
c.retries = 5 // OK: `c` holds a copy of the constant
return c.retries
}
Frozen values#
A module constant cannot be modified, at any depth. These are errors:
- a write through it, directly or down a path (
CONFIG.retries = 5,TABLE[0].n = 1,[PAIR[1], y] = ...); - lending it to a function that modifies it, directly or down the calls it hands it to. A call through a trait, a bound or method syntax is judged by every function it may run.
Binding it to a name, storing it in a field or an element, returning it or handing it to a function value makes a copy the program owns. A write to such a copy, or to a copy of variant data, that the body never reads nor keeps draws a warning: c.n = 0 in for c in TABLE { ... } changes nothing. A write below the name is no read of it, and a name that may hold another value (a parameter, a branch, a rebinding) draws none.
TABLE :: [1, 2, 3]
fill :: fn(buf: i32[], value: i32, n: i32) -> void {
for _ in 0..<n {
buf.push(value)
}
}
refill :: fn(buf: i32[]) -> void = fill(buf, 0, 4)
fill(TABLE, 1, 2) // error: `fill` modifies it (`buf` grows)
refill(TABLE) // error: `refill` modifies it through `fill`
fill(TABLE.clone(), 1, 2) // OK: a copy
A view of a string's bytes (§4 Strings) is the string's own bytes, never copied implicitly. It may be read, lent to a function that only reads it, and returned. Writing it, lending it to a function that modifies it, keeping it (in a field, an element or a module constant's value) or handing it to code the compiler cannot follow (a function value) is an error.
Only the view itself counts: a byte read from it is a value, and so is a name narrowed to a type no byte array is (r: u8[] | Failure in if r is Failure { failures.push(r) }). .clone() makes a copy the program owns. The result of a function that returns a view is a view, as that of bytes() is.
bytes_of :: fn(s: string) -> u8[] = s.bytes() // OK: it returns the view
v :: "abc".bytes()
v[0] = 'x' // error: written
bytes_of("abc").push(0) // error: `push` modifies it
p :: Packet { .payload = "abc".bytes() } // error: kept
q :: Packet { .payload = bytes_of("abc").clone() } // OK: a copy
What a function does with what it is lent#
The compiler reads from a function's body what it does with each parameter: which parts it assigns, grows or shrinks, keeps past the call or returns. Calls are followed over the whole program: a write at the end of a call chain counts at the call that lends a frozen value.
An intrinsic is described by the compiler, an @extern by its @c_out parameters (§13), and a function value may do anything. The same reading decides whether an array argument may widen its element (§4 Variance).
No mutability marker#
A type says nothing about what may be written: a ! after a type is a syntax error, const before one an error.
Scoping#
Scopes are lexical (a function, a block), and a local is visible from its declaration on. A local may shadow an outer local or a parameter in a sub-block, or a name declared earlier in its own block; the compiler warns when it does.