all_lessons/SICP JavaScript/19 · Designing register machineslesson 19 / 22

Designing register machines

Through eighteen lessons every evaluator we built — metacircular, analyzing, lazy, nondeterministic, the logic engine — quietly borrowed three things from the host language: recursion, memory, and dispatch. When evaluate called itself to evaluate a subexpression, JavaScript's own call stack remembered where to resume. That was a loan, and the loan hid the true cost of control. This lesson stops borrowing. We describe computation as a register machine: a fixed set of registers, a set of primitive operations, tests and branches, labels, and — the part the host used to give us for free — an explicit stack we must push and pop by hand. The headline: recursion needs an explicit stack, because the stack you leaned on for eighteen lessons is now your own responsibility.

Book source
Maps to SICP §5.1 (Designing Register Machines) — the register-machine language, the data-path / controller split, the abstraction of subroutines via a return-address register, and using a stack to implement recursion (gcd vs. factorial). We follow that arc and its escalation, but the JavaScript instruction-list notation, the data-path diagrams, and every trace below are original.
Linear position
Prerequisite: Lesson 18 (the query/logic evaluator — the last evaluator that leaned on the host's recursion and memory) and Lesson 02 (linear iteration vs. linear recursion, and the deferred-operation chain a recursive process builds).
New capability: express any computation as a register machine — registers, operations, tests, branches, labels, and an explicit stack — and know exactly when a stack is required (recursion) versus not (iteration), plus how to call a subroutine with a return-address register.
The plan
Six moves. (1) The register-machine model: registers, operations, the controller. (2) The data-path / controller split — what to compute vs. when. (3) The instruction language as JavaScript data — an array of instruction objects. (4) Iterative gcd: a machine with no stack. (5) Recursive factorial: why the stack is now forced, drawn out as save/restore pairs. (6) Subroutines via a continue register. Then the pressure that forces lesson 20.

1 · The register-machine model

A register machine computes by repeatedly altering the contents of a fixed collection of named storage cells. The pieces are deliberately few:

registers
A fixed, finite set of named cells, each holding exactly one value (a number, or later a pointer). Examples: a, b, t, n, val, continue. There is no "new variable" — you reuse the same cells over and over.
operations
Primitive functions wired between registers — +, -, *, rem. An operation reads some registers, computes, and the result is stored into a register. Operations are the only place arithmetic happens.
tests & branches
A test applies a predicate (e.g. =, <) to registers and sets an internal flag; a branch reads that flag and jumps to a label or falls through. This is the only conditional control the machine has.
controller
A linear list of instructions with labels, executed top to bottom except where a goto or branch jumps. The controller is the program; the registers and operations are the hardware it drives.

That is the entire vocabulary. No function calls, no local variables, no implicit return path. Everything richer — recursion, subroutines, an evaluator — must be built from these atoms. This is the bottom of the abstraction ladder we have been descending: a model close enough to real hardware that nothing is free.

2 · The data-path / controller split

A register machine has two halves, and keeping them separate is the central design discipline. The data paths are the registers and the operation/test boxes wired between them — what can be computed and where values can flow. The controller is the sequence of instructions that decides when each operation fires and which branch is taken — the order of events in time. Data paths answer "what is possible"; the controller answers "what happens, and when."

       DATA PATHS (structure: what can flow where)
   +-----+        +---------+        +-----+
   |  a  |---+--->|  op: -  |------->|  t  |
   +-----+   |    +---------+        +-----+
   +-----+   +--->|  test:= |--(flag)
   |  b  |------->+---------+

       CONTROLLER (behavior: when each box fires)
   loop:  test (op =) (reg b) (const 0)
          branch (label done)
          assign t (op rem) (reg a) (reg b)
          assign a (reg b)
          assign b (reg t)
          goto (label loop)
   done:

The same data paths can serve many controllers, and the split is exactly the procedure/process distinction from lesson 02 made physical: the data paths are the static definition of what the machine is capable of; the controller is the process — the unfolding in time. Everything below is just controllers over small data paths.

3 · The controller as JavaScript data

We will write controllers as a plain JavaScript array of instruction objects. Each instruction is a tagged record; labels are bare strings that mark positions. This is not decoration — making the controller data is exactly what lets lesson 20 feed it to an assembler. The instruction set:

InstructionMeaning
{op:"assign", reg, src}Compute src (a constant, a register, or an operation on registers) and store it into register reg.
{op:"test", fn, args}Apply predicate fn to args; set the machine's internal flag to the boolean result.
{op:"branch", label}If the flag is true, continue at label; otherwise fall through to the next instruction.
{op:"goto", label}Unconditional jump to label (or to a label held in a register).
{op:"save", reg} / {op:"restore", reg}Push the current contents of reg onto the explicit stack / pop the top of the stack into reg.

Note save/restore and the stack they touch. That stack is the new burden. For the next machine we will not need it at all — and seeing exactly why is the whole point.

4 · Iterative gcd — a machine with no stack

Euclid's algorithm from lesson 02 is a linear iterative process: its entire state lives in two registers, and each step overwrites them. Nothing needs to be remembered for later, so the machine needs no stack.

// gcd(a, b): keep replacing (a, b) with (b, a mod b) until b == 0.
const gcdMachine = [
  "gcd",
    { op:"test",   fn:"=",   args:[{reg:"b"}, {const:0}] },
    { op:"branch", label:"done" },
    { op:"assign", reg:"t", src:{ fn:"rem", args:[{reg:"a"}, {reg:"b"}] } },
    { op:"assign", reg:"a", src:{ reg:"b" } },
    { op:"assign", reg:"b", src:{ reg:"t" } },
    { op:"goto",   label:"gcd" },
  "done"
];
Worked example — running gcd(206, 40)
step   instruction          a     b     t    flag
init                        206   40    -     -
1      test b=0                                false
2      branch (not taken)
3      t = rem(a,b)         206   40    6
4-5    a=b, b=t              40    6     6
       goto gcd
6      test b=0                                false
       ...                  6     4     4
       ...                  4     2     2
       ...                  2     0     0
       test b=0                                true
       branch -> done       (a = 2 is the answer)
At no step did any value have to be stashed and recovered. The state is exactly the three registers, all live at once. Iteration is loops over a fixed register set — and a fixed register set is a constant amount of memory. This is the register-machine echo of "iteration runs in constant space" from lesson 02.

5 · Recursive factorial — the stack becomes mandatory

Now the contrast that defines the lesson. Recursive factorial — n! = n * (n-1)! — is a linear recursive process. To compute (n-1)! the machine must, for the moment, forget n and reuse the same n register for the subproblem; but it must get n back afterward to perform the pending multiplication. There is no second register to keep n in, because the subproblem is the same machine and wants the same registers. The pending multiplications are precisely the deferred-operation chain from lesson 02 — and that chain has to live somewhere. It lives on the stack.

// n! using ONE register n and ONE result register val, plus the stack.
const factMachine = [
  "fact",
    { op:"test",   fn:"=", args:[{reg:"n"}, {const:1}] },
    { op:"branch", label:"base" },
    { op:"save",   reg:"n" },                         // remember n for the multiply
    { op:"assign", reg:"n", src:{ fn:"-", args:[{reg:"n"},{const:1}] } },
    { op:"save",   reg:"continue" },                  // remember where to return
    { op:"assign", reg:"continue", src:{ label:"after" } },
    { op:"goto",   label:"fact" },                    // recurse on n-1
  "after",
    { op:"restore", reg:"continue" },
    { op:"restore", reg:"n" },                        // get our n back
    { op:"assign",  reg:"val", src:{ fn:"*", args:[{reg:"n"},{reg:"val"}] } },
    { op:"goto",    label:{ reg:"continue" } },
  "base",
    { op:"assign", reg:"val", src:{ const:1 } },
    { op:"goto",   label:{ reg:"continue" } }
];
Worked example — the save/restore stack discipline of fact(3)
Each level saves its n on the way down and restores it on the way up. The stack mirrors the deferred-operation chain exactly: it grows to depth n, then drains.
descending (push)                       stack (top = right)
  fact n=3:  save n=3 ----------------> [3]
  fact n=2:  save n=2 ----------------> [3, 2]
  fact n=1:  base case, val = 1 ------> [3, 2]   (no save at base)

ascending (pop) — pending multiplies fire in reverse
  restore n=2; val = 2 * 1 = 2 -------> [3]
  restore n=3; val = 3 * 2 = 6 -------> []
  done: val = 6
The stack reaches depth 3 — proportional to n. That is the register-machine measurement of "recursion needs space proportional to depth," and it is the cost the host language hid from us for eighteen lessons. The stack is not an optimization; it is the only place a recursive process can keep its half-finished work.

6 · Subroutines via a continue register

Notice the machine never "called" anything in the host sense — it only gotos the fact label. So how does it know where to come back to? It writes the return label into a register named continue before jumping, and the called code ends with goto (reg continue). That is the entire mechanism of a subroutine call: a jump plus a register holding the return address. Because the same continue register is reused at every level, each level must save its own continue before overwriting it — which is why the stack carries return addresses as well as data. A real machine's call/return, which the host language packaged invisibly into one nice f(), decomposes here into: set continue, push it, jump, and on return pop it and jump back.

Common mistakes / failure modes

Adding a register per recursive level
You cannot give factorial a fresh n register for each depth — the register set is fixed. Depth-many values is exactly what the stack is for; reaching for "more registers" means you have not internalized that recursion needs unbounded storage.
Forgetting to save continue
If a subroutine that itself recurses overwrites continue without saving it, the outer caller's return address is lost and the machine returns to the wrong place. Save/restore must bracket every register the callee clobbers and the caller still needs.
Mismatched save/restore order
The stack is LIFO: restore in the reverse order you saved. Saving n then continue but restoring n first swaps their values. Treat each save/restore as a balanced bracket pair.
Computing in the controller
Trying to "just multiply here" in a branch confuses the two halves. All arithmetic flows through operation boxes in the data paths; the controller only sequences and tests. Mixing them is how on-paper machines stop matching real hardware.

Checkpoint exercise

Try it
(1) Write the controller for an iterative factorial that uses an accumulator register product and a counter n — and confirm it needs no save/restore. (2) For the recursive factMachine above, what maximum stack depth does fact(5) reach, and how many save instructions execute in total? (3) In one sentence, state the property of a process that decides whether its register machine needs a stack.
iter:  test (= n 0) -> branch ans
       assign product = (* n product)
       assign n = (- n 1)
       goto iter
ans:   (product holds n!)
Answers: (1) every value (product, n) is live in a register the whole time and nothing is deferred, so no stack is touched — iteration is constant space. (2) fact(5) recurses at n=5,4,3,2 (n=1 is the base, no save), so it saves n and continue at four levels — max stack depth 8 entries (4 × {n, continue}), and 8 save instructions execute. (3) A process needs a stack iff it is recursive — it defers operations whose operands must outlive the subproblem; a purely iterative process keeps its whole state in the fixed register set and needs none.

Where this points next

We can now describe any computation — iterative or recursive — as a register machine on paper, complete with an honest, hand-drawn account of its stack discipline and therefore its space cost. But a machine described on paper is just a claim. We asserted that fact(3) reaches stack depth 3 and runs a certain number of instructions; we have no way to check it, and no way to run a machine too large to trace by hand. A description that good should be executable, and a cost model that concrete should be measurable. Lesson 20 builds the bridge: a simulator — an assembler that turns these instruction arrays into runnable code, plus instrumentation (stack-depth and instruction counters) that turns lesson 19's pencil arithmetic into numbers the machine reports itself. The on-paper machine becomes an executable, measurable artifact.

Takeaway
A register machine computes with a fixed set of registers, primitive operations wired in the data paths, and a controller — a labelled instruction list of assigns, tests, branches, and gotos — that decides when each operation fires. The data-path/controller split is lesson 02's definition-vs-process distinction made physical. Iterative processes like gcd keep their entire state in the fixed register set and need no stack; recursive processes like factorial must defer operations whose operands outlive the subproblem, so they save those registers on an explicit stack on the way down and restore them on the way up — a stack that grows to the recursion depth and exactly mirrors the deferred-operation chain. Subroutines are just a goto plus a continue register holding the return label, itself saved when a callee clobbers it. The deep point: the call stack the host language gave us free for eighteen lessons is now our data structure — making the true cost of control visible for the first time.

Interview prompts