Bindings, Scope, Cells, and Mutation

Every lexical binding is a mutable cell. Closures capture frames and therefore share later `set!` updates to the same cell.

When this matters

Use bindings to give a result a name, create a local scope, or deliberately keep state between calls. Most code needs only define and let; reach for set! only when shared mutation is part of the model.

See it run

LISPEX
(define x 1)
(define get (lambda () x))
(set! x 2)
(get)

Observed result

OUTPUT
2

Read the example

get closes over the global cell named x, not a frozen copy of the number 1. set! writes 2 into that same cell, so the later call observes 2. A new inner x would instead shadow the cell.

How to reason about it

  • Lookup chooses the nearest lexical cell; shadowing does not mutate an outer binding.
  • letrec allocates cells before initializers and raises E321 when an uninitialized cell is read.
  • A duplicate top-level define updates the existing global cell, preserving earlier closure references.

Choose quickly

FormUse it forVisibility of initializers
definea top-level name or named procedurecurrent top-level environment
letindependent local valuesouter environment
let*locals that depend on earlier localseach earlier binding
letrecmutually recursive local proceduresall allocated cells; early reads fail
set!updating an existing cellno new binding is created

A common mistake

set! on an unbound name is E303; it never creates a binding.

Current boundaries

  • The internal uninitialized sentinel cannot be represented as a guest value.

Keep going

Procedures shows how closures capture these cells. The closures guide turns that rule into a small stateful example.

Procedures · Closures guide