checksmix 0.3.13

A simplified MMIX simulator - Donald Knuth's hypothetical computer for teaching and learning assembly language programming.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
use std::collections::{BTreeSet, HashMap, HashSet};

use tracing::{debug, instrument};

#[macro_use]
mod macros;
mod dispatch;
mod display;
mod exceptions;
mod float;
mod host;
mod memory;
mod registers;
mod stack;
mod trap;

pub use display::ValueFormat;
pub use host::{Host, StdHost};
use registers::{RA_D, RA_I, RA_O, RA_ROUND_SHIFT, RA_U, RA_V, RA_W, RA_X, RA_Z, SAVE_SPECIALS};
pub use registers::{RA_MAX, SpecialReg};
use stack::PopFrame;
pub(crate) use stack::STACK_SEGMENT_START;
use trap::FileHandle;
pub use trap::TrapCode;

/// The MMIX computer architecture.
///
/// MMIX has:
/// - 256 general-purpose registers ($0-$255), each holding 64 bits (an octabyte)
/// - 32 special-purpose registers (rA-rZ, rBB, rTT, rWW, rXX, rYY, rZZ)
/// - 2^64 bytes of virtual memory
///
/// Instructions are tetrybytes (4 bytes) with format: OP X Y Z
/// where OP is the opcode and X, Y, Z are operands.
///
/// # Thread safety
///
/// `MMix` owns a [`Host`] as `Box<dyn Host>` and is therefore none of `Send`,
/// `Sync`, `UnwindSafe`, or `RefUnwindSafe` — a change from 0.2.23, where it
/// was all four. This is deliberate: the intended embedders are
/// single-threaded and capture into `Rc<RefCell<_>>`, which a `Send` bound
/// would forbid. Move the program, not the machine — construct an `MMix` on
/// the thread that runs it, and wrap it in `std::panic::AssertUnwindSafe` to
/// put one through `catch_unwind`.
pub struct MMix {
    /// 256 general-purpose registers, each 64 bits
    general_regs: [u64; 256],

    /// 32 special-purpose registers, each 64 bits
    /// Indexed by SpecialReg enum values
    special_regs: [u64; 32],

    /// Virtual memory
    /// this should use paging/segmentation
    /// Key is the memory address, value is the byte
    /// Iteration order is unspecified; [`MMix::occupied`] sorts it.
    memory: HashMap<u64, u8>,

    /// Program counter (location of next instruction)
    pc: u64,

    /// Open TRAP handles, numbered 0-255 as the caller chooses (`Fopen`
    /// picks 3+; 0-2 are the standard streams, seeded at [`MMix::initialize`]
    /// and never reassigned).
    file_handles: HashMap<u8, FileHandle>,

    /// Process exit code: the `Halt` trap sets it from `$255`; every other
    /// halt sets it to 1.
    exit_code: u64,

    /// Where process-level effects (writes, the clock, diagnostics, trap
    /// events) go. `MMix::new()` installs `StdHost`; `MMix::with_host`
    /// installs anything else.
    host: Box<dyn Host>,

    /// The strings every `debug` directive collected, `K`-indexed. Installed
    /// by [`MMix::set_debug_strings`] at load time; [`TrapCode::Debug`]
    /// reads it and touches nothing else.
    debug_strings: Vec<Vec<u8>>,

    /// Whether [`MMix::write_byte`] records the address it touches. Survives
    /// [`MMix::reset`]; see [`MMix::set_journal`].
    journal_enabled: bool,

    /// Addresses [`MMix::write_byte`] has touched since the last
    /// [`MMix::take_journal`], while the journal is enabled.
    journal: HashSet<u64>,

    /// Addresses `write_image` loaded, zero byte or not. Only
    /// [`MMix::write_loaded_byte`] records into this; plain
    /// [`MMix::write_byte`] calls (including everything a running program
    /// does after load) never touch it. Backs [`MMix::loaded_extent`].
    loaded: BTreeSet<u64>,
}

impl Default for MMix {
    fn default() -> Self {
        Self::new()
    }
}

