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.

let* creates its names one after another from the top, so a later initializer can see the name just created.

LISPEX
(let* ((x 6)
       (y (+ x 1)))
  (* x y))

Observed result

OUTPUT
42

letrec allocates every cell before it evaluates any initializer, which is what lets a name call itself.

LISPEX
(letrec ((f (lambda (n)
              (if (= n 0)
                  1
                  (* n (f (- n 1)))))))
  (f 5))

Observed result

OUTPUT
120

How to reason about it

  • Lookup chooses the nearest lexical cell, and 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, though 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 marker for an uninitialized cell 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