use super::{AluOp, Inputs, execute};
use crate::consts::flag::{
C_MASK, H_MASK, N_MASK, P_MASK, S_MASK, X_MASK, XY_MASK, Y_MASK, Z_MASK,
};
use crate::mcode::InstructionSet;
use std::prelude::rust_2024::*;
fn operands(op: AluOp, bus_a: u8, bus_b: u8, flags: u8) -> Inputs {
Inputs {
op,
ir: 0,
instruction_set: InstructionSet::Base,
bus_a,
bus_b,
flags,
preserve_result_flags: false,
combine_zero: false,
}
}
fn run(op: AluOp, bus_a: u8, bus_b: u8, flags: u8) -> (u8, u8) {
execute(operands(op, bus_a, bus_b, flags))
}
fn shifted(op: AluOp, ir: u8, bus_a: u8, flags: u8) -> (u8, u8) {
execute(Inputs {
ir,
instruction_set: InstructionSet::Cb,
..operands(op, bus_a, 0, flags)
})
}
fn bit_op(op: AluOp, ir: u8, bus_b: u8, flags: u8) -> (u8, u8) {
execute(Inputs {
ir,
instruction_set: InstructionSet::Cb,
..operands(op, 0, bus_b, flags)
})
}
#[test]
fn add_carries_out_of_bit_three_into_the_half_carry() {
let (result, flags) = run(AluOp::Add, 0x0F, 0x01, 0);
assert_eq!(result, 0x10);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & (C_MASK | N_MASK | Z_MASK | S_MASK | P_MASK), 0);
assert_eq!(flags & XY_MASK, 0);
}
#[test]
fn add_carries_out_of_bit_seven_into_the_carry() {
let (result, flags) = run(AluOp::Add, 0xFF, 0x01, 0);
assert_eq!(result, 0x00);
assert_eq!(flags & C_MASK, C_MASK);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & (S_MASK | P_MASK | N_MASK), 0);
}
#[test]
fn add_reports_overflow_when_the_carries_around_the_sign_bit_differ() {
let (result, flags) = run(AluOp::Add, 0x7F, 0x01, 0);
assert_eq!(result, 0x80);
assert_eq!(flags & P_MASK, P_MASK);
assert_eq!(flags & S_MASK, S_MASK);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & C_MASK, 0);
}
#[test]
fn add_takes_the_undocumented_flags_from_the_result() {
let (result, flags) = run(AluOp::Add, 0x28, 0x00, 0);
assert_eq!(result, 0x28);
assert_eq!(flags & XY_MASK, XY_MASK);
}
#[test]
fn adc_includes_the_carry_flag() {
let (result, flags) = run(AluOp::Adc, 0x0F, 0x00, C_MASK);
assert_eq!(result, 0x10);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & C_MASK, 0);
}
#[test]
fn sub_reports_a_borrow_as_the_carry_flag() {
let (result, flags) = run(AluOp::Sub, 0x00, 0x01, 0);
assert_eq!(result, 0xFF);
assert_eq!(flags & C_MASK, C_MASK);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & N_MASK, N_MASK);
assert_eq!(flags & S_MASK, S_MASK);
assert_eq!(flags & (Z_MASK | P_MASK), 0);
}
#[test]
fn sub_without_a_borrow_leaves_the_carry_and_half_carry_clear() {
let (result, flags) = run(AluOp::Sub, 0x18, 0x01, 0);
assert_eq!(result, 0x17);
assert_eq!(flags & (C_MASK | H_MASK), 0);
assert_eq!(flags & N_MASK, N_MASK);
}
#[test]
fn sub_borrowing_out_of_bit_four_sets_the_half_carry() {
let (result, flags) = run(AluOp::Sub, 0x10, 0x01, 0);
assert_eq!(result, 0x0F);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & C_MASK, 0);
}
#[test]
fn sub_reports_overflow_across_the_sign_boundary() {
let (result, flags) = run(AluOp::Sub, 0x80, 0x01, 0);
assert_eq!(result, 0x7F);
assert_eq!(flags & P_MASK, P_MASK);
assert_eq!(flags & N_MASK, N_MASK);
assert_eq!(flags & (C_MASK | S_MASK), 0);
}
#[test]
fn sbc_includes_the_carry_flag() {
let (result, flags) = run(AluOp::Sbc, 0x00, 0x00, C_MASK);
assert_eq!(result, 0xFF);
assert_eq!(flags & C_MASK, C_MASK);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & N_MASK, N_MASK);
}
#[test]
fn sbc_of_equal_operands_without_carry_is_zero() {
let (result, flags) = run(AluOp::Sbc, 0x42, 0x42, 0);
assert_eq!(result, 0x00);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & (C_MASK | H_MASK | P_MASK), 0);
}
#[test]
fn cp_takes_the_undocumented_flags_from_the_operand_not_the_result() {
let (_, flags) = run(AluOp::Cp, 0x00, 0x28, 0);
assert_eq!(flags & XY_MASK, XY_MASK);
}
#[test]
fn sub_takes_the_undocumented_flags_from_the_result() {
let (result, flags) = run(AluOp::Sub, 0x00, 0x28, 0);
assert_eq!(result, 0xD8);
assert_eq!(flags & X_MASK, X_MASK);
assert_eq!(flags & Y_MASK, 0);
}
#[test]
fn cp_reports_the_same_flags_as_the_subtraction_it_discards() {
for a in 0..=255u8 {
for b in 0..=255u8 {
let (_, compare) = run(AluOp::Cp, a, b, 0);
let (_, subtract) = run(AluOp::Sub, a, b, 0);
assert_eq!(compare & !XY_MASK, subtract & !XY_MASK, "{a:#04X} {b:#04X}");
}
}
}
#[test]
fn and_sets_the_half_carry_and_reports_parity() {
let (result, flags) = run(AluOp::And, 0xFF, 0x0F, C_MASK | N_MASK);
assert_eq!(result, 0x0F);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & P_MASK, P_MASK);
assert_eq!(flags & (C_MASK | N_MASK), 0);
assert_eq!(flags & X_MASK, X_MASK);
assert_eq!(flags & Y_MASK, 0);
}
#[test]
fn xor_clears_the_half_carry() {
let (result, flags) = run(AluOp::Xor, 0xFF, 0xF0, H_MASK | C_MASK);
assert_eq!(result, 0x0F);
assert_eq!(flags & (H_MASK | C_MASK | N_MASK), 0);
assert_eq!(flags & P_MASK, P_MASK);
}
#[test]
fn or_reports_odd_parity_as_a_clear_flag() {
let (result, flags) = run(AluOp::Or, 0x01, 0x00, P_MASK);
assert_eq!(result, 0x01);
assert_eq!(flags & P_MASK, 0);
assert_eq!(flags & (H_MASK | C_MASK | N_MASK | Z_MASK | S_MASK), 0);
}
#[test]
fn or_of_zero_reports_zero_and_even_parity() {
let (result, flags) = run(AluOp::Or, 0x00, 0x00, 0);
assert_eq!(result, 0x00);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & P_MASK, P_MASK);
}
#[test]
fn preserving_the_result_flags_holds_sign_zero_and_parity() {
let held = S_MASK | Z_MASK | P_MASK;
let (result, flags) = execute(Inputs {
preserve_result_flags: true,
..operands(AluOp::Add, 0x0F, 0x01, held)
});
assert_eq!(result, 0x10);
assert_eq!(flags & held, held);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & (C_MASK | N_MASK), 0);
}
#[test]
fn preserving_the_result_flags_leaves_them_clear_when_they_started_clear() {
let (result, flags) = execute(Inputs {
preserve_result_flags: true,
..operands(AluOp::Add, 0x00, 0x00, 0)
});
assert_eq!(result, 0x00);
assert_eq!(flags & (S_MASK | Z_MASK | P_MASK), 0);
}
#[test]
fn combining_the_zero_flag_reports_zero_only_when_it_was_already_set() {
let (_, carried) = execute(Inputs {
combine_zero: true,
..operands(AluOp::Adc, 0x00, 0x00, Z_MASK)
});
assert_eq!(carried & Z_MASK, Z_MASK);
let (_, fresh) = execute(Inputs {
combine_zero: true,
..operands(AluOp::Adc, 0x00, 0x00, 0)
});
assert_eq!(fresh & Z_MASK, 0);
}
#[test]
fn combining_the_zero_flag_still_clears_it_for_a_nonzero_result() {
let (result, flags) = execute(Inputs {
combine_zero: true,
..operands(AluOp::Adc, 0x01, 0x00, Z_MASK)
});
assert_eq!(result, 0x01);
assert_eq!(flags & Z_MASK, 0);
}
fn reference_decimal_adjust(a: u8, carry: bool, half: bool, subtract: bool) -> (u8, u8) {
let low = a & 0x0F;
let mut correction = 0u8;
let mut carry_out = carry;
if half || low > 9 {
correction |= 0x06;
}
if carry || a > 0x99 {
correction |= 0x60;
carry_out = true;
}
let result = if subtract {
a.wrapping_sub(correction)
} else {
a.wrapping_add(correction)
};
let half_out = if subtract { half && low < 6 } else { low > 9 };
let mut flags = 0u8;
if result & 0x80 != 0 {
flags |= S_MASK;
}
if result == 0 {
flags |= Z_MASK;
}
if result.count_ones() % 2 == 0 {
flags |= P_MASK;
}
if half_out {
flags |= H_MASK;
}
if carry_out {
flags |= C_MASK;
}
if subtract {
flags |= N_MASK;
}
flags |= result & XY_MASK;
(result, flags)
}
#[test]
fn decimal_adjust_matches_the_documented_correction_over_every_input() {
for a in 0..=255u8 {
for state in 0..8u8 {
let carry = state & 0b001 != 0;
let half = state & 0b010 != 0;
let subtract = state & 0b100 != 0;
let mut flags = 0u8;
if carry {
flags |= C_MASK;
}
if half {
flags |= H_MASK;
}
if subtract {
flags |= N_MASK;
}
let expected = reference_decimal_adjust(a, carry, half, subtract);
assert_eq!(
run(AluOp::Daa, a, 0, flags),
expected,
"a={a:#04X} c={carry} h={half} n={subtract}"
);
}
}
}
#[test]
fn decimal_adjust_never_clears_the_carry_flag() {
for a in 0..=255u8 {
let (_, flags) = run(AluOp::Daa, a, 0, C_MASK);
assert_eq!(flags & C_MASK, C_MASK, "a={a:#04X}");
}
}
#[test]
fn rld_takes_the_high_digit_of_the_memory_operand() {
let (result, flags) = run(AluOp::Rld, 0x12, 0x34, H_MASK | N_MASK);
assert_eq!(result, 0x13);
assert_eq!(flags & (H_MASK | N_MASK), 0);
assert_eq!(flags & P_MASK, 0);
assert_eq!(flags & (S_MASK | Z_MASK), 0);
}
#[test]
fn rrd_takes_the_low_digit_of_the_memory_operand() {
let (result, _) = run(AluOp::Rrd, 0x12, 0x34, 0);
assert_eq!(result, 0x14);
}
#[test]
fn rotating_a_nibble_leaves_the_carry_flag_alone() {
let (_, flags) = run(AluOp::Rld, 0x00, 0x00, C_MASK);
assert_eq!(flags & C_MASK, C_MASK);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & P_MASK, P_MASK);
}
#[test]
fn testing_a_set_bit_clears_the_zero_and_parity_flags() {
let (result, flags) = bit_op(AluOp::Bit, 0b111_000, 0x80, 0);
assert_eq!(result, 0x80);
assert_eq!(flags & S_MASK, S_MASK);
assert_eq!(flags & (Z_MASK | P_MASK), 0);
assert_eq!(flags & H_MASK, H_MASK);
assert_eq!(flags & N_MASK, 0);
}
#[test]
fn testing_a_clear_bit_sets_the_zero_and_parity_flags() {
let (result, flags) = bit_op(AluOp::Bit, 0b111_000, 0x00, N_MASK);
assert_eq!(result, 0x00);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & P_MASK, P_MASK);
assert_eq!(flags & S_MASK, 0);
assert_eq!(flags & N_MASK, 0);
}
#[test]
fn testing_a_bit_of_a_register_takes_the_undocumented_flags_from_it() {
let (_, flags) = bit_op(AluOp::Bit, 0b011_000, 0x28, 0);
assert_eq!(flags & XY_MASK, XY_MASK);
}
#[test]
fn testing_a_bit_of_memory_leaves_the_undocumented_flags_clear() {
let (_, flags) = bit_op(AluOp::Bit, 0b011_110, 0x28, XY_MASK);
assert_eq!(flags & XY_MASK, 0);
}
#[test]
fn testing_a_bit_leaves_the_carry_flag_alone() {
let (_, set) = bit_op(AluOp::Bit, 0b000_000, 0x01, C_MASK);
assert_eq!(set & C_MASK, C_MASK);
let (_, clear) = bit_op(AluOp::Bit, 0b000_000, 0x01, 0);
assert_eq!(clear & C_MASK, 0);
}
#[test]
fn setting_a_bit_leaves_every_flag_alone() {
let (result, flags) = bit_op(AluOp::Set, 0b101_000, 0x00, S_MASK | Z_MASK);
assert_eq!(result, 0x20);
assert_eq!(flags, S_MASK | Z_MASK);
}
#[test]
fn clearing_a_bit_leaves_every_flag_alone() {
let (result, flags) = bit_op(AluOp::Res, 0b101_000, 0xFF, S_MASK | Z_MASK);
assert_eq!(result, 0xDF);
assert_eq!(flags, S_MASK | Z_MASK);
}
#[test]
fn every_rotate_shifts_the_expected_bit_into_the_carry() {
let cases = [
(0b000_000u8, 0x03u8),
(0b001_000, 0xC0),
(0b010_000, 0x02),
(0b011_000, 0x40),
(0b100_000, 0x02),
(0b101_000, 0xC0),
(0b110_000, 0x03),
(0b111_000, 0x40),
];
for (ir, expected) in cases {
let (result, flags) = shifted(AluOp::Rotate, ir, 0x81, 0);
assert_eq!(result, expected, "ir={ir:#010b}");
assert_eq!(flags & C_MASK, C_MASK, "ir={ir:#010b}");
assert_eq!(flags & (H_MASK | N_MASK), 0, "ir={ir:#010b}");
}
}
#[test]
fn rotating_through_the_carry_shifts_it_back_in() {
let (result, _) = shifted(AluOp::Rotate, 0b010_000, 0x00, C_MASK);
assert_eq!(result, 0x01);
let (result, _) = shifted(AluOp::Rotate, 0b011_000, 0x00, C_MASK);
assert_eq!(result, 0x80);
}
#[test]
fn an_arithmetic_shift_right_keeps_the_sign_bit() {
let (result, flags) = shifted(AluOp::Rotate, 0b101_000, 0x80, 0);
assert_eq!(result, 0xC0);
assert_eq!(flags & S_MASK, S_MASK);
assert_eq!(flags & C_MASK, 0);
}
#[test]
fn a_logical_shift_right_clears_the_sign_bit() {
let (result, flags) = shifted(AluOp::Rotate, 0b111_000, 0x80, 0);
assert_eq!(result, 0x40);
assert_eq!(flags & (S_MASK | Z_MASK), 0);
assert_eq!(flags & C_MASK, 0);
}
#[test]
fn the_undocumented_shift_left_brings_in_a_set_bit() {
let (result, flags) = shifted(AluOp::Rotate, 0b110_000, 0x00, 0);
assert_eq!(result, 0x01);
assert_eq!(flags & (C_MASK | Z_MASK), 0);
}
#[test]
fn a_prefixed_rotate_reports_the_sign_zero_and_parity_of_its_result() {
let (result, flags) = shifted(AluOp::Rotate, 0b000_000, 0x00, S_MASK | P_MASK);
assert_eq!(result, 0x00);
assert_eq!(flags & Z_MASK, Z_MASK);
assert_eq!(flags & P_MASK, P_MASK);
assert_eq!(flags & S_MASK, 0);
}
#[test]
fn an_unprefixed_rotate_holds_the_sign_zero_and_parity_flags() {
let held = S_MASK | Z_MASK | P_MASK;
let (result, flags) = execute(Inputs {
ir: 0b000_000,
..operands(AluOp::Rotate, 0x81, 0, held)
});
assert_eq!(result, 0x03);
assert_eq!(flags & held, held);
assert_eq!(flags & C_MASK, C_MASK);
assert_eq!(flags & (H_MASK | N_MASK), 0);
}
#[test]
fn an_unprefixed_rotate_still_reports_the_undocumented_flags() {
let (result, flags) = execute(Inputs {
ir: 0b000_000,
..operands(AluOp::Rotate, 0x14, 0, XY_MASK)
});
assert_eq!(result, 0x28);
assert_eq!(flags & XY_MASK, XY_MASK);
let (_, flags) = execute(Inputs {
ir: 0b000_000,
..operands(AluOp::Rotate, 0x00, 0, XY_MASK)
});
assert_eq!(flags & XY_MASK, 0);
}
#[test]
fn the_absent_operation_produces_nothing_and_holds_the_flags() {
let (result, flags) = run(AluOp::None, 0xAA, 0x55, 0xFF);
assert_eq!(result, 0x00);
assert_eq!(flags, 0xFF);
}