use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Reg8 {
B,
C,
D,
E,
H,
L,
A,
}
impl Reg8 {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Reg8::B => "B",
Reg8::C => "C",
Reg8::D => "D",
Reg8::E => "E",
Reg8::H => "H",
Reg8::L => "L",
Reg8::A => "A",
}
}
}
impl fmt::Display for Reg8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Reg16 {
Bc,
De,
Hl,
Sp,
Af,
}
impl Reg16 {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Reg16::Bc => "BC",
Reg16::De => "DE",
Reg16::Hl => "HL",
Reg16::Sp => "SP",
Reg16::Af => "AF",
}
}
}
impl fmt::Display for Reg16 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Cond {
Nz,
Z,
Nc,
C,
}
impl Cond {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Cond::Nz => "NZ",
Cond::Z => "Z",
Cond::Nc => "NC",
Cond::C => "C",
}
}
}
impl fmt::Display for Cond {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Operand {
None,
Reg(Reg8),
MemHl,
Reg16(Reg16),
MemReg16(Reg16),
MemHlInc,
MemHlDec,
MemHighC,
Imm8,
Imm16,
Rel8,
MemImm16,
MemHighImm8,
SpRel8,
Cond(Cond),
Bit(u8),
Vector(u8),
}
impl Operand {
#[must_use]
pub const fn bytes(self) -> u16 {
match self {
Operand::Imm8 | Operand::Rel8 | Operand::MemHighImm8 | Operand::SpRel8 => 1,
Operand::Imm16 | Operand::MemImm16 => 2,
_ => 0,
}
}
#[must_use]
pub const fn is_memory(self) -> bool {
matches!(
self,
Operand::MemHl
| Operand::MemReg16(_)
| Operand::MemHlInc
| Operand::MemHlDec
| Operand::MemHighC
| Operand::MemImm16
| Operand::MemHighImm8
)
}
#[must_use]
pub const fn is_wide(self) -> bool {
matches!(self, Operand::Reg16(_) | Operand::Imm16 | Operand::SpRel8)
}
pub const B: Operand = Operand::Reg(Reg8::B);
pub const C: Operand = Operand::Reg(Reg8::C);
pub const D: Operand = Operand::Reg(Reg8::D);
pub const E: Operand = Operand::Reg(Reg8::E);
pub const H: Operand = Operand::Reg(Reg8::H);
pub const L: Operand = Operand::Reg(Reg8::L);
pub const A: Operand = Operand::Reg(Reg8::A);
pub const MHL: Operand = Operand::MemHl;
pub const BC: Operand = Operand::Reg16(Reg16::Bc);
pub const DE: Operand = Operand::Reg16(Reg16::De);
pub const HL: Operand = Operand::Reg16(Reg16::Hl);
pub const SP: Operand = Operand::Reg16(Reg16::Sp);
pub const AF: Operand = Operand::Reg16(Reg16::Af);
pub const MBC: Operand = Operand::MemReg16(Reg16::Bc);
pub const MDE: Operand = Operand::MemReg16(Reg16::De);
pub const MHLI: Operand = Operand::MemHlInc;
pub const MHLD: Operand = Operand::MemHlDec;
pub const MC: Operand = Operand::MemHighC;
pub const N8: Operand = Operand::Imm8;
pub const N16: Operand = Operand::Imm16;
pub const E8: Operand = Operand::Rel8;
pub const MN16: Operand = Operand::MemImm16;
pub const MN8: Operand = Operand::MemHighImm8;
pub const SPE8: Operand = Operand::SpRel8;
pub const CNZ: Operand = Operand::Cond(Cond::Nz);
pub const CZ: Operand = Operand::Cond(Cond::Z);
pub const CNC: Operand = Operand::Cond(Cond::Nc);
pub const CC: Operand = Operand::Cond(Cond::C);
pub const NONE: Operand = Operand::None;
}
macro_rules! define_ops {
($($name:ident = $summary:literal,)*) => {
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Op {
$(
#[doc = $summary]
$name,
)*
}
impl Op {
#[must_use]
pub const fn mnemonic(self) -> &'static str {
match self { $(Op::$name => stringify!($name),)* }
}
#[must_use]
pub const fn summary(self) -> &'static str {
match self { $(Op::$name => $summary,)* }
}
pub const ALL: &'static [Op] = &[$(Op::$name,)*];
}
};
}
define_ops! {
ADC = "add with carry into the accumulator",
ADD = "add into the accumulator, HL or SP",
AND = "AND into the accumulator",
BIT = "test one bit, setting Z from its complement",
CALL = "push the return address and jump",
CCF = "complement the carry flag",
CP = "compare with the accumulator, discarding the result",
CPL = "complement the accumulator",
DAA = "decimal-adjust the accumulator after an addition or subtraction",
DEC = "decrement",
DI = "clear the interrupt master enable",
EI = "set the interrupt master enable, one instruction late",
HALT = "stop the clock until an interrupt is pending",
INC = "increment",
JP = "jump",
JR = "jump relative to the next instruction",
LD = "load",
LDH = "load through the $FF00 page",
LOCK = "not an instruction: the chip hangs until reset",
NOP = "no operation",
OR = "OR into the accumulator",
POP = "pop a register pair off the stack",
PREFIX = "not an instruction: $CB selects the second opcode page",
PUSH = "push a register pair onto the stack",
RES = "clear one bit",
RET = "pop the return address and jump to it",
RETI = "return and set the interrupt master enable",
RL = "rotate left through carry",
RLA = "rotate the accumulator left through carry, clearing Z",
RLC = "rotate left, carry from bit 7",
RLCA = "rotate the accumulator left, clearing Z",
RR = "rotate right through carry",
RRA = "rotate the accumulator right through carry, clearing Z",
RRC = "rotate right, carry from bit 0",
RRCA = "rotate the accumulator right, clearing Z",
RST = "call one of the eight page-zero vectors",
SBC = "subtract with borrow from the accumulator",
SCF = "set the carry flag",
SET = "set one bit",
SLA = "shift left, zero into bit 0",
SRA = "shift right, preserving bit 7",
SRL = "shift right, zero into bit 7",
STOP = "stop the clock and the LCD until a button is pressed",
SUB = "subtract from the accumulator",
SWAP = "exchange the two nibbles of a byte",
XOR = "exclusive-OR into the accumulator",
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Class {
Documented,
Unimplemented,
}
impl Class {
#[must_use]
pub const fn is_documented(self) -> bool {
matches!(self, Class::Documented)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Insn {
pub op: Op,
pub dst: Operand,
pub src: Operand,
pub class: Class,
pub prefixed: bool,
}
impl Insn {
const fn new(op: Op, dst: Operand, src: Operand, class: Class) -> Insn {
Insn {
op,
dst,
src,
class,
prefixed: false,
}
}
const fn cb(op: Op, dst: Operand, src: Operand) -> Insn {
Insn {
op,
dst,
src,
class: Class::Documented,
prefixed: true,
}
}
#[must_use]
pub const fn bytes(self) -> u16 {
let prefix = if self.prefixed { 1 } else { 0 };
1 + prefix + self.dst.bytes() + self.src.bytes()
}
#[must_use]
pub const fn condition(self) -> Option<Cond> {
match self.dst {
Operand::Cond(c) => Some(c),
_ => None,
}
}
}
macro_rules! isa {
($($opcode:literal $op:ident $dst:ident $src:ident $class:ident;)*) => {
pub static BASE: [Insn; 256] = {
let mut t = [Insn::new(
Op::LOCK, Operand::NONE, Operand::NONE, Class::Unimplemented); 256];
$(t[$opcode as usize] = Insn::new(
Op::$op, Operand::$dst, Operand::$src, Class::$class);)*
t
};
pub static LISTED: [bool; 256] = {
let mut t = [false; 256];
$(t[$opcode as usize] = true;)*
t
};
};
}
isa! {
0x00 NOP NONE NONE Documented;
0x01 LD BC N16 Documented;
0x02 LD MBC A Documented;
0x03 INC BC NONE Documented;
0x04 INC B NONE Documented;
0x05 DEC B NONE Documented;
0x06 LD B N8 Documented;
0x07 RLCA NONE NONE Documented;
0x08 LD MN16 SP Documented;
0x09 ADD HL BC Documented;
0x0a LD A MBC Documented;
0x0b DEC BC NONE Documented;
0x0c INC C NONE Documented;
0x0d DEC C NONE Documented;
0x0e LD C N8 Documented;
0x0f RRCA NONE NONE Documented;
0x10 STOP NONE N8 Documented;
0x11 LD DE N16 Documented;
0x12 LD MDE A Documented;
0x13 INC DE NONE Documented;
0x14 INC D NONE Documented;
0x15 DEC D NONE Documented;
0x16 LD D N8 Documented;
0x17 RLA NONE NONE Documented;
0x18 JR NONE E8 Documented;
0x19 ADD HL DE Documented;
0x1a LD A MDE Documented;
0x1b DEC DE NONE Documented;
0x1c INC E NONE Documented;
0x1d DEC E NONE Documented;
0x1e LD E N8 Documented;
0x1f RRA NONE NONE Documented;
0x20 JR CNZ E8 Documented;
0x21 LD HL N16 Documented;
0x22 LD MHLI A Documented;
0x23 INC HL NONE Documented;
0x24 INC H NONE Documented;
0x25 DEC H NONE Documented;
0x26 LD H N8 Documented;
0x27 DAA NONE NONE Documented;
0x28 JR CZ E8 Documented;
0x29 ADD HL HL Documented;
0x2a LD A MHLI Documented;
0x2b DEC HL NONE Documented;
0x2c INC L NONE Documented;
0x2d DEC L NONE Documented;
0x2e LD L N8 Documented;
0x2f CPL NONE NONE Documented;
0x30 JR CNC E8 Documented;
0x31 LD SP N16 Documented;
0x32 LD MHLD A Documented;
0x33 INC SP NONE Documented;
0x34 INC MHL NONE Documented;
0x35 DEC MHL NONE Documented;
0x36 LD MHL N8 Documented;
0x37 SCF NONE NONE Documented;
0x38 JR CC E8 Documented;
0x39 ADD HL SP Documented;
0x3a LD A MHLD Documented;
0x3b DEC SP NONE Documented;
0x3c INC A NONE Documented;
0x3d DEC A NONE Documented;
0x3e LD A N8 Documented;
0x3f CCF NONE NONE Documented;
0x40 LD B B Documented;
0x41 LD B C Documented;
0x42 LD B D Documented;
0x43 LD B E Documented;
0x44 LD B H Documented;
0x45 LD B L Documented;
0x46 LD B MHL Documented;
0x47 LD B A Documented;
0x48 LD C B Documented;
0x49 LD C C Documented;
0x4a LD C D Documented;
0x4b LD C E Documented;
0x4c LD C H Documented;
0x4d LD C L Documented;
0x4e LD C MHL Documented;
0x4f LD C A Documented;
0x50 LD D B Documented;
0x51 LD D C Documented;
0x52 LD D D Documented;
0x53 LD D E Documented;
0x54 LD D H Documented;
0x55 LD D L Documented;
0x56 LD D MHL Documented;
0x57 LD D A Documented;
0x58 LD E B Documented;
0x59 LD E C Documented;
0x5a LD E D Documented;
0x5b LD E E Documented;
0x5c LD E H Documented;
0x5d LD E L Documented;
0x5e LD E MHL Documented;
0x5f LD E A Documented;
0x60 LD H B Documented;
0x61 LD H C Documented;
0x62 LD H D Documented;
0x63 LD H E Documented;
0x64 LD H H Documented;
0x65 LD H L Documented;
0x66 LD H MHL Documented;
0x67 LD H A Documented;
0x68 LD L B Documented;
0x69 LD L C Documented;
0x6a LD L D Documented;
0x6b LD L E Documented;
0x6c LD L H Documented;
0x6d LD L L Documented;
0x6e LD L MHL Documented;
0x6f LD L A Documented;
0x70 LD MHL B Documented;
0x71 LD MHL C Documented;
0x72 LD MHL D Documented;
0x73 LD MHL E Documented;
0x74 LD MHL H Documented;
0x75 LD MHL L Documented;
0x76 HALT NONE NONE Documented;
0x77 LD MHL A Documented;
0x78 LD A B Documented;
0x79 LD A C Documented;
0x7a LD A D Documented;
0x7b LD A E Documented;
0x7c LD A H Documented;
0x7d LD A L Documented;
0x7e LD A MHL Documented;
0x7f LD A A Documented;
0x80 ADD A B Documented;
0x81 ADD A C Documented;
0x82 ADD A D Documented;
0x83 ADD A E Documented;
0x84 ADD A H Documented;
0x85 ADD A L Documented;
0x86 ADD A MHL Documented;
0x87 ADD A A Documented;
0x88 ADC A B Documented;
0x89 ADC A C Documented;
0x8a ADC A D Documented;
0x8b ADC A E Documented;
0x8c ADC A H Documented;
0x8d ADC A L Documented;
0x8e ADC A MHL Documented;
0x8f ADC A A Documented;
0x90 SUB A B Documented;
0x91 SUB A C Documented;
0x92 SUB A D Documented;
0x93 SUB A E Documented;
0x94 SUB A H Documented;
0x95 SUB A L Documented;
0x96 SUB A MHL Documented;
0x97 SUB A A Documented;
0x98 SBC A B Documented;
0x99 SBC A C Documented;
0x9a SBC A D Documented;
0x9b SBC A E Documented;
0x9c SBC A H Documented;
0x9d SBC A L Documented;
0x9e SBC A MHL Documented;
0x9f SBC A A Documented;
0xa0 AND A B Documented;
0xa1 AND A C Documented;
0xa2 AND A D Documented;
0xa3 AND A E Documented;
0xa4 AND A H Documented;
0xa5 AND A L Documented;
0xa6 AND A MHL Documented;
0xa7 AND A A Documented;
0xa8 XOR A B Documented;
0xa9 XOR A C Documented;
0xaa XOR A D Documented;
0xab XOR A E Documented;
0xac XOR A H Documented;
0xad XOR A L Documented;
0xae XOR A MHL Documented;
0xaf XOR A A Documented;
0xb0 OR A B Documented;
0xb1 OR A C Documented;
0xb2 OR A D Documented;
0xb3 OR A E Documented;
0xb4 OR A H Documented;
0xb5 OR A L Documented;
0xb6 OR A MHL Documented;
0xb7 OR A A Documented;
0xb8 CP A B Documented;
0xb9 CP A C Documented;
0xba CP A D Documented;
0xbb CP A E Documented;
0xbc CP A H Documented;
0xbd CP A L Documented;
0xbe CP A MHL Documented;
0xbf CP A A Documented;
0xc0 RET CNZ NONE Documented;
0xc1 POP BC NONE Documented;
0xc2 JP CNZ N16 Documented;
0xc3 JP NONE N16 Documented;
0xc4 CALL CNZ N16 Documented;
0xc5 PUSH BC NONE Documented;
0xc6 ADD A N8 Documented;
0xc7 RST NONE NONE Documented;
0xc8 RET CZ NONE Documented;
0xc9 RET NONE NONE Documented;
0xca JP CZ N16 Documented;
0xcb PREFIX NONE NONE Documented;
0xcc CALL CZ N16 Documented;
0xcd CALL NONE N16 Documented;
0xce ADC A N8 Documented;
0xcf RST NONE NONE Documented;
0xd0 RET CNC NONE Documented;
0xd1 POP DE NONE Documented;
0xd2 JP CNC N16 Documented;
0xd3 LOCK NONE NONE Unimplemented;
0xd4 CALL CNC N16 Documented;
0xd5 PUSH DE NONE Documented;
0xd6 SUB A N8 Documented;
0xd7 RST NONE NONE Documented;
0xd8 RET CC NONE Documented;
0xd9 RETI NONE NONE Documented;
0xda JP CC N16 Documented;
0xdb LOCK NONE NONE Unimplemented;
0xdc CALL CC N16 Documented;
0xdd LOCK NONE NONE Unimplemented;
0xde SBC A N8 Documented;
0xdf RST NONE NONE Documented;
0xe0 LDH MN8 A Documented;
0xe1 POP HL NONE Documented;
0xe2 LDH MC A Documented;
0xe3 LOCK NONE NONE Unimplemented;
0xe4 LOCK NONE NONE Unimplemented;
0xe5 PUSH HL NONE Documented;
0xe6 AND A N8 Documented;
0xe7 RST NONE NONE Documented;
0xe8 ADD SP E8 Documented;
0xe9 JP NONE HL Documented;
0xea LD MN16 A Documented;
0xeb LOCK NONE NONE Unimplemented;
0xec LOCK NONE NONE Unimplemented;
0xed LOCK NONE NONE Unimplemented;
0xee XOR A N8 Documented;
0xef RST NONE NONE Documented;
0xf0 LDH A MN8 Documented;
0xf1 POP AF NONE Documented;
0xf2 LDH A MC Documented;
0xf3 DI NONE NONE Documented;
0xf4 LOCK NONE NONE Unimplemented;
0xf5 PUSH AF NONE Documented;
0xf6 OR A N8 Documented;
0xf7 RST NONE NONE Documented;
0xf8 LD HL SPE8 Documented;
0xf9 LD SP HL Documented;
0xfa LD A MN16 Documented;
0xfb EI NONE NONE Documented;
0xfc LOCK NONE NONE Unimplemented;
0xfd LOCK NONE NONE Unimplemented;
0xfe CP A N8 Documented;
0xff RST NONE NONE Documented;
}
const fn rst_vector(opcode: u8) -> u8 {
opcode & 0x38
}
const fn r8_operand(index: u8) -> Operand {
match index & 7 {
0 => Operand::B,
1 => Operand::C,
2 => Operand::D,
3 => Operand::E,
4 => Operand::H,
5 => Operand::L,
6 => Operand::MHL,
_ => Operand::A,
}
}
pub static PREFIXED: [Insn; 256] = {
let mut t = [Insn::cb(Op::RLC, Operand::B, Operand::NONE); 256];
let mut i = 0usize;
while i < 256 {
let opcode = i as u8;
let target = r8_operand(opcode);
t[i] = if opcode < 0x40 {
let op = match opcode >> 3 {
0 => Op::RLC,
1 => Op::RRC,
2 => Op::RL,
3 => Op::RR,
4 => Op::SLA,
5 => Op::SRA,
6 => Op::SWAP,
_ => Op::SRL,
};
Insn::cb(op, target, Operand::NONE)
} else {
let op = match opcode >> 6 {
1 => Op::BIT,
2 => Op::RES,
_ => Op::SET,
};
Insn::cb(op, Operand::Bit((opcode >> 3) & 7), target)
};
i += 1;
}
t
};
#[inline]
#[must_use]
pub fn decode(opcode: u8) -> Insn {
let mut insn = BASE[opcode as usize];
if insn.op == Op::RST {
insn.dst = Operand::Vector(rst_vector(opcode));
}
insn
}
#[inline]
#[must_use]
pub fn decode_cb(opcode: u8) -> Insn {
PREFIXED[opcode as usize]
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn every_opcode_has_a_row() {
let missing: Vec<usize> = (0..256).filter(|i| !LISTED[*i]).collect();
assert!(missing.is_empty(), "unlisted opcodes: {missing:?}");
}
#[test]
fn exactly_eleven_encodings_are_holes() {
let holes: Vec<u8> = (0..=255u8)
.filter(|o| !decode(*o).class.is_documented())
.collect();
assert_eq!(
holes,
[
0xd3, 0xdb, 0xdd, 0xe3, 0xe4, 0xeb, 0xec, 0xed, 0xf4, 0xfc, 0xfd
],
"the SM83 matrix has eleven holes and these are they"
);
}
#[test]
fn lengths_are_derived_and_correct() {
assert_eq!(decode(0x00).bytes(), 1, "NOP");
assert_eq!(decode(0x06).bytes(), 2, "LD B,n8");
assert_eq!(decode(0x01).bytes(), 3, "LD BC,n16");
assert_eq!(decode(0x08).bytes(), 3, "LD (n16),SP");
assert_eq!(decode(0xe0).bytes(), 2, "LDH (n8),A");
assert_eq!(decode(0xe2).bytes(), 1, "LDH (C),A");
assert_eq!(decode(0xf8).bytes(), 2, "LD HL,SP+e8");
assert_eq!(decode(0x18).bytes(), 2, "JR e8");
assert_eq!(decode(0x20).bytes(), 2, "JR NZ,e8");
assert_eq!(decode(0xc3).bytes(), 3, "JP n16");
assert_eq!(decode(0xe9).bytes(), 1, "JP HL");
assert_eq!(decode(0xcd).bytes(), 3, "CALL n16");
assert_eq!(decode(0xff).bytes(), 1, "RST $38");
assert_eq!(decode(0x10).bytes(), 2, "STOP");
assert_eq!(decode_cb(0x00).bytes(), 2, "RLC B");
assert_eq!(decode_cb(0x7e).bytes(), 2, "BIT 7,(HL)");
}
#[test]
fn the_ld_block_is_a_square_with_one_hole_in_it() {
for opcode in 0x40..=0x7fu8 {
let insn = decode(opcode);
if opcode == 0x76 {
assert_eq!(insn.op, Op::HALT, "$76 is HALT, not LD (HL),(HL)");
continue;
}
assert_eq!(insn.op, Op::LD, "{opcode:#04x}");
assert_eq!(insn.dst, r8_operand(opcode >> 3), "{opcode:#04x} dst");
assert_eq!(insn.src, r8_operand(opcode), "{opcode:#04x} src");
}
}
#[test]
fn rst_carries_its_vector() {
for (i, opcode) in [0xc7u8, 0xcf, 0xd7, 0xdf, 0xe7, 0xef, 0xf7, 0xff]
.into_iter()
.enumerate()
{
let insn = decode(opcode);
assert_eq!(insn.op, Op::RST);
assert_eq!(insn.dst, Operand::Vector((i as u8) * 8));
}
}
#[test]
fn the_cb_page_covers_every_bit_of_every_register() {
for bit in 0..8u8 {
for reg in 0..8u8 {
let base = 0x40 + bit * 8 + reg;
assert_eq!(decode_cb(base).op, Op::BIT);
assert_eq!(decode_cb(base).dst, Operand::Bit(bit));
assert_eq!(decode_cb(base).src, r8_operand(reg));
assert_eq!(decode_cb(base + 0x40).op, Op::RES);
assert_eq!(decode_cb(base + 0x80).op, Op::SET);
}
}
}
#[test]
fn every_op_appears_somewhere() {
for op in Op::ALL {
let in_base = BASE.iter().any(|i| i.op == *op);
let in_cb = PREFIXED.iter().any(|i| i.op == *op);
assert!(in_base || in_cb, "{op:?} is declared but never encoded");
}
}
}