Variables

Three binding forms, reactive bindings, contracts, and destructuring.

Summary

let is immutable (reassignment is a runtime error), var is mutable, and const is identical to let at runtime but signals intent. All three accept optional type annotations. bind creates a reactive binding that recomputes automatically when its dependencies change; cascading is supported.where clauses add runtime-checked contracts to any binding or function parameter. Array, tuple, and struct destructuring work in any binding position.

Canonical

let x = 42          -- immutable binding
var y = "hello"     -- mutable binding (can reassign)
const MAX = 100     -- constant (same as let at runtime, signals intent)

-- with type annotations
let count: int = 42
var name: str = "XS"

let bindings cannot be reassigned: that's a runtime error. var bindings can be reassigned with =.

const is identical to let at runtime.

Reactive Bindings

bind creates a variable that automatically recomputes when its dependencies change.

var price = 10
var qty = 3
bind total = price * qty
println(total)                   -- 30

price = 20
println(total)                   -- 60 (auto-updated)

qty = 5
println(total)                   -- 100

-- bindings can depend on other bindings (cascading)
bind doubled = total * 2
println(doubled)                 -- 200

price = 1
println(total)                   -- 5
println(doubled)                 -- 10

-- works with strings too
var name = "world"
bind greeting = "hello " ++ name
println(greeting)                -- hello world
name = "xs"
println(greeting)                -- hello xs

bind tracks which variables are read when the expression is first evaluated. When any of those variables are reassigned, the binding automatically recomputes. Cascading works: if binding A depends on binding B, and B's dependency changes, both B and A update in order.

Reactivity is wired through the interpreter, the VM, and the JIT; all three replay the bound expression on dependency change. Transpiler targets (--emit js, --emit c, --emit wasm) lower bind as a regular let since static targets can't observe variable mutation through the same hook.

Contracts (where clauses)

Add where after a type annotation to enforce a condition on the value. The condition is checked at runtime.

let age: int where age > 0 and age < 150 = 25
let name: str where name.len() > 0 = "xs"
let score: int where score >= 0 and score <= 100 = 85

If the condition fails, a catchable error is thrown:

try {
    let bad: int where bad > 100 = 5
} catch e {
    println(e)                   -- contract violation
}

Contracts work on function parameters too:

fn divide(a: int, b: int where b != 0) {
    return a / b
}

println(divide(10, 2))           -- 5
divide(10, 0)                    -- throws: contract violation

Contracts are gradual: no where clause means no checking. Add them where you want enforcement. They're checked at runtime in the interpreter. In the VM and transpilers, contracts on function params are not yet enforced (variables are).

Destructuring

-- array destructuring
let [a, b, c] = [1, 2, 3]
println(a)               -- 1

-- tuple destructuring
let (x, y) = (10, 20)
println(x)               -- 10

-- nested tuple destructuring
let (a, (b, c)) = (1, (2, 3))
println(c)               -- 3

-- struct destructuring
struct Point { x, y }
let Point { x: px, y: py } = Point { x: 100, y: 200 }
println(px)              -- 100

Array destructuring requires an exact length match.