Language Foundations

Bindings, Scope & Closures

Choose immutable or mutable bindings, understand lexical scope, and see a closure share one live mutable cell.

A binding gives a value a name. Its declaration also answers two questions that matter later: may the name be reassigned, and where is it visible? A closure can keep that binding alive after the surrounding function has returned.

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

The two calls share one counter, while the inner label disappears with its block:

Output
inner: 11, 12
outer

Follow the binding, not a copied value

makeCounter creates the mutable binding current and returns a lambda. The lambda refers to current, so it captures that binding. Because the original declaration used let mut, assignment inside the lambda is valid.

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

label demonstrates lexical scope. The inner block may shadow the outer name with its own binding. Inside the block, label means "inner". Once the block ends, that binding is out of scope and the outer "outer" binding is 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 later 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; they do not receive 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 first immutable and mutable bindings. Continue with Functions & Generics to choose between named functions, lambdas, and generic functions.