An assembler, emulator and source-level debugger for Knuth's MMIX, the 64-bit RISC machine of The Art of Computer Programming. Write MMIXAL, run it, read machine state.
cargo install checksmixThis installs checksmix, mmixasm and mmixdb. It needs a stable Rust
toolchain (rustup.rs).
To build from source:
git clone https://github.com/jac18281828/checksmix.git
cd checksmix
cargo build --release # binaries land in target/release/checksmixruns.mmssource or a.mmoobject file.checksmix checkassembles without running;checksmix buildwrites a.mmo.mmixasmassembles.mmsto a.mmofile beside the source.mmixdbsteps through.mmssource with breakpoints, register and memory inspection, and Emacs GUD support.
checksmix has 256 general-purpose registers, 32 special registers, a sparse 64-bit
address space and the MMIXAL reference's TRAP file I/O, including StdIn:
checksmix run feeds a guest from the shell's own stdin (see
MMIX.md's "TRAP interface" section).
Assemble once and run the object file:
checksmix build examples/prime.mms -o prime.mmo
checksmix prime.mmoSet RUST_LOG=checksmix=debug to trace instruction decoding and TRAP handling.
leapyear.mms: the perpetual leap year below.hello_halt.mms: Hello, Halt: the smallest program that runs.exit_code.mms: return a value to the shell.hello_world.mms: print a string to standard output.subroutine.mms: call a subroutine and return its result.fibonacci.mms: compute fib(20) in a loop and print the result.big_fib.mms: compute fib(100) in multi-precision arithmetic.prime.mms: test a number for primality, print the verdict and exit 0 if prime, 1 if not.linked_list.mms: walk a linked list and sum its nodes.time.mms: read the host clock and print the Unix time.all_instructions_test.mms: run every mnemonic as a regression suite.
The Gregorian rule, which holds for any year: 4 divides it, unless 100 does, unless 400 does too.
% leapyear.mms -- the first leap year after Year, by the Gregorian rule.
Year IS 2026
LOC Data_Segment
GREG @
Digits BYTE 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
End BYTE 0
After BYTE "The first leap year after ",0
Is BYTE " is ",0
Newline BYTE 10,0
LOC #100
Main SET $1,Year
ADDU $2,$1,1 % the candidate year
SET $6,400
SET $7,100
1H DIVU $3,$2,$6
GET $3,rR
BZ $3,Found % every 400th year is leap
DIVU $3,$2,$7
GET $3,rR
BZ $3,2F % any other century is not
AND $3,$2,3
BZ $3,Found % otherwise every 4th year is
2H ADDU $2,$2,1
JMP 1B
Found LDA $255,After
TRAP 0,Fputs,StdOut
SET $5,$1
PUSHJ $4,PrintNum
LDA $255,Is
TRAP 0,Fputs,StdOut
SET $5,$2
PUSHJ $4,PrintNum
LDA $255,Newline
TRAP 0,Fputs,StdOut
SET $255,0
TRAP 0,Halt,0
% PrintNum: write $0 in decimal, filling Digits from the right.
PrintNum LDA $1,End
SET $2,10
1H DIVU $0,$0,$2
GET $3,rR
ADDU $3,$3,'0'
SUBU $1,$1,1
STBU $3,$1,0
PBNZ $0,1B
SET $255,$1
TRAP 0,Fputs,StdOut
POP 0,0
$ checksmix leapyear.mms
=== MMIX Assembler ===
=== Parsing assembly from: leapyear.mms ===
Assembly parsed successfully
=== Initial Machine State ===
MMIX Computer State:
PC = 0x0000000000000100
General Registers:
$254 = 0x2000000000000000 (2305843009213693952)
$255 = 0x0000000000000100 (256)
Special Registers:
rN = 0x00000000000007d9 (2009)
rO = 0x6000000000000000 (6917529027641081856)
rS = 0x6000000000000000 (6917529027641081856)
rT = 0x8000000500000000 (-9223372015379939328)
rTT = 0x8000000600000000 (-9223372011084972032)
rK = 0xffffffffffffffff (-1)
rV = 0x369c200400000000 (3935055375966928896)
rG = 0x00000000000000fe (254)
Memory: 145 bytes used
=== Executing Program ===
The first leap year after 2026 is 2028
HALT trap at PC=0x0000000000000164, exit code=0
Execution stopped at PC=0x0000000000000168 after 91 instructions
Executed 91 instructions
=== Final Machine State ===
MMIX Computer State:
PC = 0x0000000000000168
General Registers:
$1 = 0x00000000000007ea (2026)
$2 = 0x00000000000007ec (2028)
$254 = 0x2000000000000000 (2305843009213693952)
Special Registers:
rJ = 0x0000000000000158 (344)
rR = 0x0000000000000002 (2)
rN = 0x00000000000007d9 (2009)
rO = 0x6000000000000000 (6917529027641081856)
rS = 0x6000000000000000 (6917529027641081856)
rT = 0x8000000500000000 (-9223372015379939328)
rTT = 0x8000000600000000 (-9223372011084972032)
rK = 0xffffffffffffffff (-1)
rV = 0x369c200400000000 (3935055375966928896)
rG = 0x00000000000000fe (254)
rL = 0x0000000000000004 (4)
Memory: 154 bytes used
Execution completed.With Year IS 2099, the century rule skips 2100: 2104.
More MMIX, less installation: playmmix, the
browser playground built on checksmix.
Step through .mms source, set breakpoints and inspect registers and memory, with
the current source line shown as you go.
mmixdb examples/fibonacci.mms
mmixdb --fullname examples/fibonacci.mms # Emacs GUD marker mode
mmixdb --stdin FILE program.mms # feed a program's StdIn from FILEmmixdb debugs .mms source; a .mmo carries no source map.
--fullname is auto-enabled when the INSIDE_EMACS environment variable is
set. Without --stdin, a guest StdIn read fails: mmixdb's own stdin
carries debugger commands, never the guest's input. With it, FILE is read
whole at startup and replayed from the start on every run.
| Command | Forms | Semantics |
|---|---|---|
| step (into) | s, step |
Execute one source line, following into calls/branches. |
| next (over) | n, next |
Execute one source line, stepping over any call it makes. |
| stepi | si, stepi |
Execute exactly one instruction, following into calls/branches. |
| continue | c, continue |
Resume, single-stepping until a breakpoint or halt. |
| run/reset | r, run |
Reset to the freshly-loaded image, then run on; a breakpoint on the entry point fires. |
| break | b <line>, b <label>, b <addr>, break … |
Set a breakpoint on the tetra holding a source line, a label, or a hex address (0x.../#...); it fires when execution reaches any address in that tetra. |
| delete | d, delete, d <line>, d <label>, d <addr> |
Delete one breakpoint, or every breakpoint given no argument. |
p <arg>, print <arg>, p/f <arg>, p/x <arg> |
Print a register ($N/N), special register (rJ, rA, ...), label address, IS/GREG symbol, or the memory octa at a hex address's aligned 8-byte base (0x.../#...). /f and /x, attached or detached (p /f <arg>), print the same octabyte as an IEEE double or in hex instead. |
|
| set | set <target> <value> |
Write a register ($N/N), special register — rL at most rG, rG 32-255 and at least rL, any other value rejected and unchanged — or the memory octa at a hex address's aligned 8-byte base. A symbol whose type is a register alias (from GREG or a register-valued IS, e.g. Sp) is settable the same way; a label or a symbol whose type is a constant (an IS bound to a non-register value) is not; neither names a storage location. value is decimal or 0x/#-prefixed hex. |
| state | bt, backtrace, info reg, info registers |
Print the full register dump. |
| breakpoints | info break, info breakpoints |
List every currently-set breakpoint by its tetra and the source line holding that tetra's first byte. |
| list | l, list |
Print source lines around the current PC. |
| help | h, help, ? |
Show this help. |
| quit | q, quit, exit |
Exit the debugger. |
Blank input repeats the last command; most debugging is stepping. Once the
program has exited, step, stepi, next and continue are refused; run
restarts it.
Emacs users: contrib/mmixdb.el provides M-x mmixdb under gud-mode. Put
contrib/ on load-path and require it:
(add-to-list 'load-path "/path/to/checksmix/contrib")
(require 'mmixdb)or autoload it instead:
(autoload 'mmixdb "mmixdb" "Run mmixdb under gud-mode." t)contrib/mmix-mode.el is a major mode for .mms source written to the dialect
checksmix assembles: % comments, a label field only on an unindented line,
the explicit immediate mnemonics and checksmix's extensions. It highlights and indents,
colours each use of a label or IS/GREG name the file defines, shows
the current line's instruction through eldoc, describes any instruction with
C-c C-d, and runs the file with C-c C-c (checksmix run). The instruction
reference is built in, so the file stands alone: copy it into a directory on
load-path and require it, with Emacs 29.1 or later:
(require 'mmix-mode)Its tests check the mode against the assembler's grammar, so run them from the repository root:
emacs --batch -L contrib -l contrib/mmix-mode-test.el -f ert-run-tests-batch-and-exitchecksmix is usable as a library, independent of the three binaries above. The
clap, rustyline and tracing-subscriber dependencies the CLIs need live behind
the cli feature, which is on by default. A library-only consumer, notably one
targeting wasm32-unknown-unknown where rustyline does not build, turns it off.
playmmix is built this way, with default-features = false,
and runs the emulator in the browser as wasm.
# default — library plus the CLI dependency tree
checksmix = "0.4"
# library only
checksmix = { version = "0.4", default-features = false }Either form gives you the library. The checksmix, mmixasm and mmixdb
executables come from cargo install checksmix, not from a [dependencies] entry.
By default an MMix reads and writes the process's own stdin, stdout and
stderr. Implement Host to intercept what the program reads and writes, the
clock behind the Time trap, diagnostics and every recognized TRAP:
use checksmix::{Host, MMix};
use std::cell::RefCell;
use std::rc::Rc;
struct Capture(Rc<RefCell<Vec<u8>>>);
impl Host for Capture {
fn write(&mut self, _fd: u8, bytes: &[u8]) -> std::io::Result<()> {
self.0.borrow_mut().extend_from_slice(bytes);
Ok(())
}
fn now_micros(&mut self) -> u64 { 0 }
fn diagnostic(&mut self, _msg: &str) {}
}
let out = Rc::new(RefCell::new(Vec::new()));
let mut mmix = MMix::with_host(Capture(out.clone()));Clone the buffer handle before moving the host in, or reach it later
through MMix::host_mut (a &mut dyn Host while you still hold the
machine) or MMix::into_host (the boxed Host once you are done with the
machine).
Debugger::load_with_host takes a host the same way, for programs you want to
step through rather than run straight out. Debugger::load installs StdHost,
so a debugged program's output goes to the process and never reaches you, and
its StdIn reads the process's own stdin.
An MMix holds its host as Box<dyn Host> and so is none of Send, Sync,
UnwindSafe or RefUnwindSafe; a Debugger holds an MMix and inherits that.
Construct one on the thread that runs it, and wrap it in
std::panic::AssertUnwindSafe to put it through catch_unwind.
MMIX is Knuth's "pretty clean" machine architecture, and he documented it himself:
- Knuth's MMIX page: the canonical home: design rationale and current news.
- The Art of Computer Programming, Volume 1 Fascicle 1, MMIX: A RISC Computer for the New Millennium (2005): the instruction set as Knuth teaches it, and the shortest path in.
checksmix follows the same instruction set, so a program written from any of these
runs here, StdIn included (see MMIX.md's "TRAP interface"
section).
- The MMIX Home Page: Martin Ruckert's collection at Munich University of Applied Sciences: documentation, sources, binaries, worked examples, and The MMIX Supplement.
- Instruction Reference: the Home Page's per-instruction reference.
- MMIXware: A RISC Computer for the Third Millennium: the full definition of MMIX, with an assembler and simulator (Springer LNCS 1750, 1999).
Bug reports and pull requests are welcome; see
CONTRIBUTING.md.
Donald Knuth's art and craft inspire my work. I once reported a "bug" in The Art of Computer Programming, but I never earned my hexadecimal dollar.
This project carries a little of his spirit forward: curiosity, precision and the belief that programming can be serious fun.
"e is as real as any other number."
— Donald E. Knuth