use super::*;
#[test]
fn test_register_stack_initialization() {
let mmix = MMix::new();
assert_eq!(mmix.get_special(SpecialReg::RO), STACK_SEGMENT_START);
assert_eq!(mmix.get_special(SpecialReg::RS), STACK_SEGMENT_START);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
assert_eq!(mmix.get_special(SpecialReg::RG), 32);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pushj_basic() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 100);
mmix.set_register(1, 101);
mmix.set_register(2, 102);
mmix.set_special(SpecialReg::RL, 3);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xF2030001);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 32);
assert_eq!(mmix.get_special(SpecialReg::RS), ro + 32);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
assert_eq!(mmix.get_pc(), 0x104);
assert_eq!(mmix.read_octa(ro), 100);
assert_eq!(mmix.read_octa(ro + 8), 101);
assert_eq!(mmix.read_octa(ro + 16), 102);
assert_eq!(mmix.read_octa(ro + 24), 3);
assert_eq!(mmix.get_register(0), 0);
assert_eq!(mmix.get_register(1), 0);
assert_eq!(mmix.get_register(2), 0);
assert_eq!(mmix.call_depth(), 1);
}
#[test]
fn test_pushj_spills_zero_not_stale_content_when_caller_rl_is_zero() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 0xDEAD);
mmix.set_register(1, 0xBEEF);
mmix.set_register(2, 0xCAFE);
mmix.set_special(SpecialReg::RL, 0);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xF2020001);
mmix.execute_instruction();
assert_eq!(mmix.read_octa(ro), 0);
assert_eq!(mmix.read_octa(ro + 8), 0);
assert_eq!(mmix.read_octa(ro + 16), 2); assert_eq!(mmix.get_special(SpecialReg::RL), 0);
}
#[test]
fn test_pop_reads_the_hole_from_memory_even_when_rewritten() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 100);
mmix.set_register(1, 101);
mmix.set_register(2, 102);
mmix.set_special(SpecialReg::RL, 3);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xF2030001);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 32);
mmix.write_octa(ro + 24, 1);
mmix.write_tetra(0x104, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 16);
assert_eq!(mmix.get_special(SpecialReg::RS), ro + 16);
}
#[test]
fn test_pushj_window_slide_argument_passing() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(5, 0xAA);
mmix.set_special(SpecialReg::RL, 8);
mmix.write_tetra(0x100, 0xF2040001);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 0xAA);
assert_eq!(mmix.get_special(SpecialReg::RL), 3);
mmix.write_tetra(0x104, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RL), 4);
assert_eq!(mmix.get_register(5), 0);
assert_eq!(mmix.get_register(4), 0);
}
#[test]
fn test_pushj_window_slide_return_value() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RL, 5);
mmix.write_tetra(0x100, 0xF2040001);
mmix.execute_instruction();
mmix.set_register(0, 0xBB);
mmix.write_tetra(0x104, 0xF8010000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(4), 0xBB);
assert_eq!(mmix.get_special(SpecialReg::RL), 5);
}
#[test]
fn test_pushj_zeros_freshly_allocated_locals() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
for i in 0..8u8 {
mmix.set_register(i, 1000 + i as u64);
}
mmix.set_special(SpecialReg::RL, 8);
mmix.write_tetra(0x100, 0xF2040001);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 1005);
assert_eq!(mmix.get_register(1), 1006);
assert_eq!(mmix.get_register(2), 1007);
assert_eq!(mmix.get_register(3), 0);
assert_eq!(mmix.get_register(4), 0);
assert_eq!(mmix.get_register(5), 0);
assert_eq!(mmix.get_register(7), 0);
}
#[test]
fn test_pop_with_return_value_shift() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RL, 4);
mmix.write_tetra(0x100, 0xF2030001);
mmix.execute_instruction();
mmix.set_register(0, 0x111);
mmix.set_register(1, 0x222);
mmix.write_tetra(0x104, 0xF8020000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(3), 0x222);
assert_eq!(mmix.get_register(4), 0x111);
assert_eq!(mmix.get_special(SpecialReg::RL), 5);
assert_eq!(mmix.get_register(0), 0);
assert_eq!(mmix.get_register(1), 0);
assert_eq!(mmix.get_register(2), 0);
}
#[test]
fn test_pushgo_pop_basic() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 10);
mmix.set_register(1, 11);
mmix.set_register(3, 0x200);
mmix.set_register(4, 0);
mmix.set_special(SpecialReg::RL, 2);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xBE020304);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 24);
assert_eq!(mmix.get_special(SpecialReg::RS), ro + 24);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
assert_eq!(mmix.get_pc(), 0x200);
assert_eq!(mmix.read_octa(ro), 10);
assert_eq!(mmix.read_octa(ro + 8), 11);
assert_eq!(mmix.read_octa(ro + 16), 2);
assert_eq!(mmix.call_depth(), 1);
mmix.write_tetra(0x200, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 10);
assert_eq!(mmix.get_register(1), 11);
assert_eq!(mmix.get_special(SpecialReg::RO), ro);
assert_eq!(mmix.get_special(SpecialReg::RS), ro);
assert_eq!(mmix.get_special(SpecialReg::RL), 2);
assert_eq!(mmix.get_pc(), 0x104);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pushgo_off_tetra_target_keeps_low_bits_in_pc() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(3, 0x200);
mmix.set_register(4, 3);
mmix.write_tetra(0x100, 0xBE020304); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x203);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
mmix.write_tetra(0x200, 0xF4050002); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(5), 0x20B);
assert_eq!(mmix.get_pc(), 0x207);
}
#[test]
fn test_pushgoi_off_tetra_target_keeps_low_bits_in_pc() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(3, 0x200);
mmix.write_tetra(0x100, 0xBF020303); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x203);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
mmix.write_tetra(0x200, 0xF4050002); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(5), 0x20B);
assert_eq!(mmix.get_pc(), 0x207);
}
#[test]
fn test_pushgo_from_off_tetra_pc_return_address_carries_low_bits() {
let mut mmix = MMix::new();
mmix.set_pc(0x101);
mmix.set_register(3, 0x200);
mmix.set_register(4, 0);
mmix.write_tetra(0x100, 0xBE020304); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x200);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x105);
}
#[test]
fn test_pushgoi_from_off_tetra_pc_return_address_carries_low_bits() {
let mut mmix = MMix::new();
mmix.set_pc(0x102);
mmix.set_register(3, 0x200);
mmix.write_tetra(0x100, 0xBF020300); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x200);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x106);
}
#[test]
fn test_pop_basic() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 100);
mmix.set_register(1, 101);
mmix.set_register(2, 102);
mmix.set_special(SpecialReg::RL, 3);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xF2030001);
mmix.execute_instruction();
mmix.set_register(0, 200);
mmix.set_register(1, 201);
mmix.set_register(2, 202);
mmix.write_tetra(0x104, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 100);
assert_eq!(mmix.get_register(1), 101);
assert_eq!(mmix.get_register(2), 102);
assert_eq!(mmix.get_special(SpecialReg::RO), ro);
assert_eq!(mmix.get_special(SpecialReg::RS), ro);
assert_eq!(mmix.get_special(SpecialReg::RL), 3);
assert_eq!(mmix.get_pc(), 0x104);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pop_with_return_values() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
for i in 0..8u8 {
mmix.set_register(i, 100 + i as u64);
}
mmix.set_special(SpecialReg::RL, 8);
mmix.write_tetra(0x100, 0xF2040001);
mmix.execute_instruction();
mmix.set_register(0, 300);
mmix.set_register(1, 301);
mmix.set_register(2, 302);
mmix.write_tetra(0x104, 0xF8030000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(4), 302);
assert_eq!(mmix.get_register(5), 300);
assert_eq!(mmix.get_register(6), 301);
assert_eq!(mmix.get_register(0), 100);
assert_eq!(mmix.get_register(1), 101);
assert_eq!(mmix.get_register(2), 102);
assert_eq!(mmix.get_register(3), 103);
assert_eq!(mmix.get_special(SpecialReg::RL), 7);
}
#[test]
fn test_pop_no_return_values() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
for i in 0..10u8 {
mmix.set_register(i, 100 + i as u64);
}
mmix.set_special(SpecialReg::RL, 10);
mmix.write_tetra(0x100, 0xF2050001);
mmix.execute_instruction();
mmix.set_register(0, 999);
mmix.set_register(1, 888);
mmix.set_register(2, 777);
mmix.write_tetra(0x104, 0xF8000000);
mmix.execute_instruction();
for i in 0..5u8 {
assert_eq!(mmix.get_register(i), 100 + i as u64);
}
assert_eq!(mmix.get_register(5), 0);
for i in 6..10u8 {
assert_eq!(mmix.get_register(i), 0);
}
assert_eq!(mmix.get_special(SpecialReg::RL), 5);
}
#[test]
fn test_pop_mmixware_program1_matches_measured_values() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(1, 111);
mmix.set_register(2, 222);
mmix.set_register(3, 333);
mmix.set_register(4, 444);
mmix.set_register(5, 555);
mmix.write_tetra(0x100, 0xF2000001);
mmix.execute_instruction();
mmix.set_register(0, 999);
mmix.set_register(1, 777);
mmix.write_tetra(0x104, 0xF8010000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 999);
for i in 1..=5u8 {
assert_eq!(mmix.get_register(i), 0);
}
assert_eq!(mmix.get_special(SpecialReg::RL), 1);
}
#[test]
fn test_pop_mmixware_program2_matches_measured_values() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 10);
mmix.set_register(1, 20);
mmix.set_register(2, 30);
mmix.set_register(3, 40);
mmix.set_register(4, 50);
mmix.set_register(5, 60);
mmix.set_register(6, 70);
mmix.write_tetra(0x100, 0xF2030001);
mmix.execute_instruction();
mmix.set_register(0, 801);
mmix.set_register(1, 802);
mmix.set_register(2, 803);
mmix.write_tetra(0x104, 0xF8020000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 10);
assert_eq!(mmix.get_register(1), 20);
assert_eq!(mmix.get_register(2), 30);
assert_eq!(mmix.get_register(3), 802);
assert_eq!(mmix.get_register(4), 801);
assert_eq!(mmix.get_register(5), 0);
assert_eq!(mmix.get_register(6), 0);
assert_eq!(mmix.get_special(SpecialReg::RL), 5);
}
#[test]
fn test_pop_0_0_leaves_hole_and_everything_above_it_zero() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 10);
mmix.set_register(1, 20);
mmix.set_register(2, 30);
mmix.set_register(3, 40);
mmix.set_special(SpecialReg::RL, 4);
mmix.write_tetra(0x100, 0xF2020001);
mmix.execute_instruction();
mmix.set_register(0, 999);
mmix.write_tetra(0x104, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 10);
assert_eq!(mmix.get_register(1), 20);
for i in 2..32u8 {
assert_eq!(mmix.get_register(i), 0);
}
assert_eq!(mmix.get_special(SpecialReg::RL), 2);
}
#[test]
fn test_pop_x_greater_than_l_clamps_and_zeros_the_hole() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 100);
mmix.set_register(2, 200);
mmix.set_special(SpecialReg::RL, 3);
mmix.write_tetra(0x100, 0xF2010001);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RL), 1);
mmix.set_register(0, 555);
mmix.write_tetra(0x104, 0xF8030000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 100);
assert_eq!(mmix.get_register(1), 0);
assert_eq!(mmix.get_register(2), 555);
assert_eq!(mmix.get_special(SpecialReg::RL), 3);
}
#[test]
fn test_pushj_x_at_or_above_rg_saves_all_locals_and_pops_at_the_hole() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RG, 10);
mmix.set_register(0, 1000);
mmix.set_register(1, 1001);
mmix.set_register(2, 1002);
mmix.set_register(3, 1003);
mmix.set_register(4, 1004);
mmix.set_special(SpecialReg::RL, 5);
mmix.set_register(50, 0xBEEF);
let rs_before = mmix.get_special(SpecialReg::RS);
mmix.write_tetra(0x100, 0xF2FF0001);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
mmix.set_register(0, 777);
mmix.write_tetra(0x104, 0xF8010000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 1000);
assert_eq!(mmix.get_register(1), 1001);
assert_eq!(mmix.get_register(2), 1002);
assert_eq!(mmix.get_register(3), 1003);
assert_eq!(mmix.get_register(4), 1004);
assert_eq!(mmix.get_register(5), 777);
assert_eq!(mmix.get_special(SpecialReg::RL), 6);
assert_eq!(mmix.get_register(50), 0xBEEF);
assert_eq!(mmix.get_special(SpecialReg::RS), rs_before);
}
#[test]
fn test_pushgoi_pop_basic() {
let mut mmix = MMix::new();
mmix.set_pc(0x300);
mmix.set_register(0, 42);
mmix.set_register(1, 43);
mmix.set_register(5, 0x310);
mmix.set_special(SpecialReg::RL, 2);
let ro = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x300, 0xBF020510);
mmix.execute_instruction();
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 24);
assert_eq!(mmix.get_special(SpecialReg::RS), ro + 24);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x304);
assert_eq!(mmix.get_pc(), 0x320);
assert_eq!(mmix.read_octa(ro), 42);
assert_eq!(mmix.read_octa(ro + 8), 43);
assert_eq!(mmix.read_octa(ro + 16), 2); assert_eq!(mmix.call_depth(), 1);
mmix.write_tetra(0x320, 0xF8000000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 42);
assert_eq!(mmix.get_register(1), 43);
assert_eq!(mmix.get_special(SpecialReg::RO), ro);
assert_eq!(mmix.get_special(SpecialReg::RS), ro);
assert_eq!(mmix.get_special(SpecialReg::RL), 2);
assert_eq!(mmix.get_pc(), 0x304);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pushj_pop_nested() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 10);
mmix.set_register(1, 11);
mmix.set_register(2, 0xCAFE); mmix.set_special(SpecialReg::RL, 5);
let ro = mmix.get_special(SpecialReg::RO);
mmix.set_register(3, 0xCAFE);
mmix.write_tetra(0x100, 0xF2020001);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 0xCAFE);
assert_eq!(mmix.call_depth(), 1);
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 24);
mmix.set_register(1, 0xBEEF);
mmix.write_tetra(0x104, 0xF2000001);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 0xBEEF);
assert_eq!(mmix.call_depth(), 2);
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 32);
mmix.set_register(0, 0xD00D);
mmix.write_tetra(0x108, 0xF8010000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(0), 0xD00D);
assert_eq!(mmix.call_depth(), 1);
assert_eq!(mmix.get_special(SpecialReg::RO), ro + 24);
mmix.write_tetra(0x10C, 0xF8010000);
mmix.execute_instruction();
assert_eq!(mmix.get_register(2), 0xD00D);
assert_eq!(mmix.get_register(0), 10);
assert_eq!(mmix.get_register(1), 11);
assert_eq!(mmix.get_special(SpecialReg::RO), ro);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pop_leaves_rj_as_pushj_set_it() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RJ, 0x999);
mmix.write_tetra(0x100, 0xF2000002); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
mmix.write_tetra(0x108, 0xF8000000); assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_special(SpecialReg::RJ),
0x104,
"POP must leave rJ exactly as PUSHJ set it, not restore 0x999"
);
assert_eq!(mmix.get_pc(), 0x104);
}
#[test]
fn test_pop_without_saving_rj_returns_to_the_nested_call_site() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.write_tetra(0x100, 0xF2000002);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x108, 0xF2000004);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x10C);
mmix.write_tetra(0x118, 0xF8000000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x10C);
mmix.write_tetra(0x10C, 0xF8000000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x10C);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pop_returns_correctly_when_rj_is_saved_and_restored() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.write_tetra(0x100, 0xF2000002);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x108, 0xFE010004);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(1), 0x104);
mmix.write_tetra(0x10C, 0xF2020003);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x118, 0xF8000000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x110);
mmix.write_tetra(0x110, 0xF6040001);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
mmix.write_tetra(0x114, 0xF8010000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x104);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_pushj() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0xF200000A); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 40); assert_eq!(mmix.get_special(SpecialReg::RJ), 4); }
#[test]
fn test_pushjb() {
let mut mmix = MMix::new();
mmix.set_pc(100);
mmix.write_tetra(100, 0xF300FFFB); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 80); assert_eq!(mmix.get_special(SpecialReg::RJ), 104); }
#[test]
fn pop_returns_a_callee_computed_remainder_to_the_hole() {
let cases = [
(42, 42),
(142, 42),
(-58, 42),
(-194, 6),
(0, 0),
(100, 0),
(-100, 0),
(-1, 99),
];
for (dividend, expected) in cases {
let source = format!(
"\
\tLOC\t#100
Main\tSETI\t$1,{dividend}
\tSET\t$2,100
\tPUSHJ\t$0,RemEuclid
\tSET\t$255,$0
\tTRAP\t0,Halt,0
RemEuclid\tDIV\t$2,$0,$1
\tMUL\t$3,$2,$1
\tSUB\t$0,$0,$3
\tBNN\t$0,Done
\tADDU\t$0,$0,$1
Done\tPOP\t1,0
"
);
assert_eq!(run_to_halt(&source), expected, "{dividend} mod 100");
}
}
#[test]
fn test_pop_frame_yz_above_half_the_field_resumes_forward() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.write_tetra(0x100, 0xF2000004); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x104);
mmix.write_tetra(0x110, 0xF8008000); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x104 + 32768 * 4);
}
#[test]
fn test_pop_without_a_frame_resumes_forward_from_rj() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RJ, 0x200);
mmix.write_tetra(0x100, 0xF8008000); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0x200 + 32768 * 4);
}
#[test]
fn test_save_rejects_a_destination_below_rg_leaving_the_machine_unchanged() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(40, 0xDEAD); mmix.set_special(SpecialReg::RG, 50);
mmix.set_special(SpecialReg::RL, 3);
mmix.write_tetra(0, 0xFA2D0000);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_special(SpecialReg::RL), 3, "rL is untouched");
assert_eq!(
mmix.get_register(40),
0xDEAD,
"the destination-rise pre-claim never ran"
);
assert_eq!(mmix.get_register(45), 0, "the rejected SAVE wrote nothing");
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("SAVE"));
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_save_y_nonzero_is_rejected() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0x1234);
mmix.write_tetra(0, 0xFA3C0100);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_register(0), 0x1234, "SAVE touched nothing");
assert_eq!(mmix.get_register(60), 0, "SAVE touched nothing");
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"SAVE Y=1: must be zero; illegal-instruction interrupt at PC=0x0000000000000000"
);
}
#[test]
fn test_save_z_nonzero_is_rejected() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0x1234); mmix.set_register(255, 0xABCD);
mmix.write_tetra(0, 0xFAFF0001);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_register(0), 0x1234, "SAVE touched nothing");
assert_eq!(mmix.get_register(255), 0xABCD, "SAVE touched nothing");
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"SAVE Z=1: must be zero; illegal-instruction interrupt at PC=0x0000000000000000"
);
}
#[test]
fn test_unsave_x_nonzero_is_rejected() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0xFEED);
mmix.write_tetra(0, 0xFB0100FF);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_register(0), 0xFEED, "UNSAVE touched nothing");
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"UNSAVE X=1: must be zero; illegal-instruction interrupt at PC=0x0000000000000000"
);
}
#[test]
fn test_unsave_y_nonzero_is_rejected() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0xFEED);
mmix.write_tetra(0, 0xFB0001FF);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_register(0), 0xFEED, "UNSAVE touched nothing");
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 1);
assert_eq!(
handle.diagnostics()[0],
"UNSAVE Y=1: must be zero; illegal-instruction interrupt at PC=0x0000000000000000"
);
}
#[test]
fn test_pop_rg_guard_preserves_a_global_set_after_a_mid_call_shrink() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_special(SpecialReg::RG, 40);
mmix.set_special(SpecialReg::RL, 38);
mmix.write_tetra(0x100, 0xF2240001);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x104, 0xF7130020);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x108, 0xE32203E7);
assert!(mmix.execute_instruction());
mmix.write_tetra(0x10C, 0xF8000000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(34), 999);
assert_eq!(mmix.get_special(SpecialReg::RG), 32);
assert_eq!(mmix.get_special(SpecialReg::RL), 32);
}
#[test]
fn test_pop() {
let mut mmix = MMix::new();
mmix.set_special(SpecialReg::RJ, 200);
mmix.write_tetra(0, 0xF8000000); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 200); }
#[test]
fn test_save() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0xFA280000); assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 4);
}
#[test]
fn test_unsave() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0xFB000001); assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0);
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_save_writes_the_documented_layout() {
let mut mmix = MMix::new();
mmix.set_register(0, 0x1111);
mmix.set_register(1, 0x2222);
mmix.set_register(2, 0x3333); mmix.set_special(SpecialReg::RG, 250); for i in 250u8..=255 {
mmix.set_register(i, 0x9000 + i as u64);
}
for (i, reg) in SAVE_SPECIALS.iter().enumerate() {
mmix.set_special(*reg, 0x7000 + i as u64);
}
mmix.set_special(SpecialReg::RA, 0x2A);
let ro_before = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0, 0xFAFA0000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.read_octa(ro_before), 0x1111);
assert_eq!(mmix.read_octa(ro_before + 8), 0x2222);
assert_eq!(mmix.read_octa(ro_before + 16), 0x3333);
let marker_addr = ro_before + 24;
assert_eq!(mmix.read_octa(marker_addr), 3, "the marker holds rL");
let globals_base = marker_addr + 8;
for i in 0u64..6 {
assert_eq!(
mmix.read_octa(globals_base + i * 8),
0x9000 + 250 + i,
"global {}",
250 + i
);
}
let specials_base = globals_base + 6 * 8;
for i in 0..SAVE_SPECIALS.len() as u64 {
assert_eq!(
mmix.read_octa(specials_base + i * 8),
0x7000 + i,
"special at index {i}"
);
}
let packed_addr = specials_base + (SAVE_SPECIALS.len() as u64) * 8;
assert_eq!(mmix.read_octa(packed_addr), (250u64 << 56) | 0x2A);
assert_eq!(
mmix.get_register(250),
packed_addr,
"$X holds the packed octa's address"
);
assert_eq!(mmix.get_special(SpecialReg::RO), packed_addr + 8);
assert_eq!(mmix.get_special(SpecialReg::RS), packed_addr + 8);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
}
#[test]
fn test_save_unsave_round_trips_every_register_class() {
let mut mmix = MMix::new();
mmix.set_register(0, 0x1111); mmix.set_register(1, 0x2222); mmix.set_register(60, 0x3333); mmix.set_special(SpecialReg::RJ, 0x4444);
mmix.set_special(SpecialReg::RM, 0x5555);
mmix.set_special(SpecialReg::RA, 0x2A);
let rl_before = mmix.get_special(SpecialReg::RL);
let ro_before = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0, 0xFA460000);
assert!(mmix.execute_instruction());
mmix.set_register(0, 0);
mmix.set_register(1, 0);
mmix.set_register(60, 0);
mmix.set_special(SpecialReg::RJ, 0);
mmix.set_special(SpecialReg::RM, 0);
mmix.set_special(SpecialReg::RA, 0);
mmix.set_special(SpecialReg::RG, 200);
mmix.set_special(SpecialReg::RL, 0);
mmix.write_tetra(4, 0xFB000046);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_register(0), 0x1111);
assert_eq!(mmix.get_register(1), 0x2222);
assert_eq!(mmix.get_register(60), 0x3333);
assert_eq!(mmix.get_special(SpecialReg::RJ), 0x4444);
assert_eq!(mmix.get_special(SpecialReg::RM), 0x5555);
assert_eq!(mmix.get_special(SpecialReg::RA), 0x2A);
assert_eq!(mmix.get_special(SpecialReg::RG), 32);
assert_eq!(mmix.get_special(SpecialReg::RL), rl_before);
assert_eq!(mmix.get_special(SpecialReg::RO), ro_before);
assert_eq!(mmix.get_special(SpecialReg::RS), ro_before);
}
#[test]
fn test_save_unsave_inside_a_call_leaves_the_caller_frame_intact() {
let mut mmix = MMix::new();
mmix.set_pc(0x100);
mmix.set_register(0, 888); mmix.set_special(SpecialReg::RL, 3); let ro_before_call = mmix.get_special(SpecialReg::RO);
mmix.write_tetra(0x100, 0xF2020001);
assert!(mmix.execute_instruction());
let ro_in_callee = mmix.get_special(SpecialReg::RO);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
mmix.set_register(0, 0xABC);
mmix.write_tetra(0x104, 0xFA280000);
assert!(mmix.execute_instruction());
mmix.set_register(0, 0xDEAD);
mmix.set_special(SpecialReg::RM, 0xBAD);
mmix.write_tetra(0x108, 0xFB000028);
assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_register(0),
0xABC,
"the callee's own local round-trips"
);
assert_eq!(
mmix.get_special(SpecialReg::RM),
0,
"rM round-trips to its pre-SAVE value"
);
assert_eq!(mmix.get_special(SpecialReg::RO), ro_in_callee);
assert_eq!(mmix.get_special(SpecialReg::RS), ro_in_callee);
assert_eq!(
mmix.get_special(SpecialReg::RL),
1,
"the callee's own single local"
);
mmix.write_tetra(0x10C, 0xF8000000);
assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_register(0),
888,
"caller's $0, pushed by PUSHJ, survives the call"
);
assert_eq!(mmix.get_special(SpecialReg::RO), ro_before_call);
assert_eq!(mmix.get_special(SpecialReg::RS), ro_before_call);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_call_depth_and_pop_follow_ro_through_save_pushj_unsave() {
let mut mmix = MMix::new();
let base = mmix.get_special(SpecialReg::RO);
assert_eq!(mmix.call_depth(), 0);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFA280000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 0, "a SAVE context is not a frame");
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF2000001);
assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 1, "PUSHJ opened one frame");
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFB000028);
assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_special(SpecialReg::RO),
base,
"UNSAVE rewinds rO to where SAVE found it"
);
assert_eq!(
mmix.call_depth(),
0,
"rO names the top level again; the dead frame does not count"
);
let rl_before_pop = mmix.get_special(SpecialReg::RL);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF8010000);
assert!(mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "POP took the no-frame fallback");
assert_eq!(
mmix.get_special(SpecialReg::RO),
base,
"the fallback leaves rO alone"
);
assert_eq!(
mmix.get_special(SpecialReg::RL),
rl_before_pop,
"the fallback leaves rL alone"
);
assert_eq!(mmix.call_depth(), 0);
}
#[test]
fn test_call_depth_reads_one_across_a_save_inside_a_call() {
let mut mmix = MMix::new();
assert_eq!(mmix.call_depth(), 0);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF2000001); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 1);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFA280000); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 1, "the SAVE context does not count");
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFB000028); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 1, "still just the one open call");
}
#[test]
fn test_call_depth_nests_three_deep_with_a_save_unsave_pair_at_depth_two() {
let mut mmix = MMix::new();
assert_eq!(mmix.call_depth(), 0);
for depth in 1..=2u64 {
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF2000001); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), depth as usize);
}
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFA280000); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 2);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFB000028); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 2);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF2000001); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), 3);
for depth in (0..=2u64).rev() {
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF8000000); assert!(mmix.execute_instruction());
assert_eq!(mmix.call_depth(), depth as usize);
}
}
#[test]
fn test_pop_after_a_bare_save_reads_the_packed_octas_low_byte_as_a_hole() {
let mut mmix = MMix::new();
mmix.set_special(SpecialReg::RA, 5);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xFA280000); assert!(mmix.execute_instruction());
let ro_after_save = mmix.get_special(SpecialReg::RO);
assert!(ro_after_save > STACK_SEGMENT_START);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF8000000); assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_special(SpecialReg::RO),
ro_after_save - 48,
"rO retracted by 8*(5+1), the packed octa's low byte read as a hole"
);
}
#[test]
fn test_pop_and_call_depth_at_or_below_the_base_take_the_fallback() {
let mut mmix = MMix::new();
assert_eq!(mmix.get_special(SpecialReg::RO), STACK_SEGMENT_START);
assert_eq!(mmix.call_depth(), 0);
mmix.set_special(SpecialReg::RJ, 0x200);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF8008000); assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_pc(),
0x200 + 32768 * 4,
"at the base: the fallback fired"
);
mmix.set_special(SpecialReg::RO, STACK_SEGMENT_START - 8);
assert_eq!(mmix.call_depth(), 0);
mmix.set_special(SpecialReg::RJ, 0x300);
mmix.set_pc(0);
mmix.write_tetra(0, 0xF8000000); assert!(mmix.execute_instruction());
assert_eq!(
mmix.get_pc(),
0x300,
"below the base: the fallback fired too"
);
}
#[test]
fn test_pop_halts_on_a_misaligned_ro() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_special(SpecialReg::RO, STACK_SEGMENT_START + 3);
mmix.set_register(0, 0xFEED);
assert_eq!(
mmix.call_depth(),
0,
"the walk stops at once on a misaligned rO"
);
mmix.write_tetra(0, 0xF8000000); assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "no PC advance on a halt");
assert_eq!(mmix.get_register(0), 0xFEED, "no register change on a halt");
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("rO"));
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_pop_halts_on_an_ro_above_the_stack_segment() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_special(SpecialReg::RO, 0x8000000000000000);
mmix.set_register(0, 0xFEED);
assert_eq!(
mmix.call_depth(),
0,
"the walk stops at once outside the segment"
);
mmix.write_tetra(0, 0xF8000000); assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0);
assert_eq!(mmix.get_register(0), 0xFEED);
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("rO"));
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_call_depth_does_not_loop_on_a_marker_mapping_back_to_itself() {
let mut mmix = MMix::new();
mmix.write_tetra(0, 0xFA280000);
assert!(mmix.execute_instruction());
let packed_addr = mmix.get_register(40);
let ro = mmix.get_special(SpecialReg::RO);
assert_eq!(ro, packed_addr + 8);
let global_count = 256u64 - 32; let marker_addr = packed_addr - (SAVE_SPECIALS.len() as u64) * 8 - global_count * 8 - 8;
let diff = marker_addr.wrapping_sub(ro);
assert_eq!(diff % 8, 0, "sanity: layout offsets are all multiples of 8");
mmix.write_octa(marker_addr, diff / 8);
assert_eq!(
mmix.call_depth(),
0,
"the corrupted local count exceeds the saved rG; the walk stops \
instead of looping back to where it started"
);
}
#[test]
fn test_call_depth_walks_a_save_context_with_nonzero_rl_and_rg_ne_32() {
let mut mmix = MMix::new();
mmix.set_special(SpecialReg::RG, 50);
mmix.set_special(SpecialReg::RL, 3);
mmix.set_register(0, 0x111);
mmix.set_register(1, 0x222);
mmix.set_register(2, 0x333);
mmix.write_tetra(0, 0xFA3C0000);
assert!(mmix.execute_instruction());
assert_eq!(
mmix.call_depth(),
0,
"the context alone, locals and all, is not a frame"
);
let pc = mmix.get_pc();
mmix.write_tetra(pc, 0xF2000001);
assert!(mmix.execute_instruction());
assert_eq!(
mmix.call_depth(),
1,
"the walk steps over the saved rL=3, rG=50 context uncounted \
and still finds the one PUSHJ frame beneath it"
);
}
#[test]
fn test_save_and_unsave_reject_their_must_be_zero_fields() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0x1234); mmix.set_register(50, 0xABCD);
mmix.write_tetra(0, 0xFA3CFFFF);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_register(0), 0x1234, "SAVE touched nothing");
assert_eq!(mmix.get_register(50), 0xABCD, "SAVE touched nothing");
assert_eq!(mmix.get_exit_code(), 1);
mmix.set_pc(4);
mmix.write_tetra(4, 0xFBFFFF3C);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 4, "the PC stays on the rejected instruction");
assert_eq!(mmix.get_exit_code(), 1);
assert_eq!(handle.diagnostics().len(), 2);
assert!(handle.diagnostics()[0].contains("SAVE Y=255"));
assert!(handle.diagnostics()[1].contains("UNSAVE X=255"));
}
#[test]
fn test_save_is_deterministic_across_instances() {
let mut a = MMix::new();
let mut b = MMix::new();
a.write_tetra(0, 0xFA280000); b.write_tetra(0, 0xFA280000);
assert!(a.execute_instruction());
assert!(b.execute_instruction());
assert_eq!(a.get_register(40), b.get_register(40));
}
#[test]
fn test_unsave_rejects_a_packed_rg_below_32() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0xFEED);
let context = 0x2000u64;
mmix.write_octa(context, 31u64 << 56); mmix.set_register(60, context);
mmix.write_tetra(0, 0xFB00003C);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0);
assert_eq!(mmix.get_register(0), 0xFEED);
assert_eq!(mmix.get_special(SpecialReg::RG), 32);
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("rG"));
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_unsave_rejects_a_packed_ra_above_ra_max() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_register(0, 0xFEED);
let context = 0x2000u64;
mmix.write_octa(context, (32u64 << 56) | (RA_MAX + 1));
mmix.set_register(60, context);
mmix.write_tetra(0, 0xFB00003C);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 0);
assert_eq!(mmix.get_register(0), 0xFEED);
assert_eq!(mmix.get_special(SpecialReg::RA), 0);
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("rA"));
assert_eq!(mmix.get_exit_code(), 1);
}
#[test]
fn test_unsave_rejects_a_saved_local_count_above_the_packed_rg() {
let (host, handle) = CaptureHost::new();
let mut mmix = MMix::with_host(host);
mmix.set_special(SpecialReg::RL, 0);
mmix.write_tetra(0, 0xFA280000);
assert!(mmix.execute_instruction());
let context = mmix.get_register(40);
let global_count = 256u64 - 32; let marker_addr = context - (SAVE_SPECIALS.len() as u64) * 8 - global_count * 8 - 8;
assert_eq!(
mmix.read_octa(marker_addr),
0,
"sanity: 0 locals were saved"
);
mmix.write_octa(marker_addr, 33);
mmix.set_register(0, 0xFEED);
mmix.write_tetra(4, 0xFB000028);
assert!(!mmix.execute_instruction());
assert_eq!(mmix.get_pc(), 4);
assert_eq!(mmix.get_register(0), 0xFEED);
assert_eq!(handle.diagnostics().len(), 1);
assert!(handle.diagnostics()[0].contains("local count"));
assert_eq!(mmix.get_exit_code(), 1);
}