use anyhow::bail;
use core::{
fmt::{Display, Formatter},
str::FromStr,
};
#[expect(clippy::min_ident_chars, reason = "the actual names")]
#[expect(clippy::arbitrary_source_item_ordering, reason = "logical order")]
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Reg {
A,
B,
C,
D,
E,
F,
G,
H,
AB,
CD,
EF,
GH,
SP,
}
impl Display for Reg {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
match *self {
Reg::A => "a",
Reg::B => "b",
Reg::C => "c",
Reg::D => "d",
Reg::E => "e",
Reg::F => "f",
Reg::G => "g",
Reg::H => "h",
Reg::AB => "ab",
Reg::CD => "cd",
Reg::EF => "ef",
Reg::GH => "gh",
Reg::SP => "sp",
}
)
}
}
impl FromStr for Reg {
type Err = anyhow::Error;
#[inline]
fn from_str(value: &str) -> Result<Self, anyhow::Error> {
Ok(match value {
"a" => Reg::A,
"b" => Reg::B,
"c" => Reg::C,
"d" => Reg::D,
"e" => Reg::E,
"f" => Reg::F,
"g" => Reg::G,
"h" => Reg::H,
"ab" => Reg::AB,
"cd" => Reg::CD,
"ef" => Reg::EF,
"gh" => Reg::GH,
"sp" => Reg::SP,
reg => bail!("invalid register {reg}"),
})
}
}
impl TryFrom<u8> for Reg {
type Error = anyhow::Error;
#[inline]
fn try_from(id: u8) -> Result<Self, Self::Error> {
Ok(match id {
0x00 => Reg::A,
0x01 => Reg::B,
0x02 => Reg::C,
0x03 => Reg::D,
0x04 => Reg::E,
0x05 => Reg::F,
0x06 => Reg::G,
0x07 => Reg::H,
0x08 => Reg::AB,
0x09 => Reg::CD,
0x0A => Reg::EF,
0x0B => Reg::GH,
0x0C => Reg::SP,
_ => bail!("invalid register id {id:#04X}"),
})
}
}
impl From<Reg> for u8 {
#[inline]
fn from(reg: Reg) -> Self {
match reg {
Reg::A => 0x00,
Reg::B => 0x01,
Reg::C => 0x02,
Reg::D => 0x03,
Reg::E => 0x04,
Reg::F => 0x05,
Reg::G => 0x06,
Reg::H => 0x07,
Reg::AB => 0x08,
Reg::CD => 0x09,
Reg::EF => 0x0A,
Reg::GH => 0x0B,
Reg::SP => 0x0C,
}
}
}
impl Reg {
#[inline]
#[must_use]
pub fn is16(&self) -> bool {
matches!(self, Reg::AB | Reg::CD | Reg::EF | Reg::GH | Reg::SP)
}
}
#[derive(Debug, Default)]
pub struct Regs {
ra: u8,
rb: u8,
rc: u8,
rd: u8,
re: u8,
rf: u8,
rg: u8,
rh: u8,
sp: u16,
}
impl Regs {
#[inline]
#[must_use]
pub fn get(&self, reg: Reg) -> u16 {
use Reg::*;
match reg {
A => u16::from(self.ra),
B => u16::from(self.rb),
C => u16::from(self.rc),
D => u16::from(self.rd),
E => u16::from(self.re),
F => u16::from(self.rf),
G => u16::from(self.rg),
H => u16::from(self.rh),
Reg::AB => u16::from_be_bytes([self.ra, self.rb]),
Reg::CD => u16::from_be_bytes([self.rc, self.rd]),
Reg::EF => u16::from_be_bytes([self.re, self.rf]),
Reg::GH => u16::from_be_bytes([self.rg, self.rh]),
Reg::SP => self.sp,
}
}
#[expect(clippy::cast_possible_truncation, reason = "truncation is correct")]
#[inline]
pub fn set(&mut self, reg: Reg, val: u16) -> u16 {
use Reg::*;
let byte = val as u8;
let [hi, lo] = val.to_be_bytes();
match reg {
A => self.ra = byte,
B => self.rb = byte,
C => self.rc = byte,
D => self.rd = byte,
E => self.re = byte,
F => self.rf = byte,
G => self.rg = byte,
H => self.rh = byte,
AB => [self.ra, self.rb] = [hi, lo],
CD => [self.rc, self.rd] = [hi, lo],
EF => [self.re, self.rf] = [hi, lo],
GH => [self.rg, self.rh] = [hi, lo],
SP => self.sp = val,
}
self.get(reg)
}
}
#[inline]
#[must_use]
pub fn source_and_target_from(regs: u8) -> Option<(Reg, Reg)> {
if let Some(source) = source_from(regs)
&& let Ok(target) = Reg::try_from(regs & 0x0F)
{
Some((source, target))
} else {
None
}
}
#[inline]
#[must_use]
pub fn source_from(reg: u8) -> Option<Reg> {
Reg::try_from((reg & 0x0F0) >> 4_u8).ok()
}
#[inline]
#[must_use]
pub fn u8_from(source: Reg, target: Reg) -> u8 {
(u8::from(source) << 4_u8) | u8::from(target)
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests")]
mod tests {
use crate::regs::{Reg::AB, Reg::*};
use super::*;
#[test]
fn addressing_individual_regs_works() {
let mut regs = Regs::default();
regs.set(A, 0x00FF);
assert_eq!(regs.get(A), 0x00FF, "wrong A");
regs.set(F, 0x00FF);
assert_eq!(regs.get(F), 0x00FF, "wrong F");
}
#[test]
fn addressing_reg_pairs_works() {
let mut regs = Regs::default();
regs.set(A, 0x00DE);
regs.set(B, 0x00AD);
assert_eq!(regs.get(AB), 0xDEAD, "wrong AB");
regs.set(AB, 0xBEEF);
assert_eq!(regs.get(AB), 0xBEEF, "wrong AB");
assert_eq!(regs.get(A), 0xBE, "wrong A");
assert_eq!(regs.get(B), 0xEF, "wrong B");
regs.set(GH, 0xCAFE);
assert_eq!(regs.get(GH), 0xCAFE, "wrong GH");
}
#[test]
fn addressing_sp_works() {
let mut regs = Regs::default();
let sp = Reg::SP;
regs.set(sp, 0xFFFF);
assert!(sp.is16(), "SP should be 16-bit");
assert_eq!(regs.get(sp), 0xFFFF, "wrong SP");
}
#[test]
fn reg_display_names_work() {
assert_eq!(format!("{}", Reg::F), "f");
}
#[test]
fn reg_name_parsing_works() {
assert_eq!("f".parse::<Reg>().unwrap(), Reg::F);
}
#[test]
fn reg_marshalling_to_u8_works() {
assert_eq!(u8::from(Reg::F), 0x05);
}
#[test]
fn reg_unmarshalling_from_u8_works() {
assert_eq!(Reg::try_from(0x05).unwrap(), Reg::F);
}
}