use hg80::{Cpu, Host, UndocumentedFlags};
use std::path::PathBuf;
struct Machine {
bytes: Vec<u8>,
pushed: Vec<u8>,
}
impl Host for Machine {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.bytes[address as usize]
}
fn write(&mut self, address: u16, value: u8, _at: u32) {
self.bytes[address as usize] = value;
self.pushed.push(value);
}
fn input(&mut self, port: u16, _at: u32) -> u8 {
self.bytes[port as usize]
}
fn output(&mut self, _port: u16, _value: u8, _at: u32) {}
}
fn parse_image(text: &str) -> Vec<u8> {
let mut bytes = vec![0u8; 0x10000];
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut words = line.split_whitespace();
let Some(Ok(start)) = words.next().map(|word| u16::from_str_radix(word, 16)) else {
continue;
};
let mut at = start;
for word in words {
if let Ok(byte) = u8::from_str_radix(word, 16) {
bytes[at as usize] = byte;
at = at.wrapping_add(1);
}
}
}
bytes
}
fn wants_a_non_maskable_interrupt(text: &str) -> bool {
text.lines()
.next()
.is_some_and(|header| header.contains("nmi="))
}
fn run(image: &[u8], nonmaskable: bool) -> Option<[u8; 12]> {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.set_undocumented_flags(UndocumentedFlags::Accumulator);
let mut machine = Machine {
bytes: image.to_vec(),
pushed: Vec::new(),
};
let mut raised = !nonmaskable;
let mut left_the_halt = false;
for _ in 0..64 {
if cpu.is_halted() {
if !raised {
cpu.request_nmi();
raised = true;
} else if left_the_halt {
break;
}
} else if raised {
left_the_halt = true;
}
cpu.step(&mut machine);
}
let pushed = machine.pushed;
if pushed.len() < 12 {
return None;
}
pushed[pushed.len() - 12..].try_into().ok()
}
fn simulation() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sim/t80n")
}
const NAMES: [&str; 12] = [
"A", "F", "H", "L", "D", "E", "B", "C", "IXh", "IXl", "IYh", "IYl",
];
const VECTORS_GOVERN: [(&str, u8); 12] = [
("ini", 0x11),
("inir", 0x15),
("outi", 0x11),
("otir", 0x15),
("enable_then_read_iff", 0x28),
("enable_then_read_iff_delayed", 0x28),
("disable_then_read_iff", 0x28),
("interrupt_vector_round_trip", 0x28),
("refresh_keeps_its_top_bit", 0x28),
("nmi_keeps_the_second_flip_flop", 0x28),
("nmi_from_disabled_reads_clear", 0x28),
("address_latch_bits_set", 0x28),
];
#[test]
fn every_extended_instruction_leaves_the_registers_the_reference_leaves() {
let root = simulation();
let golden = std::fs::read_to_string(root.join("state.golden"))
.expect("the measured results are committed");
let mut failures = Vec::new();
let mut compared = 0;
let mut diverged = 0;
for line in golden.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split_whitespace();
let Some(name) = fields.next() else { continue };
let want: Vec<u8> = fields.filter_map(|word| word.parse().ok()).collect();
assert_eq!(want.len(), 12, "{name}: malformed result line");
let image = std::fs::read_to_string(root.join(format!("state/{name}.hex")))
.unwrap_or_else(|_| panic!("{name}: no probe program"));
let nonmaskable = wants_a_non_maskable_interrupt(&image);
let Some(got) = run(&parse_image(&image), nonmaskable) else {
failures.push(format!(" {name}: pushed fewer than twelve bytes"));
continue;
};
compared += 1;
let excused = VECTORS_GOVERN
.iter()
.find(|&&(probe, _)| probe == name)
.map(|&(_, mask)| mask);
if excused.is_some() {
diverged += 1;
}
let compare = |at: usize, value: u8| match excused {
Some(mask) if NAMES[at] == "F" => value & !mask,
_ => value,
};
let differing: Vec<String> = (0..12)
.filter(|&at| compare(at, got[at]) != compare(at, want[at]))
.map(|at| format!("{}={:02X} want {:02X}", NAMES[at], got[at], want[at]))
.collect();
if !differing.is_empty() {
failures.push(format!(" {name}: {}", differing.join(", ")));
}
}
if diverged != VECTORS_GOVERN.len() {
failures.push(format!(
" {diverged} of the {} probes whose flags the vectors govern were run",
VECTORS_GOVERN.len()
));
}
println!(
"{compared} instructions compared against the reference, {diverged} with their flags \
governed by the vectors instead"
);
for failure in &failures {
println!("{failure}");
}
assert!(
failures.is_empty(),
"{} of {compared} diverged",
failures.len()
);
}