impl MMix {
    /// Create a new MMIX computer with all registers and memory initialized
    /// to zero, and process I/O routed through `StdHost` — today's behavior,
    /// unchanged.
    pub fn new() -> Self {
        Self::with_host(StdHost)
    }

    /// Create a new MMIX computer with all registers and memory initialized
    /// to zero, routing every process-level write, the clock, diagnostics,
    /// and trap events through `host` instead of the process.
    ///
    /// The resulting `MMix` is none of `Send`, `Sync`, `UnwindSafe`, or
    /// `RefUnwindSafe` — see the [`MMix`] docs.
    pub fn with_host<H: Host + 'static>(host: H) -> Self {
        Self::blank(Box::new(host))
    }

    /// A machine in its starting state, owning `host`. The single place every
    /// field is named, so construction and [`MMix::reset`] cannot disagree
    /// about what "fresh" means — adding a field to `MMix` fails to compile
    /// here rather than silently surviving a reset.
    fn blank(host: Box<dyn Host>) -> Self {
        let mut mmix = Self {
            general_regs: [0; 256],
            special_regs: [0; 32],
            memory: HashMap::new(),
            pc: 0,
            file_handles: HashMap::new(),
            exit_code: 0,
            host,
            debug_strings: Vec::new(),
            journal_enabled: false,
            journal: HashSet::new(),
            loaded: BTreeSet::new(),
        };
        mmix.initialize();
        mmix
    }

    /// Return the machine to its freshly-constructed state — every register,
    /// all of memory, the program counter, the call-frame stack, open file
    /// handles, and the exit code — while keeping the installed [`Host`].
    ///
    /// A caller that injected a host to capture output needs this: dropping
    /// the machine and building another would take the host with it, and only
    /// the machine's state is stale between runs. The host's own buffers are
    /// untouched, so a host that accumulates sees successive runs appended;
    /// clear them through your own handle if that is not what you want.
    ///
    /// The journal's enabled flag (see [`MMix::set_journal`]) survives; the
    /// accumulated buffer does not, since a fresh machine has written
    /// nothing yet.
    pub fn reset(&mut self) {
        let host = std::mem::replace(&mut self.host, Box::new(StdHost));
        let journal_enabled = self.journal_enabled;
        *self = Self::blank(host);
        self.journal_enabled = journal_enabled;
    }

    /// The installed [`Host`], for a caller that needs to reach it after
    /// construction rather than keeping a shared handle.
    ///
    /// [`Host`] requires [`Any`], so an embedder can recover its concrete
    /// host type:
    ///
    /// ```
    /// # use checksmix::{Host, MMix};
    /// # use std::any::Any;
    /// # struct Capture(Vec<u8>);
    /// # impl Host for Capture {
    /// #     fn write(&mut self, _fd: u8, b: &[u8]) -> std::io::Result<()> {
    /// #         self.0.extend_from_slice(b); Ok(())
    /// #     }
    /// #     fn now_micros(&mut self) -> u64 { 0 }
    /// #     fn diagnostic(&mut self, _m: &str) {}
    /// # }
    /// let mut mmix = MMix::with_host(Capture(Vec::new()));
    /// let host: &mut dyn Any = mmix.host_mut();
    /// let capture = host.downcast_mut::<Capture>().expect("our own host type");
    /// capture.0.clear();
    /// ```
    pub fn host_mut(&mut self) -> &mut dyn Host {
        &mut *self.host
    }

    /// Consume the machine and return its [`Host`], for a caller that wants
    /// what the host captured once the program is done.
    pub fn into_host(self) -> Box<dyn Host> {
        self.host
    }

    /// The special-register values a machine starts life with.
    fn initialize(&mut self) {
        // Initialize rN (serial number register) to a default value
        // The MMIX specification says this should be a unique machine serial number
        self.set_special(SpecialReg::RN, 2009);
        // Initialize rG (global threshold register) - registers $rG..$255 are global
        // With no GREG declarations, rG=32
        self.set_special(SpecialReg::RG, 32);
        // Initialize rL (local threshold register) - number of local registers in use
        // Start with 0, will be updated by PUSHJ/POP
        self.set_special(SpecialReg::RL, 0);
        // rO and rS both start at the register stack's base, segment 6 per
        // MMIX convention. PUSHJ/POP store eagerly, so the two stay equal
        // and move together for the life of the machine.
        self.set_special(SpecialReg::RO, STACK_SEGMENT_START);
        self.set_special(SpecialReg::RS, STACK_SEGMENT_START);

        // rK, rT, rTT and rV start at the values TAOCP Vol. 1 Fascicle 1
        // (p. 90) gives the machine before any program runs.
        self.set_special(SpecialReg::RK, 0xFFFFFFFFFFFFFFFF);
        self.set_special(SpecialReg::RT, 0x8000000500000000);
        self.set_special(SpecialReg::RTT, 0x8000000600000000);
        self.set_special(SpecialReg::RV, 0x369C200400000000);

        // StdIn/StdOut/StdErr are open at start, TextRead/TextWrite/TextWrite
        // per the reference. None carries a `File`: fd 0's reads and fd 1/2's
        // writes route through the installed `Host`.
        self.file_handles.insert(
            0,
            FileHandle {
                file: None,
                read: true,
                write: false,
                seek: false,
                read_write: false,
            },
        );
        for fd in [1u8, 2] {
            self.file_handles.insert(
                fd,
                FileHandle {
                    file: None,
                    read: false,
                    write: true,
                    seek: false,
                    read_write: false,
                },
            );
        }
    }

    /// Install the string table every `debug` directive's `TRAP 0,Debug,K`
    /// reads from, `K`-indexed. The loader (`write_image`, `run_mmo`) calls
    /// this once, before the program runs.
    pub fn set_debug_strings(&mut self, strings: Vec<Vec<u8>>) {
        self.debug_strings = strings;
    }

    /// Get the value of a general-purpose register.
    pub fn get_register(&self, reg: u8) -> u64 {
        self.general_regs[reg as usize]
    }

    /// Set the value of a general-purpose register.
    ///
    /// Writing to a local register $i with i >= rL grows rL to i+1 and zeroes
    /// every register the growth exposes, so a register between the old and
    /// new rL never reads a stale value.
    pub fn set_register(&mut self, reg: u8, value: u64) {
        self.claim_local(reg);
        self.general_regs[reg as usize] = value;
    }

    /// Raise rL to cover `reg` when it names a marginal local register,
    /// zeroing every register from the old rL through `reg`. A no-op when
    /// `reg` is already local or is a global register (`reg >= rG`).
    fn claim_local(&mut self, reg: u8) {
        // UNSAVE restores rL and rG straight from guest memory, so either can
        // name a register the file does not have. Compare at full width: a
        // truncated rL would read as a low register and drop live locals.
        let rg = self.special_regs[SpecialReg::RG as usize];
        if (reg as u64) >= rg {
            return;
        }
        let rl = self.special_regs[SpecialReg::RL as usize];
        if (reg as u64) < rl {
            return;
        }
        let rl = rl as u8;
        for marginal in rl..=reg {
            self.general_regs[marginal as usize] = 0;
        }
        self.special_regs[SpecialReg::RL as usize] = (reg as u64) + 1;
    }

    /// True when `op_byte`'s X field names a general-register destination:
    /// the floating-point and integer arithmetic and compare instructions,
    /// the conditional- and zero-set instructions, the loads, GO, PUSHGO,
    /// the bitwise and SETL-family instructions, PUSHJ, GETA, SAVE and GET.
    /// Branches read a register as a source (not a destination) and are excluded.
    /// Stores, PUT, POP, UNSAVE and the opcodes with no register operand are not.
    fn writes_general_register_x(op_byte: u8) -> bool {
        matches!(
            op_byte,
            0x01..=0x3F | 0x60..=0x99 | 0x9E..=0x9F | 0xBE..=0xEF | 0xF2..=0xF5 | 0xFA | 0xFE
        )
    }

    /// Get the value of a special-purpose register.
    pub fn get_special(&self, reg: SpecialReg) -> u64 {
        self.special_regs[reg as usize]
    }

    /// Set the value of a special-purpose register.
    pub fn set_special(&mut self, reg: SpecialReg, value: u64) {
        self.special_regs[reg as usize] = value;
    }

    /// Emit a diagnostic and report the halt `execute_instruction` should
    /// propagate: no register or memory change and no PC advance, on the
    /// caller's promise that it made none before calling this. Sets the
    /// exit code to 1, as every halt but the `Halt` trap does. Every
    /// `PUT`/`PUTI` rejection, every must-be-zero and `SYNC`-range
    /// violation, and `SAVE`/`UNSAVE`'s validation failures route through
    /// this one diagnose-then-refuse path.
    fn reject(&mut self, message: &str) -> bool {
        self.host.diagnostic(message);
        self.exit_code = 1;
        false
    }

    /// Get the current program counter.
    pub fn get_pc(&self) -> u64 {
        self.pc
    }

    /// Set the program counter.
    pub fn set_pc(&mut self, pc: u64) {
        self.pc = pc;
    }

    /// Advance the program counter by 4 bytes (one instruction).
    pub fn advance_pc(&mut self) {
        self.pc = self.pc.wrapping_add(4);
    }

    /// Get the process exit code: `$255`, unreduced, after a `Halt` trap
    /// — `process::exit` takes it mod 256 — or 1 after any other halt.
    pub fn get_exit_code(&self) -> u64 {
        self.exit_code
    }

    /// Execute instructions starting from the current PC until a halt condition.
    /// Returns the number of instructions executed.
    #[instrument(skip(self))]
    pub fn run(&mut self) -> usize {
        self.run_bounded(usize::MAX).0
    }

    /// Execute instructions starting from the current PC until the machine
    /// halts or `budget` instructions have run, whichever comes first.
    /// Returns the instruction count and which condition stopped it.
    ///
    /// `MMix` has no breakpoint concept, so the result is never
    /// [`Stop::Breakpoint`] — that variant exists for [`crate::Debugger`]'s
    /// use.
    #[instrument(skip(self))]
    pub fn run_bounded(&mut self, budget: usize) -> (usize, Stop) {
        debug!("Starting MMIX execution");
        let mut count = 0;
        let stop = loop {
            if count >= budget {
                break Stop::BudgetExhausted;
            }
            if !self.execute_instruction() {
                break Stop::Halted;
            }
            count += 1;
        };
        match stop {
            Stop::Halted => self.host.diagnostic(&format!(
                "Execution stopped at PC={:#018x} after {} instructions",
                self.pc, count
            )),
            Stop::BudgetExhausted => self.host.diagnostic(&format!(
                "Execution paused at PC={:#018x} after {} instructions (budget exhausted)",
                self.pc, count
            )),
            Stop::Breakpoint(_) => unreachable!("run_bounded never returns Stop::Breakpoint"),
        }
        debug!(instruction_count = count, "Execution completed");
        (count, stop)
    }
}

/// Why [`MMix::run_bounded`] or a [`crate::Debugger`] step loop stopped.
///
/// More variants may be added in future releases, so downstream matches
/// must carry a wildcard arm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Stop {
    /// The machine halted: a `Halt` trap, an unhandled TRIP, an unhandled
    /// register trap, or a diagnostic rejection (illegal-instruction,
    /// privileged-operation, or an unloaded vector) — `execute_instruction`
    /// returning `false` covers every case with no finer distinction.
    /// [`MMix::get_exit_code`] distinguishes only the `Halt` trap from
    /// everything else.
    Halted,
    /// The instruction budget ran out before the machine halted. The
    /// machine is unchanged; resuming from here is calling the same
    /// bounded-run method again.
    BudgetExhausted,
    /// A breakpoint address was reached. `MMix` itself has no breakpoint
    /// concept, so [`MMix::run_bounded`] never produces this — it exists for
    /// [`crate::Debugger`], which owns the breakpoint set.
    Breakpoint(u64),
}

#[cfg(test)]
mod tests;