use super::*;
fn write_two_octa_params(mmix: &mut MMix, buffer_addr: u64, size: u64) {
let param_addr = 200_000u64;
mmix.write_octa(param_addr, buffer_addr);
mmix.write_octa(param_addr + 8, size);
mmix.set_register(255, param_addr);
}
struct FailsAfterFirstWrite {
writes: usize,
}
impl Host for FailsAfterFirstWrite {
fn write(&mut self, _fd: u8, _bytes: &[u8]) -> std::io::Result<()> {
self.writes += 1;
if self.writes == 1 {
Ok(())
} else {
Err(std::io::Error::other("FailsAfterFirstWrite: later write"))
}
}
fn now_micros(&mut self) -> u64 {
0
}
fn diagnostic(&mut self, _msg: &str) {}
}
struct FailsEveryWrite {
requests: std::rc::Rc<std::cell::RefCell<Vec<usize>>>,
}
impl Host for FailsEveryWrite {
fn write(&mut self, _fd: u8, bytes: &[u8]) -> std::io::Result<()> {
self.requests.borrow_mut().push(bytes.len());
Err(std::io::Error::other("FailsEveryWrite"))
}
fn now_micros(&mut self) -> u64 {
0
}
fn diagnostic(&mut self, _msg: &str) {}
}
fn patterned(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
#[test]
fn test_trap_fwrite_stdout_moves_a_size_past_one_chunk_whole() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let buffer_addr = 1_000_000u64;
let data = patterned(1_048_576 + 3);
for (i, &byte) in data.iter().enumerate() {
mmix.write_byte(buffer_addr + i as u64, byte);
}
write_two_octa_params(&mut mmix, buffer_addr, data.len() as u64);
mmix.write_tetra(0, 0x00000601); assert!(mmix.execute_instruction());
assert_eq!(handle.stdout(), data);
assert_eq!(mmix.get_register(255), 0);
}
#[test]
fn test_trap_fwrite_stdout_reports_short_write_when_a_later_chunk_fails() {
let mut mmix = MMix::with_host(FailsAfterFirstWrite { writes: 0 });
write_two_octa_params(&mut mmix, 10_000, 0x100003);
mmix.write_tetra(0, 0x00000601); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-3i64) as u64);
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_two_octa_params(&mut mmix, 10_000, 0x100000);
mmix.write_tetra(0, 0x00000601);
assert!(mmix.execute_instruction());
assert_eq!(handle.stdout().len(), 1_048_576);
assert_eq!(mmix.get_register(255), 0);
}
#[test]
fn test_trap_fwrite_stdin_rejects_and_does_not_negate_overflow() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_two_octa_params(&mut mmix, 10_000, 0x8000000000000000);
mmix.write_tetra(0, 0x00000600); assert!(mmix.execute_instruction());
assert!(handle.stdout().is_empty());
assert_eq!(mmix.get_register(255), 0x8000000000000000);
}
#[test]
fn test_trap_fwrite_stdout_size_above_i64_max_wraps_correctly() {
let mut mmix = MMix::with_host(FailsAfterFirstWrite { writes: 0 });
write_two_octa_params(&mut mmix, 10_000, 0x8000000000000000);
mmix.write_tetra(0, 0x00000601); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0x8000000000100000);
}
#[test]
fn test_trap_fwrite_stdout_size_u64_max_reports_one_chunk_written() {
let mut mmix = MMix::with_host(FailsAfterFirstWrite { writes: 0 });
write_two_octa_params(&mut mmix, 10_000, u64::MAX);
mmix.write_tetra(0, 0x00000601); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0x100001);
}
#[test]
fn test_trap_empty_request_makes_one_empty_host_write() {
let requests = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut mmix = MMix::with_host(FailsEveryWrite {
requests: requests.clone(),
});
mmix.set_register(255, 10_000); mmix.write_tetra(0, 0x00000701); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
assert_eq!(*requests.borrow(), vec![0]);
let requests = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut mmix = MMix::with_host(FailsEveryWrite {
requests: requests.clone(),
});
write_two_octa_params(&mut mmix, 10_000, 0);
mmix.write_tetra(0, 0x00000601); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0);
assert_eq!(*requests.borrow(), vec![0]);
let requests = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut mmix = MMix::with_host(FailsEveryWrite {
requests: requests.clone(),
});
mmix.set_register(255, 10_000);
mmix.write_tetra(0, 0x00000801); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
assert_eq!(*requests.borrow(), vec![0]);
}
struct NoReadHost;
impl Host for NoReadHost {
fn write(&mut self, _fd: u8, _bytes: &[u8]) -> std::io::Result<()> {
Ok(())
}
fn now_micros(&mut self) -> u64 {
0
}
fn diagnostic(&mut self, _msg: &str) {}
}
#[test]
fn test_trap_fread_handle0_reads_host_bytes() {
let (mut host, _handle) = CaptureHost::new();
host.queue_input(b"ABCDE");
let mut mmix = MMix::with_host(host);
let buffer_addr = 5_000u64;
write_two_octa_params(&mut mmix, buffer_addr, 5);
mmix.write_tetra(0, 0x00000300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0);
assert_eq!(
(0..5)
.map(|i| mmix.read_byte(buffer_addr + i))
.collect::<Vec<_>>(),
b"ABCDE"
);
let (mut host, _handle) = CaptureHost::new();
host.queue_input(b"XYZ");
let mut mmix = MMix::with_host(host);
write_two_octa_params(&mut mmix, buffer_addr, 5);
mmix.write_tetra(0, 0x00000300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (3u64).wrapping_sub(5));
assert_eq!(
(0..3)
.map(|i| mmix.read_byte(buffer_addr + i))
.collect::<Vec<_>>(),
b"XYZ"
);
}
#[test]
fn test_trap_fgets_handle0_stops_at_newline() {
let (mut host, _handle) = CaptureHost::new();
host.queue_input(b"hi\nignored");
let mut mmix = MMix::with_host(host);
let buffer_addr = 6_000u64;
for i in 0..16 {
mmix.write_byte(buffer_addr + i, 0xFF);
}
write_two_octa_params(&mut mmix, buffer_addr, 16);
mmix.write_tetra(0, 0x00000400); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 3);
assert_eq!(
(0..3)
.map(|i| mmix.read_byte(buffer_addr + i))
.collect::<Vec<_>>(),
b"hi\n"
);
assert_eq!(mmix.read_byte(buffer_addr + 3), 0);
}
#[test]
fn test_trap_fgetws_handle0_reads_whole_wydes_and_stops_at_000a() {
let (mut host, _handle) = CaptureHost::new();
host.queue_input(&[0x00, 0x41, 0x00, 0x0A, 0x00, 0x42]);
let mut mmix = MMix::with_host(host);
let buffer_addr = 7_000u64;
for i in 0..8 {
mmix.write_byte(buffer_addr + i, 0xFF);
}
write_two_octa_params(&mut mmix, buffer_addr, 8);
mmix.write_tetra(0, 0x00000500); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 2);
assert_eq!(
(0..4)
.map(|i| mmix.read_byte(buffer_addr + i))
.collect::<Vec<_>>(),
vec![0x00, 0x41, 0x00, 0x0A]
);
assert_eq!(mmix.read_byte(buffer_addr + 4), 0);
assert_eq!(mmix.read_byte(buffer_addr + 5), 0);
}
#[test]
fn test_trap_default_host_read_fails_on_handle0_but_fread_size_0_succeeds() {
let mut mmix = MMix::with_host(NoReadHost);
write_two_octa_params(&mut mmix, 8_000, 10);
mmix.write_tetra(0, 0x00000300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), u64::MAX.wrapping_sub(10));
let mut mmix = MMix::with_host(NoReadHost);
write_two_octa_params(&mut mmix, 8_000, 0);
mmix.write_tetra(0, 0x00000300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0);
let mut mmix = MMix::with_host(NoReadHost);
for i in 0..16 {
mmix.write_byte(8_000 + i, 0xFF);
}
write_two_octa_params(&mut mmix, 8_000, 16);
mmix.write_tetra(0, 0x00000400); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
assert_eq!(
mmix.read_byte(8_000),
0,
"the zero terminator is still stored"
);
let mut mmix = MMix::with_host(NoReadHost);
for i in 0..8 {
mmix.write_byte(8_000 + i, 0xFF);
}
write_two_octa_params(&mut mmix, 8_000, 8);
mmix.write_tetra(0, 0x00000500); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
assert_eq!(
mmix.read_byte(8_000),
0,
"the zero terminator is still stored"
);
assert_eq!(mmix.read_byte(8_001), 0);
}
#[test]
fn test_trap_halt() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00000000); let should_continue = mmix.execute_instruction();
assert!(!should_continue); assert_eq!(mmix.get_pc(), 4); }
#[test]
fn test_trap_x_nonzero_halts_unsimulated_and_exits_1() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(2, 10);
mmix.set_register(3, 20);
mmix.write_tetra(0, 0x00010203); assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"unsimulated TRAP 1,2,3 at PC=0x0000000000000000"
);
}
#[test]
fn test_trap_x_zero_unknown_code_halts_with_trap_page_registers() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(99, 0x1111);
mmix.set_register(7, 0x2222);
mmix.set_register(255, 0x3333);
mmix.set_special(SpecialReg::RJ, 0x4444);
mmix.write_tetra(0, 0x00006307);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(mmix.get_pc(), 0);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"unsimulated TRAP 0,99,7 at PC=0x0000000000000000"
);
assert_eq!(mmix.get_special(SpecialReg::RBB), 0x3333);
assert_eq!(mmix.get_register(255), 0x4444);
assert_eq!(mmix.get_special(SpecialReg::RWW), 4);
assert_eq!(mmix.get_special(SpecialReg::RXX), 0x8000_0000_0000_6307);
assert_eq!(mmix.get_special(SpecialReg::RYY), 0x1111);
assert_eq!(mmix.get_special(SpecialReg::RZZ), 0x2222);
}
#[test]
fn test_trap_x_nonzero_sets_trap_page_registers_from_full_width_y() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(10, 0x1234_0000_0000_0000);
mmix.set_register(20, 0xABCD);
mmix.set_register(255, 0x5555);
mmix.set_special(SpecialReg::RJ, 0x6666);
mmix.write_tetra(0, 0x00050A14);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(mmix.get_pc(), 0);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"unsimulated TRAP 5,10,20 at PC=0x0000000000000000"
);
assert_eq!(mmix.get_special(SpecialReg::RBB), 0x5555);
assert_eq!(mmix.get_register(255), 0x6666);
assert_eq!(mmix.get_special(SpecialReg::RWW), 4);
assert_eq!(mmix.get_special(SpecialReg::RXX), 0x8000_0000_0005_0A14);
assert_eq!(mmix.get_special(SpecialReg::RYY), 0x1234_0000_0000_0000);
assert_eq!(mmix.get_special(SpecialReg::RZZ), 0xABCD);
}
#[test]
fn test_trap_y_and_z_as_255_read_before_255_is_overwritten() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(255, 0x1357);
mmix.set_special(SpecialReg::RJ, 0x2468);
mmix.write_tetra(0, 0x0005FFFF);
assert!(!mmix.execute_instruction());
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(mmix.get_special(SpecialReg::RYY), 0x1357);
assert_eq!(mmix.get_special(SpecialReg::RZZ), 0x1357);
assert_eq!(mmix.get_special(SpecialReg::RBB), 0x1357);
assert_eq!(mmix.get_register(255), 0x2468);
}
#[test]
fn test_trap_fputs_stdout() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let test_string = b"Hello, MMIX!\0";
let str_addr = 1000u64;
for (i, &byte) in test_string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.set_register(255, str_addr); mmix.write_tetra(0, 0x00000701); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_pc(), 4);
assert_eq!(mmix.get_register(255), 12); assert_eq!(handle.stdout(), b"Hello, MMIX!");
assert!(handle.stderr().is_empty());
}
#[test]
fn test_trap_fputs_stderr() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let test_string = b"Error message\0";
let str_addr = 2000u64;
for (i, &byte) in test_string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000702); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_pc(), 4);
assert_eq!(mmix.get_register(255), 13);
assert_eq!(handle.stderr(), b"Error message");
assert!(handle.stdout().is_empty());
}
#[test]
fn test_trap_fputc_stdout() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(255, b'X' as u64);
mmix.write_tetra(0, 0x00008001); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_register(255), 0); assert_eq!(mmix.get_pc(), 4);
assert_eq!(handle.stdout(), b"X");
}
#[test]
fn test_trap_fputws() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let str_addr = 3000u64;
for (i, &byte) in [b'H', b'i', 0x00, 0x00].iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.set_register(255, str_addr); mmix.write_tetra(0, 0x00000801); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_register(255), 1); assert_eq!(handle.stdout(), b"Hi");
}
#[test]
fn test_trap_fputs_of_exactly_one_chunk_writes_whole() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let str_addr = 10_000u64;
for i in 0..1_048_576u64 {
mmix.write_byte(str_addr + i, b'A');
}
mmix.write_byte(str_addr + 1_048_576, 0);
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000701); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 1_048_576);
assert_eq!(handle.stdout().len(), 1_048_576);
assert!(handle.diagnostics().is_empty());
}
#[test]
fn test_trap_fputs_past_one_chunk_writes_whole() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let str_addr = 1_000_000u64;
let string: Vec<u8> = patterned(1_048_577).into_iter().map(|b| b.max(1)).collect();
for (i, &byte) in string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.write_byte(str_addr + string.len() as u64, 0);
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000701);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 1_048_577);
assert_eq!(handle.stdout(), string);
assert!(handle.diagnostics().is_empty());
}
#[test]
fn test_trap_fputs_returns_minus_one_when_a_later_chunk_fails() {
let mut mmix = MMix::with_host(FailsAfterFirstWrite { writes: 0 });
let str_addr = 1_000_000u64;
for i in 0..1_048_577u64 {
mmix.write_byte(str_addr + i, b'A');
}
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000701);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
}
#[test]
fn test_trap_fputws_of_exactly_one_chunk_writes_whole() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let str_addr = 20_000u64;
for i in 0..524_288u64 {
mmix.write_byte(str_addr + i * 2, 0x00);
mmix.write_byte(str_addr + i * 2 + 1, 0x41);
}
mmix.write_byte(str_addr + 524_288 * 2, 0);
mmix.write_byte(str_addr + 524_288 * 2 + 1, 0);
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000801); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 524_288);
assert_eq!(handle.stdout().len(), 1_048_576);
assert!(handle.diagnostics().is_empty());
}
#[test]
fn test_trap_fputws_past_one_chunk_writes_whole() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let str_addr = 1_000_000u64;
let wydes = 524_289usize;
let mut string = Vec::with_capacity(wydes * 2);
for i in 0..wydes {
string.push((i % 251) as u8);
string.push(0x41);
}
for (i, &byte) in string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.write_byte(str_addr + string.len() as u64, 0);
mmix.write_byte(str_addr + string.len() as u64 + 1, 0);
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000801);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), wydes as u64);
assert_eq!(handle.stdout(), string);
assert!(handle.diagnostics().is_empty());
}
#[test]
fn test_trap_fputws_returns_minus_one_when_a_later_chunk_fails() {
let mut mmix = MMix::with_host(FailsAfterFirstWrite { writes: 0 });
let str_addr = 1_000_000u64;
for i in 0..524_289u64 {
mmix.write_byte(str_addr + i * 2, 0x00);
mmix.write_byte(str_addr + i * 2 + 1, 0x41);
}
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000801);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
}
#[test]
fn test_host_trap_hook_reports_arg_and_both_255_values() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let test_string = b"Hi\0";
let str_addr = 4000u64;
for (i, &byte) in test_string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000701); assert!(mmix.execute_instruction());
let traps = handle.traps();
assert_eq!(traps.len(), 1);
let (code, arg, arg255, result255) = traps[0];
assert_eq!(code, TrapCode::Fputs);
assert_eq!(arg, 1); assert_eq!(arg255, str_addr); assert_eq!(result255, 2); assert!(handle.diagnostics().is_empty()); }
#[test]
fn test_halt_routes_diagnostic_and_flush_to_host() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(255, 42);
mmix.write_tetra(0, 0x00000000); let should_continue = mmix.execute_instruction();
assert!(!should_continue);
assert_eq!(
handle.diagnostics(),
vec!["HALT trap at PC=0x0000000000000000, exit code=42".to_string()]
);
assert_eq!(handle.flushes(), 1);
assert!(handle.stderr().is_empty()); assert_eq!(handle.traps(), vec![(TrapCode::Halt, 0, 42, 42)]);
}
#[test]
fn test_boxed_host_delegates_every_method() {
let (mut host, handle) = CaptureHost::with_clock(7_000_000);
host.queue_input(b"Q");
let boxed: Box<dyn Host> = Box::new(host);
let mut mmix = MMix::with_host(boxed);
mmix.set_register(255, u64::from(b'Z'));
mmix.write_tetra(0, 0x00008001); assert!(mmix.execute_instruction());
mmix.set_pc(4);
mmix.write_tetra(4, 0x00008100); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 7);
mmix.set_pc(8);
let buffer_addr = 100u64;
write_two_octa_params(&mut mmix, buffer_addr, 1);
mmix.write_tetra(8, 0x00000300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), 0);
assert_eq!(mmix.read_byte(buffer_addr), b'Q');
mmix.set_pc(12);
mmix.set_register(255, 9);
mmix.write_tetra(12, 0x00000000); assert!(!mmix.execute_instruction());
assert_eq!(handle.stdout(), b"Z"); assert_eq!(handle.flushes(), 1); assert_eq!(handle.diagnostics().len(), 1); assert_eq!(
handle.traps(),
vec![
(TrapCode::Fputc, 1, u64::from(b'Z'), 0),
(TrapCode::Time, 0, 0, 7),
(TrapCode::Fread, 0, 200_000, 0),
(TrapCode::Halt, 0, 9, 9),
]
); }
#[test]
fn test_injected_clock_drives_handle_time() {
let (host, _handle) = CaptureHost::with_clock(5_000_000); let mut mmix = MMix::with_host(host);
mmix.write_tetra(0, 0x00008100); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_register(255), 5);
}
#[test]
fn test_trap_fclose_error() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00000263); let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_register(255), (-1i64) as u64); }
#[test]
fn test_trap_time_microseconds() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00008102);
let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_pc(), 4);
let time_us = mmix.get_register(255);
assert!(time_us > 0);
assert!(time_us > 1_577_836_800_000_000);
assert!(time_us < 32_503_680_000_000_000);
}
#[test]
fn test_trap_time_milliseconds() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00008101);
let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_pc(), 4);
let time_ms = mmix.get_register(255);
assert!(time_ms > 0);
assert!(time_ms > 1_577_836_800_000);
assert!(time_ms < 32_503_680_000_000);
}
#[test]
fn test_trap_time_seconds() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00008100);
let should_continue = mmix.execute_instruction();
assert!(should_continue);
assert_eq!(mmix.get_pc(), 4);
let time_s = mmix.get_register(255);
assert!(time_s > 0);
assert!(time_s > 1_577_836_800);
assert!(time_s < 32_503_680_000);
}
#[test]
fn test_trap_time_monotonic() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0x00008102); mmix.execute_instruction();
let time1 = mmix.get_register(255);
mmix.set_pc(0);
mmix.execute_instruction();
let time2 = mmix.get_register(255);
assert!(time2 >= time1);
}
#[test]
fn test_trap_fputs_unknown_fd_returns_error() {
let mut mmix = MMix::new();
let test_string = b"data\0";
let str_addr = 200u64;
for (i, &byte) in test_string.iter().enumerate() {
mmix.write_byte(str_addr + i as u64, byte);
}
mmix.set_register(255, str_addr);
mmix.write_tetra(0, 0x00000763);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
}
#[test]
fn test_trap_fputc_unknown_fd_returns_error() {
let mut mmix = MMix::new();
mmix.set_register(255, b'A' as u64);
mmix.write_tetra(0, 0x00008063);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(255), (-1i64) as u64);
}
#[test]
fn test_debug_preserves_every_register_around_a_nested_call() {
use crate::debugger::{entry_point, write_image};
use crate::mmixal::MMixAssembler;
const SOURCE: &str = "\
\tLOC\t#100
Main\tSET\t$1,11
\tPUSHJ\t$2,Sub
\tTRAP\t0,Halt,0
Sub\tSET\t$0,5
\tSET\t$1,7
\tdebug\t\"first\"
\tGET\t$2,rJ
\tPUSHJ\t$3,Nested
\tPUT\trJ,$2
\tdebug\t\"second\"
\tPOP\t1,0
Nested\tSET\t$0,42
\tPOP\t0,0
";
let mut asm = MMixAssembler::new(SOURCE, "<test>");
asm.parse().expect("program must assemble");
let main_addr = *asm.labels.get("Main").expect("Main label");
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_image(&mut mmix, &asm);
mmix.set_pc(entry_point(&asm));
let (_, stop) = mmix.run_bounded(10_000);
assert_eq!(stop, Stop::Halted);
let stdout = String::from_utf8(handle.stdout()).expect("valid utf8");
assert!(stdout.contains("first\n"), "got {stdout:?}");
assert!(stdout.contains("second\n"), "got {stdout:?}");
assert!(
stdout.find("first").unwrap() < stdout.find("second").unwrap(),
"the two debug lines must print in order, got {stdout:?}"
);
assert_eq!(mmix.get_register(1), 11, "caller's local $1 must survive");
assert_eq!(mmix.get_register(2), 5, "returned value");
assert_eq!(mmix.get_special(SpecialReg::RL), 3);
assert_eq!(mmix.get_special(SpecialReg::RJ), main_addr + 8);
assert_eq!(mmix.get_pc(), main_addr + 12, "halt address");
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_debug_at_rg_255_prints_and_continues() {
use crate::debugger::{entry_point, write_image};
use crate::mmixal::MMixAssembler;
const SOURCE: &str = "\
\tLOC\t#100
\tSET\t$1,255
\tPUT\trG,$1
\tdebug\t\"reachable\"
\tTRAP\t0,Halt,0
";
let mut asm = MMixAssembler::new(SOURCE, "<test>");
asm.parse().expect("program must assemble");
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_image(&mut mmix, &asm);
mmix.set_pc(entry_point(&asm));
let (_, stop) = mmix.run_bounded(100);
assert_eq!(stop, Stop::Halted);
assert_eq!(handle.stdout(), b"reachable\n");
assert_eq!(mmix.get_special(SpecialReg::RG), 255);
}
fn assemble_and_run_bounded(source: &str, budget: usize) -> (MMix, Stop, String) {
use crate::debugger::{entry_point, write_image};
use crate::mmixal::MMixAssembler;
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().expect("program must assemble");
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_image(&mut mmix, &asm);
mmix.set_pc(entry_point(&asm));
let (_, stop) = mmix.run_bounded(budget);
let stdout = String::from_utf8(handle.stdout()).expect("valid utf8");
(mmix, stop, stdout)
}
#[test]
fn test_debug_directive_followed_by_a_blank_line_at_eof() {
let source = "\tLOC\t#100\nMain\tdebug \"hi\"\n\n";
let (_, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted, "must not loop, got {stdout:?}");
assert_eq!(stdout, "hi\n");
}
#[test]
fn test_debug_directive_followed_by_a_comment_line_at_eof() {
let source = "\tLOC\t#100\nMain\tdebug \"hi\"\n% nothing else follows\n";
let (_, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted, "must not loop, got {stdout:?}");
assert_eq!(stdout, "hi\n");
}
#[test]
fn test_debug_directive_as_the_last_statement() {
let source = "\tLOC\t#100\nMain\tdebug \"hi\"\n";
let (_, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted, "must not loop, got {stdout:?}");
assert_eq!(stdout, "hi\n");
}
#[test]
fn test_debug_directive_followed_by_an_is_line() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
Ret2\tIS\t#10C
\tSET\t$1,7
\tSET\t$2,Ret2
\tTRAP\t0,Halt,0
";
let (mmix, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(stdout, "hi\n");
assert_eq!(mmix.get_register(1), 7);
assert_eq!(mmix.get_register(2), 0x10C, "Ret2 must still bind #10C");
}
#[test]
fn test_debug_directive_followed_by_a_greg_line() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
Foo\tGREG\t@
\tSET\tFoo,42
\tSET\t$1,Foo
\tTRAP\t0,Halt,0
";
let (mmix, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(stdout, "hi\n");
assert_eq!(mmix.get_register(1), 42, "Foo must still hold 42");
}
#[test]
fn test_debug_directive_followed_by_a_label_only_line() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
Done
\tSET\t$1,7
\tTRAP\t0,Halt,0
";
let (mmix, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(stdout, "hi\n");
assert_eq!(mmix.get_register(1), 7);
}
#[test]
fn test_debug_directive_followed_by_a_colon_prefixed_label() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
:Done
\tSET\t$1,7
\tTRAP\t0,Halt,0
";
let (mmix, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(stdout, "hi\n");
assert_eq!(mmix.get_register(1), 7);
}
#[test]
fn test_debug_directive_writes_non_ascii_text_as_utf8_bytes() {
let source = "\tLOC\t#100\nMain\tdebug\t\"€\"\n\tTRAP\t0,Halt,0\n";
let (_, stop, stdout) = assemble_and_run_bounded(source, 1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(stdout.as_bytes(), [0xE2, 0x82, 0xAC, b'\n']);
}
#[test]
fn test_debug_trap_writes_text_and_a_newline_touching_no_register() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_debug_strings(vec![b"hello".to_vec()]);
for reg in 0..=255u8 {
mmix.set_register(reg, 0xABCD_0000 | u64::from(reg));
}
let before: Vec<u64> = (0..=255u8).map(|r| mmix.get_register(r)).collect();
mmix.write_tetra(0, 0x00008200); assert!(mmix.execute_instruction());
assert_eq!(handle.stdout(), b"hello\n");
assert_eq!(mmix.get_pc(), 4);
for reg in 0..=255u8 {
assert_eq!(
mmix.get_register(reg),
before[reg as usize],
"$#{reg} must survive debug untouched"
);
}
}
#[test]
fn test_debug_trap_index_past_the_table_reports_and_continues() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_debug_strings(vec![b"only one".to_vec()]);
mmix.write_tetra(0, 0x00008201); assert!(mmix.execute_instruction());
assert!(handle.stdout().is_empty());
assert_eq!(mmix.get_pc(), 4);
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains('1'));
}
#[test]
fn test_two_translation_units_each_print_their_own_debug_string() {
use crate::debugger::{entry_point, write_image};
use crate::mmixal::MMixAssembler;
let mut asm = MMixAssembler::new("\tLOC\t#100\nMain\tdebug\t\"from a\"\n", "a.mms");
asm.add_source("\tdebug\t\"from b\"\n\tTRAP\t0,Halt,0\n", "b.mms");
asm.parse().expect("program must assemble");
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
write_image(&mut mmix, &asm);
mmix.set_pc(entry_point(&asm));
let (_, stop) = mmix.run_bounded(1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(handle.stdout(), b"from a\nfrom b\n");
}
#[test]
fn test_debug_string_survives_the_mmo_round_trip() {
use crate::debugger::{entry_point, write_image};
use crate::mmixal::MMixAssembler;
use crate::mmo::MmoDecoder;
let source = "\tLOC\t#100\nMain\tdebug\t\"roundtrip\"\n\tTRAP\t0,Halt,0\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().expect("program must assemble");
let object_code = asm.generate_object_code();
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
let decoder = MmoDecoder::new(object_code);
let entry = decoder
.decode(|addr, byte| mmix.write_byte(addr, byte))
.expect("well-formed object code");
mmix.set_debug_strings(decoder.debug_strings());
mmix.set_pc(entry);
let (_, stop) = mmix.run_bounded(1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(handle.stdout(), b"roundtrip\n");
let (host2, handle2) = CaptureHost::new();
let mut mmix2 = MMix::with_host(host2);
write_image(&mut mmix2, &asm);
mmix2.set_pc(entry_point(&asm));
mmix2.run_bounded(1_000);
assert_eq!(handle.stdout(), handle2.stdout());
}
#[test]
fn trip_handler_at_vector_zero_is_reachable_after_an_mmo_load() {
use crate::mmixal::MMixAssembler;
use crate::mmo::{MmoDecoder, MmoGenerator};
let source = "\
LOC #00
SETI $9,42
RESUME 0
LOC #100
Main TRIP 0,0,0
SETI $10,7
TRAP 0,Halt,0
";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().expect("program must assemble");
let mmo_data = MmoGenerator::new(asm.instructions.clone(), asm.labels.clone())
.with_debug_strings(asm.debug_strings().to_vec())
.with_greg_inits(asm.greg_inits.clone())
.generate();
let mut mmix = MMix::new();
let decoder = MmoDecoder::new(mmo_data);
let entry = decoder.load(&mut mmix).expect("well-formed object code");
mmix.set_pc(entry);
let (_, stop) = mmix.run_bounded(1_000);
assert_eq!(stop, Stop::Halted);
assert_eq!(mmix.get_register(9), 42, "the TRIP handler at #00 must run");
assert_eq!(mmix.get_register(10), 7, "RESUME must return to Main");
}