Checksmix - Blazing Fast MMIX
Posted on Fri 26 December 2025 in tech
Blazing Fast MMIX Emulator
I like learning computer architectures by running code. MIX is good fun, but it’s dated and awkward for writing anything beyond small examples. MMIX, on the other hand, is a complete computer architecture—much closer to machines that were genuinely pleasant to program, like MIPS or DEC Alpha.
For a while I was deeply obsessed with Alpha. Someone even gave me a Decstation 5000 that was headed to the dumpster to mess around with. I wanted to convert it from VMS to Unix but the tapes were prohibitively expensive. Digital didn't seem to understand the idea of scavenged hardware and wouldn't take pity on a poor college kid. I eventually switched to something designed in California—which, to be fair, is also expensive, but comes with better price for performance characteristics. But that doesn't mean I still can't write code in a model RISC computer.
Out of that latent interest came checksmix: a Rust-based MMIX assembler and emulator that lets you assemble or execute .mms source modules (or existing .mmo objects). Legacy MIX is still partially supported, though it hasn’t been the focus in my last round of changes.
Removing Friction from MMIX Experiments
The goal was simple: make MMIX feel accessible.
That meant:
- An assembler (
mmixasm) that emits.mmoin a simple and obvious way. - An emulator (
checksmix) that can take raw.mmsand just run it via the assembler in memory. - Clear register and memory dumps before and after execution so you can see exactly what changed.
- Enough TRAP support (notably
HaltandFputs) to print output and exit while experimenting with control flow and data structures.
Assemble or Run Directly
Run MMIX assembly without producing an object file:
checksmix examples/hello_world.mms
If you prefer to keep artifacts around, assemble first and then emulate:
mmixasm examples/linked_list.mms list.mmo
checksmix list.mmo
For debugging, add:
RUST_LOG=checksmix=debug
This enables instruction decoding traces and TRAP handling logs.
A Minimal Program (Hello, MMIX)
LOC Data_Segment
GREG @
Text BYTE "Hello world!",10,0
LOC #100
Main LDA $0,Text
TRAP 0,Fputs,StdOut
TRAP 0,Halt,0
Run it with checksmix and you’ll see register state before and after execution, along with the printed string via TRAP 0,Fputs,StdOut.
Under the Hood
- 256 general-purpose registers, 32 special registers, sparse 64-bit address space.
.mmsparsing, encoding, and in-memory loading;.mmodecoding with entry-point PC setup.- TRAP handling for
HaltandFputs, so programs can print without extra tooling. - Legacy MIX
.mix/.mixalparsing if you want to compare architectures side by side.
Technical Discussion: From Grammar to Run Loop
The grammar in mmixal.pest is intentionally literal. Tokens, expressions, directives, and opcodes map closely to Knuth’s notation—there is no “friendly” abstraction layer hiding the architecture. Pest gives byte spans, but we store source line and column on every error to keep diagnostics in the familiar file:line:col: message format. The tradeoff is a slightly heavier parse path, but much better ergonomics while iterating.
mmixal.rs implements a two-pass assembler over the pest pairs. Labels are resolved, expressions folded, and instructions lowered into MMixInstruction enums. Encoding happens exactly once. In checksmix, the encoded bytes are dropped straight into memory; in mmixasm, the same stream is wrapped with the MMO postamble and entry PC. Keeping this representation unified avoids two subtly different assemblers drifting apart over time.
MmoDecoder performs the inverse operation: it reads MMO sections and sparsely maps them into memory, returning the entry PC. Sparse loading via offsets keeps relocation handling honest without materializing gigabytes of zeroes. Malformed section headers are rejected early so an MMO can’t scribble over arbitrary addresses.
The emulator core in mmix.rs is table-driven. It fetches 4-byte words, splits opcode and operands, and dispatches through small macros covering arithmetic, logic, branches, and floating-point instructions. General-purpose and special registers live in fixed arrays for speed; memory is a hash map, so uninitialized addresses read as zero and we avoid allocating a 2⁶⁴-byte address space. The cost is extra lookups, but the REPL-style loop stays responsive without complex memory machinery.
TRAP support is intentionally minimal. TRAP 0,Halt,0 exits; TRAP 0,Fputs,StdOut walks a null-terminated string from $0 and writes it to stdout or stderr. Unknown traps simply advance the PC and continue. For exploratory work and porting snippets from richer simulators, this is usually more helpful than halting or panicking.
Debugging relies on logs rather than a bespoke UI. With RUST_LOG=checksmix=debug, you get decode and execute traces, PC movement, and TRAP calls. The binaries also dump registers before and after execution so you can diff machine state across runs. It’s a pragmatic compromise: enough visibility to reason about correctness without freezing the design around a particular debugger interface.
What You Get
- Fast feedback: Edit → run → inspect registers and memory in seconds.
- Confidence: Deterministic load and execution with debug logging when needed.
- Lower friction: More time spent thinking about your code
Project Links
- Repository: https://github.com/jac18281828/checksmix
- Crate: https://crates.io/crates/checksmix
Run the machine, watch the registers move, and make MMIX feel connected.
Simple Example
% ----------------------------------------------------
% Fibonacci Sequence
% ----------------------------------------------------
Zero IS $255
LOC #100
% Entry point
Main SETI $1,20 % compute fib(20)
PUSHJ $0,Fibonacci % call Fibonacci
JMP Done
% ----------------------------------------------------
% Registers used:
% $0 = result
% $1 = n (input)
% $2 = fib(n-2)
% $3 = fib(n-1)
% $4 = counter
% $5 = temp
% ----------------------------------------------------
% calculate fib($1) and return result in $0
Fibonacci
SETI $2,2
CMP $5,$1,$2
BN $5,TwoOrLess
SETI $2,0
SETI $3,1
SETI $4,2
AddLoop
CMP $5,$4,$1
BP $5,FibEnd
ADDU $5,$2,$3
SET $2,$3
SET $3,$5
ADDUI $4,$4,1
JMP AddLoop
FibEnd
SET $0,$3
POP 0,0 % return to caller (rJ)
TwoOrLess
SET $0,$1
POP 0,0 % return to caller (rJ)
Done
TRAP 0,Halt,0