use super::*;
#[test]
fn the_extended_nibble_swap_and_mirror_reach_the_accumulator() {
let (cpu, t) = extended(&[0xED, 0x23], |cpu| cpu.registers_mut().af = 0x12FF);
assert_eq!(t, 8);
assert_eq!(cpu.accumulator(), 0x21);
let (cpu, _) = extended(&[0xED, 0x24], |cpu| cpu.registers_mut().af = 0x80FF);
assert_eq!(cpu.accumulator(), 0x01);
}
#[test]
fn the_extended_multiply_fills_the_pair_with_the_product_of_its_halves() {
let (cpu, t) = extended(&[0xED, 0x30], |cpu| cpu.registers_mut().de = 0x0304);
assert_eq!(t, 8);
assert_eq!(cpu.registers().de, 12);
}
#[test]
fn the_extended_accumulator_adds_reach_each_pair_and_clear_the_carry() {
let (cpu, t) = extended(&[0xED, 0x31], |cpu| {
cpu.registers_mut().hl = 0xFFFF;
cpu.registers_mut().af = 0x02FF;
});
assert_eq!(t, 8);
assert_eq!(cpu.registers().hl, 0x0001);
assert_eq!(cpu.flags() & crate::flag::C_MASK, 0);
let (cpu, _) = extended(&[0xED, 0x32], |cpu| {
cpu.registers_mut().de = 0x1000;
cpu.registers_mut().af = 0x3400;
});
assert_eq!(cpu.registers().de, 0x1034);
let (cpu, _) = extended(&[0xED, 0x33], |cpu| {
cpu.registers_mut().bc = 0x1000;
cpu.registers_mut().af = 0x3400;
});
assert_eq!(cpu.registers().bc, 0x1034);
}
#[test]
fn the_extended_immediate_adds_take_the_two_bytes_that_follow() {
let (cpu, t) = extended(&[0xED, 0x34, 0x34, 0x12], |cpu| {
cpu.registers_mut().hl = 0x1111;
});
assert_eq!(t, 16);
assert_eq!(cpu.registers().hl, 0x2345);
assert_eq!(cpu.registers().pc, 0x0004);
let (cpu, _) = extended(&[0xED, 0x35, 0x01, 0x00], |cpu| {
cpu.registers_mut().de = 0xFFFF;
});
assert_eq!(cpu.registers().de, 0x0000);
let (cpu, _) = extended(&[0xED, 0x36, 0x10, 0x00], |cpu| {
cpu.registers_mut().bc = 0x0020;
});
assert_eq!(cpu.registers().bc, 0x0030);
}
#[test]
fn the_extended_bit_select_reads_the_low_three_bits_of_the_pair() {
let (cpu, t) = extended(&[0xED, 0x95], |cpu| cpu.registers_mut().de = 0x0000);
assert_eq!(t, 8);
assert_eq!(cpu.accumulator(), 0x80);
let (cpu, _) = extended(&[0xED, 0x95], |cpu| cpu.registers_mut().de = 0x0007);
assert_eq!(cpu.accumulator(), 0x01);
}
#[test]
fn the_extended_screen_helpers_reach_the_pair_they_write() {
let (cpu, t) = extended(&[0xED, 0x94], |cpu| cpu.registers_mut().de = 0x0000);
assert_eq!(t, 8);
assert_eq!(cpu.registers().hl, 0x4000);
let (cpu, _) = extended(&[0xED, 0x93], |cpu| cpu.registers_mut().hl = 0x4000);
assert_eq!(cpu.registers().hl, 0x4100);
}
#[test]
fn the_extended_barrel_shifts_step_the_pair_by_the_counter() {
let cases = [
(0x28u8, 0x1234u16, 0x2340u16),
(0x29, 0x8000, 0xF800),
(0x2A, 0x8000, 0x0800),
(0x2B, 0x0000, 0xF000),
(0x2C, 0x1234, 0x2341),
];
for (opcode, start, expected) in cases {
let (cpu, t) = extended(&[0xED, opcode], |cpu| {
cpu.registers_mut().de = start;
cpu.registers_mut().bc = 0x0400;
});
assert_eq!(t, 8, "{opcode:#04X}");
assert_eq!(cpu.registers().de, expected, "{opcode:#04X}");
}
}
#[test]
fn the_extended_test_instruction_reports_flags_without_writing_the_accumulator() {
let (cpu, t) = extended(&[0xED, 0x27, 0x0F], |cpu| cpu.registers_mut().af = 0xF000);
assert_eq!(t, 11);
assert_eq!(cpu.accumulator(), 0xF0);
assert_eq!(cpu.flags() & crate::flag::Z_MASK, crate::flag::Z_MASK);
assert_eq!(cpu.flags() & crate::flag::H_MASK, crate::flag::H_MASK);
let (cpu, _) = extended(&[0xED, 0x27, 0xFF], |cpu| cpu.registers_mut().af = 0x8000);
assert_eq!(cpu.accumulator(), 0x80);
assert_eq!(cpu.flags() & crate::flag::Z_MASK, 0);
assert_eq!(cpu.flags() & crate::flag::S_MASK, crate::flag::S_MASK);
}
#[test]
fn the_extended_operations_are_inert_when_the_set_is_disabled() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().de = 0x0304;
let mut ram = Ram::with(&[0xED, 0x30]);
let t = cpu.step_by_edges(&mut ram);
assert_eq!(t, 8);
assert_eq!(cpu.registers().de, 0x0304);
assert_eq!(cpu.registers().pc, 0x0002);
}
#[test]
fn the_extended_jump_through_a_port_replaces_the_middle_of_the_counter() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().bc = 0x1234;
let mut ram = Ram::with(&[0xED, 0x98]);
ram.ports[0x1234] = 0xFF;
let t = cpu.step_by_edges(&mut ram);
assert_eq!(t, 13);
assert_eq!(cpu.registers().pc, 0x3FC0);
}
#[test]
fn the_extended_single_port_output_moves_a_byte_without_counting_down() {
let mut program = vec![0xED, 0x90];
program.resize(0x40, 0);
program[0x30] = 0x5A;
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().bc = 0x4055;
let mut ram = Ram::with(&program);
let t = cpu.step_by_edges(&mut ram);
assert_eq!(t, 16);
assert_eq!(ram.ports[0x4055], 0x5A);
assert_eq!(cpu.registers().hl, 0x0031);
assert_eq!(high_byte(cpu.registers().bc), 0x40);
}
#[test]
fn the_extended_copy_skips_a_byte_that_matches_the_accumulator() {
let (cpu, ram, t) = extended_block(
&pattern(0xA4),
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0038;
cpu.registers_mut().bc = 0x0002;
cpu.registers_mut().af = 0x0000;
},
1,
);
assert_eq!(t, 16);
assert_eq!(ram.bytes[0x0038], 0x11);
assert_eq!(cpu.registers().hl, 0x0031);
assert_eq!(cpu.registers().de, 0x0039);
assert_eq!(cpu.registers().bc, 0x0001);
let (_, ram, _) = extended_block(
&pattern(0xA4),
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0038;
cpu.registers_mut().bc = 0x0002;
cpu.registers_mut().af = 0x1100;
},
1,
);
assert_eq!(ram.bytes[0x0038], 0x00);
}
#[test]
fn the_extended_copy_downwards_steps_the_source_back_and_the_target_on() {
let (cpu, ram, t) = extended_block(
&pattern(0xAC),
|cpu| {
cpu.registers_mut().hl = 0x0031;
cpu.registers_mut().de = 0x0038;
cpu.registers_mut().bc = 0x0002;
cpu.registers_mut().af = 0x0000;
},
1,
);
assert_eq!(t, 16);
assert_eq!(ram.bytes[0x0038], 0x22);
assert_eq!(cpu.registers().hl, 0x0030);
assert_eq!(cpu.registers().de, 0x0039);
assert_eq!(cpu.registers().bc, 0x0001);
}
#[test]
fn the_extended_repeating_copies_run_until_the_counter_empties() {
let (cpu, ram, t) = extended_block(
&pattern(0xB4),
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0038;
cpu.registers_mut().bc = 0x0002;
cpu.registers_mut().af = 0x0000;
},
2,
);
assert_eq!(t, 21 + 16);
assert_eq!(ram.bytes[0x0038], 0x11);
assert_eq!(ram.bytes[0x0039], 0x22);
assert_eq!(cpu.registers().bc, 0x0000);
assert_eq!(cpu.registers().pc, 0x0002);
}
#[test]
fn the_extended_word_copy_steps_one_half_of_each_pair() {
let (cpu, ram, t) = extended_block(
&pattern(0xA5),
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0038;
},
1,
);
assert_eq!(t, 14);
assert_eq!(ram.bytes[0x0038], 0x11);
assert_eq!(cpu.registers().hl, 0x0031);
assert_eq!(cpu.registers().de, 0x0138);
}
#[test]
fn the_extended_pattern_copy_reads_through_a_pattern_and_writes_to_the_target() {
let mut program = vec![0xED, 0xB7, 0x76];
program.resize(0x80, 0);
for offset in 0..8 {
program[0x30 + offset] = 0xA0 + u8::try_from(offset).unwrap();
}
let (cpu, ram, t) = extended_block(
&program,
|cpu| {
cpu.registers_mut().hl = 0x0034;
cpu.registers_mut().de = 0x0042;
cpu.registers_mut().bc = 0x0001;
cpu.registers_mut().af = 0x0000;
},
1,
);
assert_eq!(t, 16);
assert_eq!(ram.bytes[0x0042], 0xA2);
assert_eq!(cpu.registers().de, 0x0043);
assert_eq!(cpu.registers().hl, 0x0034);
assert_eq!(cpu.registers().bc, 0x0000);
}
#[test]
fn the_extended_pattern_copy_skips_a_byte_that_matches_the_accumulator() {
let mut program = vec![0xED, 0xB7, 0x76];
program.resize(0x80, 0);
program[0x30] = 0x5A;
let (_, ram, _) = extended_block(
&program,
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0040;
cpu.registers_mut().bc = 0x0001;
cpu.registers_mut().af = 0x5A00;
},
1,
);
assert_eq!(ram.bytes[0x0040], 0x00);
}
#[test]
fn the_extended_scaling_copy_moves_a_byte_and_steps_both_pointers_on() {
let (cpu, ram, t) = extended_block(
&pattern(0xB6),
|cpu| {
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0038;
cpu.registers_mut().bc = 0x0001;
cpu.registers_mut().af = 0x0000;
},
1,
);
assert_eq!(t, 16);
assert_eq!(ram.bytes[0x0038], 0x11);
assert_eq!(cpu.registers().hl, 0x0031);
assert_eq!(cpu.registers().de, 0x0039);
assert_eq!(cpu.registers().bc, 0x0000);
}
#[test]
fn writing_an_extended_register_reaches_the_host_once_with_both_operand_bytes() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
let mut ram = Ram::with(&[0xED, 0x91, 0x07, 0x5A]);
cpu.step_by_edges(&mut ram);
assert_eq!(
ram.commands,
vec![(crate::Z80nCommand::NextRegWrite, 0x075A)]
);
assert_eq!(cpu.registers().pc, 0x0004);
}
#[test]
fn writing_an_extended_register_from_the_accumulator_takes_it_as_the_low_half() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().af = 0x2200;
let mut ram = Ram::with(&[0xED, 0x92, 0x33]);
cpu.step_by_edges(&mut ram);
assert_eq!(
ram.commands,
vec![(crate::Z80nCommand::NextRegWrite, 0x3322)]
);
}
#[test]
fn an_operation_completed_inside_the_processor_still_reports_itself_once() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().de = 0x0304;
let mut ram = Ram::with(&[0xED, 0x30, 0xED, 0x30]);
cpu.step_by_edges(&mut ram);
assert_eq!(ram.commands, vec![(crate::Z80nCommand::MulDe, 0)]);
cpu.step_by_edges(&mut ram);
assert_eq!(ram.commands.len(), 2);
assert_eq!(ram.commands[1].0, crate::Z80nCommand::MulDe);
}
#[test]
fn a_block_instruction_reports_itself_once_rather_than_once_a_cycle() {
let mut program = vec![0xED, 0xB7, 0x76];
program.resize(0x80, 0);
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().hl = 0x0030;
cpu.registers_mut().de = 0x0040;
cpu.registers_mut().bc = 0x0001;
let mut ram = Ram::with(&program);
cpu.step_by_edges(&mut ram);
assert_eq!(ram.commands, vec![(crate::Z80nCommand::LdPirx, 0)]);
}
#[test]
fn a_return_from_a_non_maskable_interrupt_reports_both_halves_in_order() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().sp = 0x0040;
let mut ram = Ram::with(&[0xED, 0x45]);
cpu.step_by_edges(&mut ram);
assert_eq!(
ram.commands
.iter()
.map(|(command, _)| *command)
.collect::<Vec<_>>(),
vec![
crate::Z80nCommand::ReturnFromNmiLow,
crate::Z80nCommand::ReturnFromNmiHigh
]
);
}
#[test]
fn nothing_reaches_the_host_when_the_extended_set_is_disabled() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.registers_mut().de = 0x0304;
let mut ram = Ram::with(&[0xED, 0x30, 0xED, 0x91, 0x07, 0x5A]);
cpu.step_by_edges(&mut ram);
cpu.step_by_edges(&mut ram);
assert!(ram.commands.is_empty());
}
#[test]
fn the_extended_immediate_push_stores_its_operand_high_byte_first() {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
cpu.registers_mut().sp = 0x0040;
let mut ram = Ram::with(&[0xED, 0x8A, 0x12, 0x34]);
let t = cpu.step_by_edges(&mut ram);
assert_eq!(cpu.registers().sp, 0x003E);
assert_eq!(cpu.registers().pc, 0x0004);
assert_eq!(t, 23);
assert_eq!(ram.bytes[0x003E], 0x34);
assert_eq!(ram.bytes[0x003F], 0x12);
assert_eq!(ram.commands, vec![]);
}
#[test]
fn the_extended_operations_leave_the_flags_alone() {
let untouched = [
0x23u8, 0x24, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x30, 0x93, 0x94, 0x95, 0x98, 0x8A,
];
for opcode in untouched {
let (cpu, _, _) = loaded_extended(opcode, &[0x00, 0x00]);
assert_eq!(cpu.flags(), 0x5A, "{opcode:#04X}");
}
for opcode in [0x34u8, 0x35, 0x36] {
let (cpu, _, _) = loaded_extended(opcode, &[0x01, 0x00]);
assert_eq!(cpu.flags(), 0x5A, "{opcode:#04X}");
}
for opcode in [0x91u8, 0x92] {
let (cpu, _, _) = loaded_extended(opcode, &[0x07, 0x5A]);
assert_eq!(cpu.flags(), 0x5A, "{opcode:#04X}");
}
}
#[test]
fn the_extended_accumulator_adds_touch_only_the_carry() {
for opcode in [0x31u8, 0x32, 0x33] {
let (cpu, _, _) = loaded_extended(opcode, &[]);
assert_eq!(cpu.flags(), 0x5A & !crate::flag::C_MASK, "{opcode:#04X}");
}
}
#[test]
fn the_extended_operations_that_do_report_flags_change_them() {
let (cpu, _, _) = loaded_extended(0x27, &[0xFF]);
assert_ne!(cpu.flags(), 0x5A);
let (cpu, _, _) = loaded_extended(0xA5, &[]);
assert_ne!(cpu.flags(), 0x5A);
}
#[test]
fn every_extended_operation_takes_the_time_it_should_with_a_counter_left_to_run() {
let cases = [
(0x23u8, 8u32),
(0x24, 8),
(0x27, 11),
(0x28, 8),
(0x29, 8),
(0x2A, 8),
(0x2B, 8),
(0x2C, 8),
(0x30, 8),
(0x31, 8),
(0x32, 8),
(0x33, 8),
(0x34, 16),
(0x35, 16),
(0x36, 16),
(0x8A, 23),
(0x90, 16),
(0x91, 20),
(0x92, 17),
(0x93, 8),
(0x94, 8),
(0x95, 8),
(0x98, 13),
(0xA4, 16),
(0xA5, 14),
(0xAC, 16),
(0xB4, 21),
(0xB6, 21),
(0xB7, 21),
(0xBC, 21),
];
for (opcode, expected) in cases {
let (_, _, t) = loaded_extended(opcode, &[0x01, 0x00]);
assert_eq!(t, expected, "{opcode:#04X}");
}
}
#[test]
fn every_extended_operation_is_a_two_cycle_no_operation_when_disabled() {
let opcodes = [
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 opcode in opcodes {
let mut cpu = Cpu::new();
cpu.reset();
{
let registers = cpu.registers_mut();
registers.af = 0x0A5A;
registers.bc = 0x0401;
registers.de = 0x0038;
registers.hl = 0x0030;
registers.sp = 0x0100;
}
let before = *cpu.registers();
let mut ram = Ram::with(&[0xED, opcode, 0x01, 0x00]);
let t = cpu.step_by_edges(&mut ram);
assert_eq!(t, 8, "{opcode:#04X}");
assert_eq!(cpu.registers().pc, 0x0002, "{opcode:#04X}");
assert_eq!(cpu.registers().af, before.af, "{opcode:#04X}");
assert_eq!(cpu.registers().bc, before.bc, "{opcode:#04X}");
assert_eq!(cpu.registers().de, before.de, "{opcode:#04X}");
assert_eq!(cpu.registers().hl, before.hl, "{opcode:#04X}");
assert_eq!(cpu.registers().sp, before.sp, "{opcode:#04X}");
assert!(ram.commands.is_empty(), "{opcode:#04X}");
assert!(ram.written.is_empty(), "{opcode:#04X}");
}
}
#[test]
fn a_repeating_extended_copy_costs_less_on_the_pass_that_empties_the_counter() {
for opcode in [0xB4u8, 0xB6, 0xB7, 0xBC] {
let mut program = vec![0xED, opcode];
program.resize(0x80, 0);
program[0x30] = 0x11;
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
{
let registers = cpu.registers_mut();
registers.af = 0x0A5A;
registers.bc = 0x0001;
registers.de = 0x0038;
registers.hl = 0x0030;
}
let mut ram = Ram::with(&program);
assert_eq!(cpu.step_by_edges(&mut ram), 16, "{opcode:#04X}");
assert_eq!(cpu.registers().bc, 0x0000, "{opcode:#04X}");
}
}
#[test]
fn the_extended_pattern_copy_writes_block_transfer_flags() {
let mut program = vec![0u8; 0x500];
program[0] = 0xED;
program[1] = 0xB7;
for (at, byte) in program.iter_mut().enumerate().skip(0x400).take(8) {
*byte = u8::try_from(at & 0xFF).unwrap_or_default();
}
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
{
let registers = cpu.registers_mut();
registers.af = 0x01FF;
registers.bc = 0x0001;
registers.de = 0x0301;
registers.hl = 0x0400;
registers.sp = 0x0200;
}
let mut ram = Ram::with(&program);
cpu.step_by_edges(&mut ram);
assert_eq!(cpu.flags(), 0xC1, "flags after the pattern copy");
}
#[test]
fn the_extended_single_port_output_leaves_the_flags_alone() {
let (cpu, _, _) = loaded_extended(0x90, &[]);
assert_eq!(cpu.flags(), 0x5A);
}
#[test]
fn the_extended_word_copy_computes_the_carry_rather_than_preserving_it() {
for (high, flags) in [(0x05u8, 0x00u8), (0x7F, 0x94), (0xFF, 0x51)] {
let mut program = vec![0u8; 0x100];
program[0] = 0xED;
program[1] = 0xA5;
program[0x40] = 0x99;
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
{
let registers = cpu.registers_mut();
registers.af = 0x2CFF;
registers.bc = 0x0002;
registers.de = u16::from(high) << 8;
registers.hl = 0x0040;
}
let mut ram = Ram::with(&program);
cpu.step_by_edges(&mut ram);
assert_eq!(cpu.flags(), flags, "destination high half {high:#04X}");
assert_eq!(high_byte(cpu.registers().de), high.wrapping_add(1));
}
}