Language Foundations

Bindings, Scope & Closures

Select immutable or mutable bindings, understand lexical scope, and observe closures sharing live mutable cells.

A binding associates a name with a value. Its declaration answers two essential questions: can the name be reassigned, and where is it visible? A closure can keep that binding alive even after the enclosing function returns.

Watch one captured binding change

Save this as bindings-scope-closures.tpz:

TOPAZ
function makeCounter(start: int) -> () -> int {
    let mut current = start
    () => {
        current = current + 1
        current
    }
}

let label = "outer"
let next = makeCounter(10)

{
    let label = "inner"
    print("{label}: {next()}, {next()}")
}

print(label)

Check and run it:

BASH
topaz check bindings-scope-closures.tpz
topaz run bindings-scope-closures.tpz

Both calls share a single counter, while the inner label disappears when its block ends:

Output
inner: 11, 12
outer

Follow the binding, not a copied value

makeCounter creates the mutable binding current and returns a lambda. The lambda references current, capturing that binding directly. Because the original declaration used let mut, reassignment inside the lambda is valid.

The closure does not receive a frozen copy of 10. It retains the same mutable cell. The first call updates the cell to 11. The second call reads that new value and updates it to 12. The cell remains alive because next still refers to it after makeCounter returns.

label demonstrates lexical scope. The inner block may shadow the outer name with its own binding. Inside the block, label evaluates to "inner". Once the block ends, that binding goes out of scope and the outer "outer" binding becomes visible again. Redeclaring the same name twice in one scope is a static error.

Choose the binding deliberately

FormUse it forReassignment
constA compile-time value built from the restricted constant expression grammarNot allowed
letAn ordinary runtime value that keeps the same bindingNot allowed
let mutState whose reassignment is part of the modelAllowed

Prefer let. Add mut only when a subsequent assignment is meaningful. This keeps changing state visible at the declaration instead of making every name potentially mutable.

Parameters and names introduced by patterns are immutable. If a function needs changing local state, copy the parameter into a new let mut binding with a name that describes that state.

Common mistake: treating capture as a snapshot

Capturing an immutable binding is read-only. Capturing a mutable binding preserves its live cell, so writes made through one closure are visible to later reads through the same binding. If two closures capture the same mutable binding, they share that cell rather than receiving independent counters.

Scope still applies when a closure is created. It may capture names that are lexically visible at that point, but it cannot reach a name declared later or a sibling block’s private binding.

Exact limits

  • Inner scopes may shadow an outer name. One scope may not declare the same name twice.
  • An immutable let, a const, a parameter, or a pattern binding cannot be assigned.
  • Mutable declarations accept a bare identifier rather than a destructuring pattern.
  • Topaz closures have no explicit capture list, move-capture syntax, Rust closure traits, or borrow-receiver syntax.
  • Resource lifetime is a separate decision. The File-only using form and deterministic cleanup belong in Defer & Resources.

Revisit Values and Functions for the initial immutable and mutable bindings. Continue with Functions & Generics to choose between named functions, lambdas, and generic functions.