Expand description
§checksmix
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.
§Install
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/§Usage
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, except a StdIn
read, which always fails (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.
§Examples
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 and print the verdict.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.
§Perpetual leap year
The Gregorian rule, which holds for any year: 4 divides it, unless 100 does, unless 400 does too.
§Listing — leapyear.mms
% 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§Run
$ 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.
§mmixdb
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 modemmixdb debugs .mms source; a .mmo carries no source map.
--fullname is auto-enabled when the INSIDE_EMACS environment variable is
set.
| 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-exit§Using checksmix as a library
checksmix 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.
§Capturing what a program emits
By default an MMix writes to the process’s stdout and stderr. Implement
Host to intercept what the program 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.
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.
§Learning MMIX
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, except for a StdIn read, which always fails (see
MMIX.md’s “TRAP interface” section).
§Related projects
- 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).
§Contribute
Bug reports and pull requests are welcome; see
CONTRIBUTING.md.
§Tribute
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
Structs§
- Debugger
- The interactive debugger core: owns the loaded
MMix, theMMixAssembler(for the source map and symbol tables), breakpoints, and REPL state. - MMix
- The MMIX computer architecture.
- MMix
Assembler - MmoDecoder
- MMO file decoder
- MmoGenerator
- MMO file generator
- Source
Loc - The original (user-facing) source location of an assembled instruction: the file as given on the command line, and the 1-based line in that file’s ORIGINAL (un-preprocessed) text.
- StdHost
- The
HostbehindMMix::new(): the process’s own I/O — lockedstdout/stderrwrites,stdout().flush()on halt,SystemTimefor the clock, andeprintln!for diagnostics.
Enums§
- Command
- A parsed debugger command. One variant per command in the command table;
Repeatrepresents blank input, which re-runs the last executed command. - Print
Format - A
printoutput format, gdb’s/fand/xsuffixes. Non-exhaustive: a later format is an additive change here, not a breaking one. - Special
Reg - Special register identifiers for MMIX. These 32 special registers control various aspects of MMIX operation. Per TAOCP specification.
- Stop
- Why
MMix::run_boundedstopped. - Trap
Code - TRAP code identifiers for MMIX, numbered per the MMIXAL reference: every
call is
TRAP 0,Code,Z.Zmeans a different thing per code: ignored forHalt; the handle (0-255) for the file calls andFputc; the unit forTime(0 seconds, 1 milliseconds, 2 microseconds); and forDebug, the 0-based index, in program order, of adebug "text"directive, looked up in the tableset_debug_stringsinstalled.$255carries any further argument (an address, for a call that takes two).Fputc,TimeandDebugare checksmix’s own extensions, given codes (#80-#82) well above the reference’s range so an old binary’s codes 11-13 halt as an unsimulatedTRAPrather than running the wrong call. - Value
Format
Traits§
- Host
- Routes every process-level effect an
MMixproduces: writes to fd 1/2, the wall clock, and diagnostic messages (StdHostprints them to stderr).
Functions§
- entry_
point - The program’s entry point: the
Mainlabel if present, else the first code address below the text/data segment boundary. - parse_
command - Parse one line of debugger input into a
Command. - start_
program - Start a program at
entry: set the PC there, and$255to the same address, MMIXware’s start state for a running program.$255is always global, so this never raisesrL. - write_
image - Write every assembled instruction’s encoded bytes into
mmix’s memory, apply everyGREGinitializer to its allocated register, and raiserGto mark where the global register range actually starts.