use super::*;
#[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_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_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 (host, handle) = CaptureHost::with_clock(7_000_000);
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);
mmix.set_register(255, 9);
mmix.write_tetra(8, 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::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_read_cstring() {
let mut mmix = MMix::new();
let test_string = b"Test String\0";
let addr = 1000u64;
for (i, &byte) in test_string.iter().enumerate() {
mmix.write_byte(addr + i as u64, byte);
}
let result = mmix.read_cstring(addr, 256);
assert_eq!(result, "Test String");
}
#[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_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));
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());
}