hg80 1.0.0

Z80 and Z80N CPU core, stepped one clock edge at a time
Documentation
//! Final register and flag state after a single instruction, against the reference simulation.
//!
//! The per-T-state comparison in `cycles.rs` proves what appears on the bus and when, but never
//! what ends up in the registers. This closes that. Each probe seeds the flags and the register
//! pairs, runs one instruction, then pushes the accumulator and flags followed by every pair — the
//! index registers included, so an instruction clobbering one gets caught.
//!
//! The result is read off the bus rather than out of the register file. That's what lets the same
//! programs drive both the simulation and this crate.
//!
//! It covers the extended instructions, which have no other oracle at all, and the ordinary ones
//! where the published vectors and this design are known to disagree: the undocumented flag bits of
//! `SCF` and `CCF`, the undocumented pair the block transfers write, and the flags of the port
//! block instructions. That last one is a deliberate divergence, named in `VECTORS_GOVERN`.
//!
//! The measured results are carried here; the simulation that produced them is not. See
//! `sim/t80n/state.sh`.

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(),
    };

    // The run ends at the halt that follows the interrupt, not at the first halt after the line is
    // raised: a halted core keeps fetching until it accepts, so it is still halted for a step or
    // more after being asked.
    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",
];

/// Probes whose flags follow the published vectors rather than the reference design.
///
/// The port block instructions are documented to report the byte transferred and the port in the
/// flags: add/subtract from the byte's top bit, half-carry and carry from whether the byte plus the
/// adjusted port overflows, and parity from three bits of that sum against the counter. The
/// reference does none of it. The only flag write on its port block path comes from the register
/// decrement, so that's all it reports. Measured, it gives `02` and `42` where the documented rule
/// gives `13` and `57`.
///
/// A bit test through `HL` is the same shape again. The documented rule copies two bits of the
/// internal address latch into the undocumented pair; the reference copies neither. Its companion
/// probe, with those latch bits clear, agrees on both sides and is compared in full. That's why the
/// pair exists — one seed with the bits clear reads as agreement whichever side is right.
///
/// The instructions reading the interrupt vector and refresh registers are the same shape but
/// narrower. The reference leaves the two undocumented bits alone where the documented rule takes
/// them from the value loaded. Only those two bits are excused, so the parity bit still gets
/// compared — and that bit carries the interrupt enable state, which is why those probes exist.
///
/// Real software depends on the documented rule and the vectors assert it, so they win here
/// and the divergence is deliberate. Everything outside the mask is still compared: every register,
/// and the counter and pointer each instruction moves.
/// Each mask is the measured difference and no wider. The four port block instructions were a
/// whole-byte exemption until 2026-07-29, which excused the sign, zero, subtract and undocumented
/// flags as well — none of which diverges. Planted faults confirm the difference: a wrong sign bit
/// on `INI` passed under the old mask and fails under this one.
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()
    );
}