Download

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 @comptime constant 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.