(Lispex)sicp.io
Chapter 3 · Checkpoint

Track locations. Delay work. Mutate only the needed link.

Use twelve canonical lesson programs to reconnect private state, delayed work, mutable structure, simulated time, protected updates, cooperating local definitions, and nested table records.

Checkpoint question

Can you distinguish the binding that persists, the work that has not happened yet, and the exact link or record that a mutation changes?

  • Follow assignment across calls to one stateful closure
  • Distinguish private locations created by separate closures
  • Separate the first force from a memoized force
  • Demand only a finite prefix of an infinite stream
  • Track front, rear, and link mutations in a queue
  • Reproduce the same state transitions from one seed
  • Propagate a missing value through connected constraints
  • Order events by explicit simulated time
  • Locate a lost update caused by stale reads
  • Serialize two state changes through one shared lock
  • Resolve mutually recursive helpers inside one private invocation environment
  • Update existing table records and link new nested records

Local state and private environments make history observable through bindings that survive a call. Memoized promises and streams add a second dimension: some work exists as a delayed computation and becomes observable only when demanded.

Queues and tables both mutate selected links rather than reconstructing the whole structure. A queue changes its end pointers and rear link; a table changes a found record or the cdr of the relevant table header when a key is new.

Seeded randomness, constraint propagation, and event agendas turn otherwise implicit context into explicit state: a seed, a network of known values, or a time-ordered event list.

Interleaving and serialization compare unprotected reads with one selected protected schedule. Internal definitions show another use of an environment: several local procedure names can cooperate while remaining private to one invocation.

Lispex · SICP sourceScheme-compatible SICP syntax executed by the Lispex SICP profile.
(let ((balance 100))
  (let ((withdraw
         (lambda (amount)
           (if (>= balance amount)
               (begin
                 (set! balance (- balance amount))
                 balance)
               'insufficient-funds))))
    (let ((first (withdraw 25)))
      (let ((second (withdraw 25)))
        (let ((third (withdraw 60)))
          (list first second third))))))
Lispex learning runtimeLispex SICP profile 1.0.0
Loading Lispex SICP runtime
Lispex · SICP source384 / 1,048,576 UTF-8 bytes
Examples
Result
Output
Value
Diagnostic
Visible execution0 / 0 trace events
    This browser result is not a Lispex Vouch record or authority.wasm —
    Expected observations

    Each program returns the first expected observation from its corresponding Chapter 3 lesson. The internal-definition program returns ((#f #t) (#t #f)). The one-key table program returns (9 5 #f (*table* (beta . 5) (alpha . 9))).

    What the trace can show

    Find assignments to closure state, first and memoized force paths, demanded stream tails, queue links, seed updates, connector notifications, ordered event insertion, stale balance reads, lock acquisition and release, alternating local helper calls, assoc traversals, and set-cdr! on records versus table headers. These bounded traces describe only the selected runs.

    Explain before revealing

    Twelve checks for state, time, and mutable structure

    What changes when an answer depends on the calls that came before it?

    Answer Three nested single-binding let expressions make the sequence explicit. Each result therefore reflects the state left by the preceding call instead of starting again from 100.

    Why do two counters made by one procedure not overwrite each other?

    Answer The second example moves value outside both procedures. That single location is then shared, so an update through either procedure becomes the starting point for the other.

    How can two force calls cause only one evaluation?

    Answer The second force returns the stored value without running the body again. The final calls value remains 1, making memoization visible in the result.

    How can a finite run use a sequence with no final element?

    Answer stream-ref forces exactly as many tails as it needs to reach the requested index. Asking for index 9 constructs a finite prefix and returns 10, so this particular run terminates.

    Why does constant local insertion need a rear pointer as well as a front pointer?

    Answer Deletion does not rewrite the links. It advances front to its current cdr. After inserting a, b, and c and deleting once, front names the b pair while rear still names the c pair.

    How can a stateful generator change every call yet repeat exactly?

    Answer The recurrence is deterministic. Two separately constructed generators begin with private locations, but equal starting seeds make their value sequences equal. This small generator is useful for studying state and reproducibility, not for cryptography or security decisions.

    How can one relation react correctly no matter which two values arrive first?

    Answer forget-value! succeeds only when the retractor is the stored informant. When the user forgets total, the adder retracts right because it supplied that derived value, but it cannot retract the independently supplied left value. Supplying a new right value then derives a new total through the same relation.

    How can a simulation determine what happens next without following scheduling order?

    Answer propagate removes the earliest event, applies its numeric change to signal, and records the resulting state beside that event time. The agenda models logical simulation time rather than waiting for a wall clock, and these examples do not model simultaneous-event physics beyond their explicit insertion rule.

    What disappears when two updates read the same shared balance before either finishes?

    Answer The second schedule lets the deposit read and write before the withdrawal reads. The withdrawal therefore sees 110 and writes 90. These programs enumerate two already chosen finite schedules. They do not create threads or a serializer, and they do not establish the result of every possible interleaving.

    What must be protected so that an update cannot read state and then finish after another update has changed it?

    Answer make-serializer accepts a shared lock and returns a procedure wrapper. The wrapper acquires before calling the state-changing procedure and clears after receiving its result. In the shipped finite schedule, the serialized deposit finishes before the withdrawal reads balance, so both updates remain in the final value 90. The comparison program does not prove fairness, deadlock freedom, exception safety, or the outcome of every possible concurrent schedule.

    How can several local procedures refer to one another without leaking their names into the surrounding environment?

    Answer The second program treats definitions as quoted data and builds the scan-out shape used to reason about simultaneous local scope: create every name with an explicit unassigned marker first, then install each procedure with set!, then evaluate the remaining body. This run constructs the transformation as data; it does not execute the generated form or claim that every Scheme implementation must expose the same intermediate representation.

    Which links must change when a table updates one record or creates a new nested key path?

    Answer The two-key table stores a first-key record whose cdr is itself an association list. Inserting a new second key mutates that subtable, while a new first key links an entire subtable into the outer table. Updating arithmetic from 10 to 11 changes the existing innermost record rather than creating a duplicate. These examples use #f as the missing result, so storing #f would require a richer lookup protocol.