use std::collections::HashSet;
use k580_core::{Memory64K, decode_opcode};
pub(super) struct OperandKinds {
pub addresses: HashSet<u16>,
pub data: HashSet<u16>,
pub ports: HashSet<u16>,
}
pub(super) fn classify_operands(start: u16, count: usize, memory: &Memory64K) -> OperandKinds {
let mut operands = OperandKinds {
addresses: HashSet::new(),
data: HashSet::new(),
ports: HashSet::new(),
};
if count == 0 {
return operands;
}
let boundary = find_scan_boundary(start, memory);
let mut address = boundary;
let mut classified = 0usize;
while classified < count {
let value = memory.read(address);
let size = decode_opcode(value).map(|info| info.size).unwrap_or(1);
let kind = operand_kind(value);
for offset in 0..size {
let addr = address.wrapping_add(offset as u16);
if in_range(addr, start, count) {
classified += 1;
if offset > 0 {
match kind {
OperandKind::Address => operands.addresses.insert(addr),
OperandKind::Data => operands.data.insert(addr),
OperandKind::Port => operands.ports.insert(addr),
};
}
}
}
address = address.wrapping_add(size as u16);
}
operands
}
pub(crate) fn operand_jump_target(address: u16, memory: &Memory64K) -> Option<u16> {
let boundary = find_scan_boundary(address, memory);
let mut cursor = boundary;
for _ in 0..3 {
let opcode = memory.read(cursor);
let size = decode_opcode(opcode).map(|info| info.size).unwrap_or(1);
if instruction_covers(cursor, size, address) {
if address != cursor && is_address_opcode(opcode) && size == 3 {
let low = memory.read(cursor.wrapping_add(1));
let high = memory.read(cursor.wrapping_add(2));
return Some(u16::from_le_bytes([low, high]));
}
return None;
}
cursor = cursor.wrapping_add(size as u16);
}
None
}
pub(crate) fn operand_port_number(address: u16, memory: &Memory64K) -> Option<u8> {
let boundary = find_scan_boundary(address, memory);
let mut cursor = boundary;
for _ in 0..3 {
let opcode = memory.read(cursor);
let size = decode_opcode(opcode).map(|info| info.size).unwrap_or(1);
if instruction_covers(cursor, size, address) {
if address != cursor && is_port_opcode(opcode) && size == 2 {
return Some(memory.read(cursor.wrapping_add(1)));
}
return None;
}
cursor = cursor.wrapping_add(size as u16);
}
None
}
fn instruction_covers(start: u16, size: u8, address: u16) -> bool {
if size == 0 {
return false;
}
let end = start.wrapping_add((size - 1) as u16);
if end >= start {
(start..=end).contains(&address)
} else {
address >= start || address <= end
}
}
enum OperandKind {
Address,
Data,
Port,
}
fn operand_kind(opcode: u8) -> OperandKind {
if is_port_opcode(opcode) {
return OperandKind::Port;
}
if is_address_opcode(opcode) {
return OperandKind::Address;
}
OperandKind::Data
}
fn is_address_opcode(opcode: u8) -> bool {
matches!(
opcode,
0x01 | 0x11 | 0x21 | 0x31 | 0x22 | 0x2A | 0x32 | 0x3A | 0xC3 | 0xCD | 0xC2 | 0xCA | 0xD2 | 0xDA | 0xE2 | 0xEA | 0xF2 | 0xFA | 0xC4 | 0xCC | 0xD4 | 0xDC | 0xE4 | 0xEC | 0xF4 | 0xFC )
}
fn is_port_opcode(opcode: u8) -> bool {
matches!(opcode, 0xD3 | 0xDB)
}
fn find_scan_boundary(target: u16, memory: &Memory64K) -> u16 {
for back in [1, 2] {
let candidate = target.wrapping_sub(back);
let size = decode_opcode(memory.read(candidate))
.map(|info| info.size)
.unwrap_or(1);
if target.wrapping_sub(candidate) < size as u16 {
return candidate;
}
}
target
}
fn in_range(address: u16, start: u16, count: usize) -> bool {
(address.wrapping_sub(start) as usize) < count
}
#[cfg(test)]
mod tests {
use k580_core::Memory64K;
use super::{classify_operands, operand_jump_target, operand_port_number};
#[test]
fn one_byte_instructions_have_no_operands() {
let mut memory = Memory64K::default();
memory.write(0, 0x00); memory.write(1, 0x07); memory.write(2, 0x76); let operands = classify_operands(0, 3, &memory);
assert!(operands.addresses.is_empty());
assert!(operands.data.is_empty());
assert!(operands.ports.is_empty());
}
#[test]
fn eight_bit_data_operand_is_marked() {
let mut memory = Memory64K::default();
memory.write(0, 0x06); memory.write(1, 0x42);
memory.write(2, 0x00);
let operands = classify_operands(0, 3, &memory);
assert!(operands.data.contains(&1));
assert!(!operands.addresses.contains(&1));
assert!(!operands.ports.contains(&1));
}
#[test]
fn sixteen_bit_address_operands_are_marked() {
let mut memory = Memory64K::default();
memory.write(0, 0x01); memory.write(1, 0x34);
memory.write(2, 0x12);
memory.write(3, 0xC3); memory.write(4, 0x00);
memory.write(5, 0x01);
let operands = classify_operands(0, 6, &memory);
assert!(operands.addresses.contains(&1));
assert!(operands.addresses.contains(&2));
assert!(operands.addresses.contains(&4));
assert!(operands.addresses.contains(&5));
assert!(operands.data.is_empty());
}
#[test]
fn operand_classification_wraps_across_64k_boundary() {
let mut memory = Memory64K::default();
memory.write(0xFFFF, 0x06); memory.write(0x0000, 0x42);
let operands = classify_operands(0, 1, &memory);
assert!(operands.data.contains(&0));
}
#[test]
fn in_and_out_port_operands_are_purple() {
let mut memory = Memory64K::default();
memory.write(0, 0xD3); memory.write(1, 0x04); memory.write(2, 0xDB); memory.write(3, 0x00); let operands = classify_operands(0, 4, &memory);
assert!(operands.ports.contains(&1));
assert!(operands.ports.contains(&3));
assert!(!operands.data.contains(&1));
assert!(!operands.addresses.contains(&1));
}
#[test]
fn jump_target_from_low_operand_byte_is_little_endian_address() {
let mut memory = Memory64K::default();
memory.write(0, 0x01); memory.write(1, 0x34); memory.write(2, 0x12); assert_eq!(operand_jump_target(1, &memory), Some(0x1234));
}
#[test]
fn jump_target_from_high_operand_byte_matches_low_half() {
let mut memory = Memory64K::default();
memory.write(0, 0x01); memory.write(1, 0x34);
memory.write(2, 0x12);
assert_eq!(operand_jump_target(2, &memory), Some(0x1234));
}
#[test]
fn jump_target_is_none_on_the_opcode_byte_itself() {
let mut memory = Memory64K::default();
memory.write(0, 0xC3); memory.write(1, 0x00);
memory.write(2, 0x10);
assert_eq!(operand_jump_target(0, &memory), None);
}
#[test]
fn jump_target_resolves_after_a_preceding_two_byte_instruction() {
let mut memory = Memory64K::default();
memory.write(0, 0x06); memory.write(1, 0xFF);
memory.write(2, 0xC3); memory.write(3, 0x00);
memory.write(4, 0x02);
assert_eq!(operand_jump_target(3, &memory), Some(0x0200));
assert_eq!(operand_jump_target(4, &memory), Some(0x0200));
}
#[test]
fn jump_target_is_none_for_data_and_port_operands() {
let mut memory = Memory64K::default();
memory.write(0, 0x06); memory.write(1, 0x42);
memory.write(2, 0xD3); memory.write(3, 0x04);
assert_eq!(operand_jump_target(1, &memory), None);
assert_eq!(operand_jump_target(3, &memory), None);
}
#[test]
fn jump_target_wraps_across_64k_boundary() {
let mut memory = Memory64K::default();
memory.write(0xFFFF, 0x01); memory.write(0x0000, 0x78);
memory.write(0x0001, 0x56);
assert_eq!(operand_jump_target(0x0000, &memory), Some(0x5678));
assert_eq!(operand_jump_target(0x0001, &memory), Some(0x5678));
}
#[test]
fn port_number_from_out_operand_byte() {
let mut memory = Memory64K::default();
memory.write(0, 0xD3); memory.write(1, 0x04); assert_eq!(operand_port_number(1, &memory), Some(0x04));
}
#[test]
fn port_number_from_in_operand_byte() {
let mut memory = Memory64K::default();
memory.write(0, 0xDB); memory.write(1, 0x00); assert_eq!(operand_port_number(1, &memory), Some(0x00));
}
#[test]
fn port_number_is_none_on_opcode_byte() {
let mut memory = Memory64K::default();
memory.write(0, 0xD3); memory.write(1, 0x04);
assert_eq!(operand_port_number(0, &memory), None);
}
#[test]
fn port_number_is_none_for_data_and_address_operands() {
let mut memory = Memory64K::default();
memory.write(0, 0x06); memory.write(1, 0x42);
memory.write(2, 0x01); memory.write(3, 0x34);
memory.write(4, 0x12);
assert_eq!(operand_port_number(1, &memory), None);
assert_eq!(operand_port_number(3, &memory), None);
assert_eq!(operand_port_number(4, &memory), None);
}
#[test]
fn port_number_resolves_after_preceding_instruction() {
let mut memory = Memory64K::default();
memory.write(0, 0x3E); memory.write(1, 0xFF);
memory.write(2, 0xD3); memory.write(3, 0x02);
assert_eq!(operand_port_number(3, &memory), Some(0x02));
}
}