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.

playmmix in action

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.