PlayMMIX — MMIX in the Browser
Posted on Sat 05 September 2026 in tech
An MMIX playground, playmmix, puts an MMIX machine in your browser with no install, no local setup, no build step.
MMIX is Knuth's 64-bit RISC architecture. I built checksmix to assemble and run programs for it. playmmix wraps that same engine with an editor and a live debugger, in Rust, Yew and WebAssembly.
Editing the source re-assembles and reloads the machine at its entry point, the same
way mmixdb does — but nothing executes until you click Run.

Driving the MMIX Machine
Five controls drive execution:
- Run executes until the program halts or hits a breakpoint.
- Step executes one source-level step, following calls into the callee.
- Step Over runs a call until it returns, unless a breakpoint or halt stops it first.
- Stop interrupts execution where it is.
- Reset reloads the current source, clearing output and change highlights.
Click a line number to set a breakpoint on an instruction. The machine pane highlights values that changed since the previous pause. Step once and see what moved; run to a breakpoint and see what changed along the way.
Testing a Small Prime
This approach is to try dividing N by increasing candidates D, starting at 2. If D*D exceeds N before anything divides evenly, N is prime. If D divides N, N is composite.
A prime is divisible only by 1 and itself, so no even number above 2 qualifies. Every even number is divisible by two. Above 2, N is odd, and an odd number has no even factors. Even numbers are never possible factors, so the recursive step jumps straight from 2 to the next odd candidate — 3, 5, 7, 9, … — instead of spending a call on every even D in between. There's no memoization to reach for here: each D is asked once, with no repeated subproblem to cache. The saving comes from skipping pointless divisors, not from caching answers.
This example uses recursion to keep the code simple and to exercise Step and Step Over: each candidate gets its own call and its own register window, so the two controls have visibly different jobs.
Paste this into playmmix:
LOC #100
Main SETL $1,97 % N
SETL $2,2 % first divisor
PUSHJ $0,IsPrime
TRAP 0,Halt,0 % result in $0: 1 prime, 0 composite
IsPrime MULU $3,$1,$1 % D * D
CMPU $3,$3,$0
BP $3,Prime
DIVU $3,$0,$1
GET $3,rR % N mod D
BZ $3,Composite
GET $2,rJ % preserve our return address
SET $4,$0 % next call's N
AND $3,$1,1 % 0 only when D is 2 (even)
ADDU $3,$3,1 % step: 1 after 2, 2 after any odd D
ADDU $5,$1,$3 % next call's D, skipping even divisors
PUSHJ $3,IsPrime
SET $0,$3 % pass the result back
PUT rJ,$2
POP 1,0
Prime SETL $0,1
POP 1,0
Composite SETL $0,0
POP 1,0
This example is for small integers starting at 2. SETL loads a 16-bit
constant, which gives us plenty of numbers to try without making this a
general-purpose primality library.
Click Run. The program halts with $0 = 1: the search visits 2, 3, 5, 7,
9, 11 and stops once 11² passes 97 — six calls, not the nine a divisor-by-one search
would need. Change 97 to 91 and run again: $0 = 0, found at 7 in four calls
instead of six. The answer lives in the register; the halt message's exit code
is a separate value, left at zero in both cases.
Following the call
Put 97 back and click Reset. Two clicks on Step load the arguments into
$1 and $2. The next instruction is PUSHJ $0,IsPrime.
Click Step again. Now we're at IsPrime, call depth is 1, and the arguments
are in $0 and $1: 97 and 2. The register numbers changed because the call
moved the register window. The caller stages arguments just above the register
named by PUSHJ; the callee sees them starting at $0.
Set a breakpoint on PUSHJ $3,IsPrime, the recursive call, and click Run.
We're about to try divisor 3. $4 holds 97 and $5 holds 3, ready to become
the next frame's $0 and $1.
Use Step here and you enter that frame: call depth 2, divisor 3. Keep the breakpoint; Run stops before each deeper call. The divisor climbs — by 1 the first time, by 2 after that — while the number stays put.
Now Reset, then Run to the same breakpoint. Click its line number to
remove it and choose Step Over. All the deeper calls execute, then execution
pauses at SET $0,$3. We're back at call depth 1, with $3 = 1: the recursive
call found no factor. Run lets that result return to Main and halts.
Removing the breakpoint matters. Step Over still respects breakpoints inside the call; leaving this one set would stop us in the next frame.
Getting home
PUSHJ writes the return address to the special register rJ. A recursive call
will overwrite it, so GET $2,rJ saves it in a local register before we call
again. PUSHJ $3,IsPrime preserves the registers below $3, including that
saved address.
POP 1,0 returns one value, placing the callee's $0 in the caller's chosen
register: $3 for the recursive call, $0 for the call from Main. We copy
the result into our own $0, restore rJ, and return it another level.
The MMIX instruction reference
has the full register-stack rules.
For 97, the deepest call reaches divisor 11: 121 is greater than 97, so it
returns 1. For 91, divisor 7 leaves a zero remainder in rR, so it returns 0.
The same return path carries either answer home.
Under the hood
checksmix is the assembler and the emulator with registers,
memory and TRAP handling. Playmmix uses its public MMixAssembler and MMix
APIs. The editor, execution controls and machine display live in playmmix.
Re-assembly pauses for half a second after typing stops, so an unfinished line doesn't flash an error on every keystroke. Run and Step flush any pending edit before executing. A successful assembly starts a fresh machine.
Step Over doesn't skip the call. It single-steps through the callee automatically, checking the call depth after every instruction until execution returns to where it started. That's also why a breakpoint set inside the callee still fires even under Step Over: the same per-instruction check Run relies on applies here too.
Long runs execute in bounded chunks, yielding to the browser between them. That keeps Stop available, including while Step Over is working through a call. The arithmetic runs locally in WebAssembly.
For .mmo object files or a shell, use
checksmix, also available on
crates.io. Its command-line debugger,
mmixdb, has its own worked example.
Open playmmix, paste the program, and try 49. Watch which branch it takes when the divisor's square equals the number. The source is on GitHub.