use super::{AddressSource, Context, IndexState, InstructionSet, SpecialLoad, decode};
use crate::alu::AluOp;
use crate::consts::flag::{C_MASK, S_MASK, Z_MASK};
use crate::types::{InterruptMode, MachineCycle, Z80nCommand};
use std::prelude::rust_2024::*;
fn context(set: InstructionSet, ir: u8, cycle: MachineCycle, flags: u8) -> Context {
Context {
ir,
instruction_set: set,
machine_cycle: cycle,
flags,
nmi_cycle: false,
interrupt_cycle: false,
index_state: IndexState::None,
accumulator: 0,
data: 0,
z80n_enabled: false,
}
}
fn at(set: InstructionSet, ir: u8, cycle: u8, flags: u8) -> super::Decoded {
decode(context(set, ir, MachineCycle::from_number(cycle), flags))
}
fn timing(set: InstructionSet, ir: u8, flags: u8) -> u32 {
let mut total = if matches!(set, InstructionSet::Base) {
0
} else {
4
};
let mut cycle = 1u8;
loop {
let decoded = at(set, ir, cycle, flags);
total += u32::from(decoded.t_states);
if cycle >= decoded.machine_cycles {
return total;
}
cycle += 1;
}
}
#[test]
fn an_opcode_fetch_is_four_t_states_and_the_cycles_after_it_are_three() {
let fetch = at(InstructionSet::Base, 0x00, 1, 0);
assert_eq!(fetch.t_states, 4);
assert_eq!(fetch.machine_cycles, 1);
let later = at(InstructionSet::Base, 0x01, 2, 0);
assert_eq!(later.t_states, 3);
}
#[test]
fn unprefixed_instructions_take_the_documented_number_of_t_states() {
let cases = [
(0x00u8, 4u32),
(0x41, 4),
(0x06, 7),
(0x46, 7),
(0x70, 7),
(0x36, 10),
(0x0A, 7),
(0x1A, 7),
(0x3A, 13),
(0x02, 7),
(0x12, 7),
(0x32, 13),
(0x01, 10),
(0x2A, 16),
(0x22, 16),
(0xF9, 6),
(0xC5, 11),
(0xC1, 10),
(0xEB, 4),
(0x08, 4),
(0xD9, 4),
(0xE3, 19),
(0x80, 4),
(0x86, 7),
(0xC6, 7),
(0x04, 4),
(0x34, 11),
(0x05, 4),
(0x35, 11),
(0x27, 4),
(0x2F, 4),
(0x3F, 4),
(0x37, 4),
(0x76, 4),
(0xF3, 4),
(0xFB, 4),
(0x09, 11),
(0x03, 6),
(0x0B, 6),
(0x07, 4),
(0xC3, 10),
(0x18, 12),
(0xE9, 4),
(0x10, 13),
(0xCD, 17),
(0xC9, 10),
(0xC7, 11),
];
for (ir, expected) in cases {
assert_eq!(timing(InstructionSet::Base, ir, 0), expected, "{ir:#04X}");
}
}
#[test]
fn a_taken_branch_costs_more_than_one_that_is_not() {
assert_eq!(timing(InstructionSet::Base, 0x20, 0), 12);
assert_eq!(timing(InstructionSet::Base, 0x20, Z_MASK), 7);
assert_eq!(timing(InstructionSet::Base, 0x28, Z_MASK), 12);
assert_eq!(timing(InstructionSet::Base, 0x28, 0), 7);
assert_eq!(timing(InstructionSet::Base, 0x30, 0), 12);
assert_eq!(timing(InstructionSet::Base, 0x30, C_MASK), 7);
assert_eq!(timing(InstructionSet::Base, 0x38, C_MASK), 12);
assert_eq!(timing(InstructionSet::Base, 0x38, 0), 7);
}
#[test]
fn a_conditional_jump_costs_the_same_either_way() {
assert_eq!(timing(InstructionSet::Base, 0xC2, 0), 10);
assert_eq!(timing(InstructionSet::Base, 0xC2, Z_MASK), 10);
}
#[test]
fn a_conditional_call_and_return_cost_more_when_taken() {
assert_eq!(timing(InstructionSet::Base, 0xC4, 0), 17);
assert_eq!(timing(InstructionSet::Base, 0xC4, Z_MASK), 10);
assert_eq!(timing(InstructionSet::Base, 0xC0, 0), 11);
assert_eq!(timing(InstructionSet::Base, 0xC0, Z_MASK), 5);
}
#[test]
fn every_condition_code_selects_the_flag_it_names() {
assert!(at(InstructionSet::Base, 0xC2, 3, 0).jump());
assert!(!at(InstructionSet::Base, 0xC2, 3, Z_MASK).jump());
assert!(at(InstructionSet::Base, 0xCA, 3, Z_MASK).jump());
assert!(at(InstructionSet::Base, 0xD2, 3, 0).jump());
assert!(at(InstructionSet::Base, 0xDA, 3, C_MASK).jump());
assert!(at(InstructionSet::Base, 0xF2, 3, 0).jump());
assert!(at(InstructionSet::Base, 0xFA, 3, S_MASK).jump());
}
#[test]
fn prefixed_bit_instructions_take_the_documented_number_of_t_states() {
let cases = [
(0x00u8, 8u32),
(0x06, 15),
(0x40, 8),
(0x46, 12),
(0xC0, 8),
(0xC6, 15),
(0x80, 8),
(0x86, 15),
];
for (ir, expected) in cases {
assert_eq!(timing(InstructionSet::Cb, ir, 0), expected, "{ir:#04X}");
}
}
#[test]
fn extended_instructions_take_the_documented_number_of_t_states() {
let cases = [
(0x57u8, 9u32),
(0x5F, 9),
(0x47, 9),
(0x4F, 9),
(0x44, 8),
(0x4B, 20),
(0x43, 20),
(0x4A, 15),
(0x42, 15),
(0x6F, 18),
(0x67, 18),
(0x4D, 14),
(0x45, 14),
(0x46, 8),
(0x56, 8),
(0x5E, 8),
];
for (ir, expected) in cases {
assert_eq!(timing(InstructionSet::Ed, ir, 0), expected, "{ir:#04X}");
}
}
#[test]
fn the_repeating_block_instructions_end_with_a_five_t_state_cycle() {
for ir in [0xA0u8, 0xA8, 0xB0, 0xB8, 0xA1, 0xA9, 0xB1, 0xB9] {
let decoded = at(InstructionSet::Ed, ir, 4, 0);
assert_eq!(decoded.machine_cycles, 4, "{ir:#04X}");
assert_eq!(decoded.t_states, 5, "{ir:#04X}");
assert!(decoded.no_read(), "{ir:#04X}");
}
}
#[test]
fn a_block_transfer_moves_a_byte_and_steps_the_pairs() {
let load = at(InstructionSet::Ed, 0xA0, 1, 0);
assert_eq!(load.set_addr_to, AddressSource::IndexOrHl);
assert_eq!(load.inc_dec_16, 0b1100);
let forward = at(InstructionSet::Ed, 0xA0, 2, 0);
assert_eq!(forward.set_addr_to, AddressSource::De);
assert_eq!(forward.inc_dec_16, 0b0110);
let backward = at(InstructionSet::Ed, 0xA8, 2, 0);
assert_eq!(backward.inc_dec_16, 0b1110);
let store = at(InstructionSet::Ed, 0xA0, 3, 0);
assert!(store.write());
assert!(store.i_bt());
assert_eq!(store.inc_dec_16, 0b0101);
}
#[test]
fn a_block_compare_reports_through_the_flags_without_writing() {
let compare = at(InstructionSet::Ed, 0xA1, 2, 0);
assert_eq!(compare.alu_op, AluOp::Cp);
assert!(compare.save_alu());
assert!(compare.preserve_c());
assert!(!compare.write());
assert!(at(InstructionSet::Ed, 0xA1, 3, 0).i_bc());
}
#[test]
fn the_interrupt_register_transfers_are_decoded_apart_from_other_loads() {
let cases = [
(0x57u8, SpecialLoad::AccumulatorFromVector),
(0x5F, SpecialLoad::AccumulatorFromRefresh),
(0x47, SpecialLoad::VectorFromAccumulator),
(0x4F, SpecialLoad::RefreshFromAccumulator),
];
for (ir, expected) in cases {
assert_eq!(at(InstructionSet::Ed, ir, 1, 0).special_ld, expected);
}
}
#[test]
fn the_interrupt_modes_are_selected_by_their_opcodes() {
for ir in [0x46u8, 0x4E, 0x66, 0x6E] {
assert_eq!(
at(InstructionSet::Ed, ir, 1, 0).interrupt_mode,
Some(InterruptMode::Mode0)
);
}
for ir in [0x56u8, 0x76] {
assert_eq!(
at(InstructionSet::Ed, ir, 1, 0).interrupt_mode,
Some(InterruptMode::Mode1)
);
}
for ir in [0x5Eu8, 0x7E] {
assert_eq!(
at(InstructionSet::Ed, ir, 1, 0).interrupt_mode,
Some(InterruptMode::Mode2)
);
}
assert_eq!(at(InstructionSet::Base, 0x00, 1, 0).interrupt_mode, None);
}
#[test]
fn each_prefix_selects_the_table_the_next_opcode_is_read_from() {
assert_eq!(
at(InstructionSet::Base, 0xCB, 1, 0).prefix,
InstructionSet::Cb
);
assert_eq!(
at(InstructionSet::Base, 0xED, 1, 0).prefix,
InstructionSet::Ed
);
assert_eq!(
at(InstructionSet::Base, 0xDD, 1, 0).prefix,
InstructionSet::DdFd
);
assert_eq!(
at(InstructionSet::Base, 0xFD, 1, 0).prefix,
InstructionSet::DdFd
);
assert_eq!(
at(InstructionSet::Base, 0x00, 1, 0).prefix,
InstructionSet::Base
);
}
#[test]
fn the_displacement_cycle_advances_the_program_counter_and_the_one_after_it_takes_five_t_states() {
let displacement = at(InstructionSet::Base, 0x46, 6, 0);
assert!(displacement.inc_pc());
assert!(displacement.no_pc());
let addition = at(InstructionSet::Base, 0x46, 7, 0);
assert_eq!(addition.t_states, 5);
assert_eq!(addition.set_addr_to, AddressSource::IndexOrHl);
assert!(addition.no_read());
}
#[test]
fn a_displaced_immediate_store_keeps_reading_rather_than_holding_the_program_counter() {
let displacement = at(InstructionSet::Base, 0x36, 6, 0);
assert!(displacement.inc_pc());
assert!(!displacement.no_pc());
assert_eq!(displacement.set_addr_to, AddressSource::None);
let addition = at(InstructionSet::Base, 0x36, 7, 0);
assert!(addition.inc_pc());
assert!(!addition.no_read());
}
#[test]
fn a_displaced_bit_instruction_leaves_the_address_alone_across_both_index_cycles() {
let displacement = at(InstructionSet::Cb, 0x06, 6, 0);
assert!(!displacement.no_pc());
let addition = at(InstructionSet::Cb, 0x06, 7, 0);
assert_eq!(addition.set_addr_to, AddressSource::IndexOrHl);
assert!(addition.inc_pc());
}
#[test]
fn a_displaced_bit_instruction_reports_its_undocumented_flags_from_the_address() {
let indexed = Context {
index_state: IndexState::Ix,
..context(InstructionSet::Cb, 0x00, MachineCycle::M2, 0)
};
let decoded = decode(indexed);
assert!(decoded.xy_bit_undoc());
assert_eq!(decoded.machine_cycles, 3);
assert_eq!(decoded.alu_op, AluOp::Rotate);
assert!(decoded.save_alu());
assert!(!at(InstructionSet::Cb, 0x00, 1, 0).xy_bit_undoc());
}
#[test]
fn the_arithmetic_operation_defaults_to_the_field_the_opcode_carries() {
let cases = [
(0x80u8, AluOp::Add),
(0x88, AluOp::Adc),
(0x90, AluOp::Sub),
(0x98, AluOp::Sbc),
(0xA0, AluOp::And),
(0xA8, AluOp::Xor),
(0xB0, AluOp::Or),
(0xB8, AluOp::Cp),
];
for (ir, expected) in cases {
let decoded = at(InstructionSet::Base, ir, 1, 0);
assert_eq!(decoded.alu_op, expected, "{ir:#04X}");
assert!(decoded.save_alu());
assert_eq!(decoded.set_bus_a_to, 0b111);
}
}
#[test]
fn increment_and_decrement_hold_the_carry_flag() {
let increment = at(InstructionSet::Base, 0x04, 1, 0);
assert_eq!(increment.alu_op, AluOp::Add);
assert!(increment.preserve_c());
assert_eq!(increment.set_bus_b_to, 0b1010);
let decrement = at(InstructionSet::Base, 0x05, 1, 0);
assert_eq!(decrement.alu_op, AluOp::Sub);
assert!(decrement.preserve_c());
}
#[test]
fn a_register_to_register_load_routes_the_operand_bus_to_the_write_back() {
let decoded = at(InstructionSet::Base, 0x41, 1, 0);
assert!(decoded.exchange_rp());
assert!(decoded.read_to_reg());
assert_eq!(decoded.set_bus_a_to, 0b000);
assert_eq!(decoded.set_bus_b_to, 0b001);
}
#[test]
fn the_opcode_that_would_be_a_load_from_itself_halts_instead() {
assert!(at(InstructionSet::Base, 0x76, 1, 0).halt());
assert!(!at(InstructionSet::Base, 0x76, 1, 0).read_to_reg());
}
#[test]
fn a_sixteen_bit_add_holds_the_sign_zero_and_parity_flags_across_both_passes() {
let low = at(InstructionSet::Base, 0x09, 2, 0);
assert!(low.arith16());
assert_eq!(low.alu_op, AluOp::Add);
let high = at(InstructionSet::Base, 0x09, 3, 0);
assert!(high.arith16());
assert_eq!(high.alu_op, AluOp::Adc);
}
#[test]
fn a_sixteen_bit_add_with_carry_reports_the_result_flags() {
for cycle in 2..=3 {
let decoded = at(InstructionSet::Ed, 0x4A, cycle, 0);
assert!(!decoded.arith16());
assert_eq!(decoded.alu_op, AluOp::Adc);
}
for cycle in 2..=3 {
assert_eq!(at(InstructionSet::Ed, 0x42, cycle, 0).alu_op, AluOp::Sbc);
}
}
#[test]
fn a_port_cycle_is_requested_only_where_the_instruction_reaches_a_port() {
assert!(at(InstructionSet::Base, 0xDB, 3, 0).iorq());
assert!(at(InstructionSet::Base, 0xD3, 3, 0).iorq());
assert!(at(InstructionSet::Ed, 0x40, 2, 0).iorq());
assert!(at(InstructionSet::Ed, 0x41, 2, 0).iorq());
assert!(!at(InstructionSet::Base, 0x46, 2, 0).iorq());
}
#[test]
fn reading_a_port_into_the_unnameable_register_still_reports_through_the_flags() {
let discarded = at(InstructionSet::Ed, 0x70, 2, 0);
assert!(!discarded.read_to_reg());
assert!(discarded.i_inrc());
let kept = at(InstructionSet::Ed, 0x40, 2, 0);
assert!(kept.read_to_reg());
assert!(kept.i_inrc());
}
#[test]
fn writing_a_port_from_the_unnameable_register_drives_a_constant() {
assert_eq!(at(InstructionSet::Ed, 0x71, 1, 0).set_bus_b_to, 0b1110);
assert_eq!(at(InstructionSet::Ed, 0x41, 1, 0).set_bus_b_to, 0b000);
}
#[test]
fn the_extended_instructions_are_inert_when_they_are_not_enabled() {
let disabled = [
0x23u8, 0x24, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36,
0x8A, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x98, 0xA4, 0xA5, 0xAC, 0xB4, 0xB6, 0xB7, 0xBC,
];
for ir in disabled {
let decoded = at(InstructionSet::Ed, ir, 1, 0);
assert_eq!(decoded.machine_cycles, 1, "{ir:#04X}");
assert_eq!(decoded.t_states, 4, "{ir:#04X}");
assert_eq!(decoded.z80n_command, None, "{ir:#04X}");
assert!(!decoded.write(), "{ir:#04X}");
assert!(!decoded.read_to_reg(), "{ir:#04X}");
assert!(!decoded.save_alu(), "{ir:#04X}");
assert_eq!(timing(InstructionSet::Ed, ir, 0), 8, "{ir:#04X}");
}
}
#[test]
fn the_extended_instructions_decode_once_they_are_enabled() {
let enabled = Context {
z80n_enabled: true,
..context(InstructionSet::Ed, 0x23, MachineCycle::M1, 0)
};
let decoded = decode(enabled);
assert_eq!(decoded.z80n_command, Some(Z80nCommand::SwapNibbleA));
assert_eq!(decoded.t_states, 4);
let wide = Context {
z80n_enabled: true,
ir: 0x91,
..context(InstructionSet::Ed, 0x91, MachineCycle::M1, 0)
};
assert_eq!(decode(wide).machine_cycles, 5);
}
#[test]
fn an_unassigned_extended_opcode_is_a_two_cycle_no_operation() {
for ir in [0x00u8, 0x7F, 0xC0, 0xFF] {
let decoded = at(InstructionSet::Ed, ir, 1, 0);
assert_eq!(decoded.machine_cycles, 1, "{ir:#04X}");
assert_eq!(timing(InstructionSet::Ed, ir, 0), 8, "{ir:#04X}");
}
}
#[test]
fn accepting_a_non_maskable_interrupt_pushes_the_return_address() {
let accepting = Context {
nmi_cycle: true,
..context(InstructionSet::Base, 0x00, MachineCycle::M1, 0)
};
let decoded = decode(accepting);
assert_eq!(decoded.machine_cycles, 3);
assert_eq!(decoded.t_states, 5);
assert_eq!(decoded.inc_dec_16, 0b1111);
assert_eq!(decoded.set_addr_to, AddressSource::Sp);
}
#[test]
fn accepting_a_maskable_interrupt_in_the_vectored_mode_reads_the_handler_address() {
let accepting = Context {
interrupt_cycle: true,
..context(InstructionSet::Base, 0x00, MachineCycle::M1, 0)
};
assert_eq!(decode(accepting).machine_cycles, 5);
let vector = Context {
interrupt_cycle: true,
..context(InstructionSet::Base, 0x00, MachineCycle::M4, 0)
};
assert!(decode(vector).ldz());
let jump = Context {
interrupt_cycle: true,
..context(InstructionSet::Base, 0x00, MachineCycle::M5, 0)
};
assert!(decode(jump).jump());
}
#[test]
fn a_plain_opcode_of_zero_does_nothing_at_all() {
let decoded = at(InstructionSet::Base, 0x00, 1, 0);
assert_eq!(decoded.machine_cycles, 1);
assert!(!decoded.write());
assert!(!decoded.read_to_reg());
assert!(!decoded.save_alu());
assert!(!decoded.jump());
assert_eq!(decoded.set_addr_to, AddressSource::None);
}
#[test]
fn the_stack_pointer_moves_the_right_way_for_a_push_and_a_pop() {
assert_eq!(at(InstructionSet::Base, 0xC5, 1, 0).inc_dec_16, 0b1111);
assert_eq!(at(InstructionSet::Base, 0xC1, 2, 0).inc_dec_16, 0b0111);
assert_eq!(at(InstructionSet::Base, 0x03, 1, 0).inc_dec_16, 0b0100);
assert_eq!(at(InstructionSet::Base, 0x33, 1, 0).inc_dec_16, 0b0111);
assert_eq!(at(InstructionSet::Base, 0x0B, 1, 0).inc_dec_16, 0b1100);
assert_eq!(at(InstructionSet::Base, 0x3B, 1, 0).inc_dec_16, 0b1111);
}
#[test]
fn the_accumulator_and_flags_are_pushed_from_a_different_pair_than_the_others() {
assert_eq!(at(InstructionSet::Base, 0xF5, 1, 0).set_bus_b_to, 0b0111);
assert_eq!(at(InstructionSet::Base, 0xF5, 2, 0).set_bus_b_to, 0b1011);
assert_eq!(at(InstructionSet::Base, 0xC5, 1, 0).set_bus_b_to, 0b0000);
assert_eq!(at(InstructionSet::Base, 0xC5, 2, 0).set_bus_b_to, 0b0001);
}
#[test]
fn the_exchanges_are_each_decoded_to_their_own_signal() {
assert!(at(InstructionSet::Base, 0xEB, 1, 0).exchange_dh());
assert!(at(InstructionSet::Base, 0x08, 1, 0).exchange_af());
assert!(at(InstructionSet::Base, 0xD9, 1, 0).exchange_rs());
assert!(at(InstructionSet::Base, 0xE3, 5, 0).exchange_wh());
}
#[test]
fn a_return_from_an_interrupt_restores_the_saved_enable() {
assert!(at(InstructionSet::Ed, 0x4D, 3, 0).i_retn());
for ir in [0x45u8, 0x55, 0x5D, 0x65, 0x6D, 0x75, 0x7D] {
assert!(at(InstructionSet::Ed, ir, 3, 0).i_retn(), "{ir:#04X}");
}
assert!(!at(InstructionSet::Base, 0xC9, 3, 0).i_retn());
}
#[test]
fn the_nibble_rotates_read_their_operand_before_the_cycle_that_writes_it() {
let read = at(InstructionSet::Ed, 0x6F, 2, 0);
assert_eq!(read.alu_op, AluOp::Rld);
assert!(read.save_alu());
assert!(!read.write());
let hold = at(InstructionSet::Ed, 0x6F, 3, 0);
assert!(hold.i_rld());
assert!(hold.no_read());
assert_eq!(hold.t_states, 4);
assert!(at(InstructionSet::Ed, 0x6F, 4, 0).write());
assert!(at(InstructionSet::Ed, 0x67, 3, 0).i_rrd());
}