siso 0.1.0

IBM 5150 emulator
Documentation
use std::fmt;

use crate::chips::{Rom, ROM_SIZE};

const ADDR_BOUND: u32 = 0x100000;

const SYSTEM_RAM: usize = 0x40000;
const IO_CHANNEL_RAM: usize = 0x60000;

const CASSETTE_BASIC_ROMS: usize = 4;

/// The 16-bit effective address of a memory operand.
///
/// This is the operand's distance in bytes from the beginning of the segment in which it
/// resides.
#[derive(Clone, Copy)]
pub(super) struct EffectiveAddress(u16);

impl fmt::Debug for EffectiveAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("EffectiveAddress")
            .field(&format_args!("{:#06x}", self.0))
            .finish()
    }
}

impl From<u16> for EffectiveAddress {
    fn from(offset: u16) -> Self {
        EffectiveAddress(offset)
    }
}

impl EffectiveAddress {
    pub(super) fn to_inner(self) -> u16 {
        self.0
    }
}

/// The 20-bit physical address of a memory operand.
#[derive(Clone, Copy)]
pub(super) struct PhysicalAddress(u32);

impl fmt::Debug for PhysicalAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("PhysicalAddress")
            .field(&format_args!("{:#07x}", self.0))
            .finish()
    }
}

impl fmt::Display for PhysicalAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:#07x}", self.0)
    }
}

impl PhysicalAddress {
    /// Constructs a physical address from a segment base address and offset within the
    /// segment.
    pub(super) fn new(segment: u16, offset: EffectiveAddress) -> Self {
        let addr = ((segment as u32) << 4) + (offset.0 as u32);
        assert!(addr < ADDR_BOUND);
        PhysicalAddress(addr)
    }
}

impl IntoIterator for PhysicalAddress {
    type Item = Self;
    type IntoIter = PhysicalAddressIterator;

    fn into_iter(self) -> Self::IntoIter {
        PhysicalAddressIterator { next: Some(self) }
    }
}

/// An iterator that produces a sequence of [`PhysicalAddress`]es.
pub(super) struct PhysicalAddressIterator {
    next: Option<PhysicalAddress>,
}

impl Iterator for PhysicalAddressIterator {
    type Item = PhysicalAddress;

    fn next(&mut self) -> Option<Self::Item> {
        let ret = self.next;
        self.next = self
            .next
            .and_then(|addr| addr.0.checked_add(1))
            .map(PhysicalAddress);
        ret
    }
}

/// Memory map for the IBM 5150.
///
/// Sources:
/// - IBM 5150 Technical Reference, Page 1-9
/// - http://minuszerodegrees.net/5150/misc/5150%20-%20Memory%20Map%20of%20the%20640%20KB%20to%201%20MB%20Area.jpg
fn map_memory<R>(
    addr: PhysicalAddress,
    system_ram: impl FnOnce(usize) -> R,
    io_channel_ram: impl FnOnce(usize) -> R,
    cassette_basic: impl FnOnce(usize) -> R,
    bios_rom: impl FnOnce(usize) -> R,
) -> R {
    const SYSTEM_RAM_START: u32 = 0;
    const SYSTEM_RAM_END: u32 = SYSTEM_RAM_START + (SYSTEM_RAM as u32) - 1;

    const IO_CHANNEL_RAM_START: u32 = SYSTEM_RAM_END + 1;
    const IO_CHANNEL_RAM_END: u32 = IO_CHANNEL_RAM_START + (IO_CHANNEL_RAM as u32) - 1;

    const OPTIONAL_MB_ROM_START: u32 = CASSETTE_BASIC_START - (ROM_SIZE as u32);
    const OPTIONAL_MB_ROM_END: u32 = CASSETTE_BASIC_START - 1;

    const CASSETTE_BASIC_START: u32 = BIOS_ROM_START - (CASSETTE_BASIC_ROMS * ROM_SIZE) as u32;
    const CASSETTE_BASIC_END: u32 = BIOS_ROM_START - 1;

    const BIOS_ROM_START: u32 = ADDR_BOUND - (ROM_SIZE as u32);
    const BIOS_ROM_END: u32 = ADDR_BOUND - 1;

    match addr.0 {
        SYSTEM_RAM_START..=SYSTEM_RAM_END => system_ram(addr.0 as usize),
        IO_CHANNEL_RAM_START..=IO_CHANNEL_RAM_END => {
            io_channel_ram((addr.0 - IO_CHANNEL_RAM_START) as usize)
        }
        0xB0000..=0xB3FFF => panic!("Monochrome"),
        0xB8000..=0xBBFFF => panic!("Color/Graphics"),
        0xA0000..=0xBFFFF => panic!("128K Reserved"),
        0xC0000..=0xC7FFF => panic!("Reserved for any video card's BIOS expansion ROM"),
        0xC8000..=0xCBFFF => panic!("Fixed Disk Control"),
        0xCC000..=0xEFFFF => panic!("ROM Expansion and Control"),
        0xF0000..=0xF3FFF => panic!("Reserved"),
        OPTIONAL_MB_ROM_START..=OPTIONAL_MB_ROM_END => panic!("Optional motherboard ROM"),
        CASSETTE_BASIC_START..=CASSETTE_BASIC_END => {
            cassette_basic((addr.0 - CASSETTE_BASIC_START) as usize)
        }
        BIOS_ROM_START..=BIOS_ROM_END => bios_rom((addr.0 - BIOS_ROM_START) as usize),
        ADDR_BOUND.. => unreachable!(),
    }
}

/// The memory available in a system.
pub(super) struct Memory {
    /// Memory on the system board.
    system_ram: [u8; SYSTEM_RAM],
    /// Memory plugged into I/O ports.
    ///
    /// This is temporarily hard-coded until we have I/O support.
    io_channel_ram: [u8; IO_CHANNEL_RAM],
    /// Optional Cassette BASIC ROMs.
    cassette_basic: Option<[Rom; CASSETTE_BASIC_ROMS]>,
    /// Motherboard BIOS ROM.
    bios_rom: Rom,
}

impl Memory {
    /// Sets up the system memory.
    pub(super) fn new(bios_rom: Rom, cassette_basic: Option<[Rom; CASSETTE_BASIC_ROMS]>) -> Self {
        Self {
            system_ram: [0; SYSTEM_RAM],
            io_channel_ram: [0; IO_CHANNEL_RAM],
            cassette_basic,
            bios_rom,
        }
    }

    /// Sets up the system memory without a BIOS ROM.
    #[cfg(test)]
    pub(super) fn without_bios() -> Self {
        Self::new(Rom::new([0; ROM_SIZE]), None)
    }

    /// Obtains a view over the memory starting from the given address.
    ///
    /// The view ends at the end of the logical component into which the address points.
    /// For example, an address in the main system RAM will return a slice that does not
    /// include the I/O channel RAM or the BIOS.
    ///
    /// This is a temporary method for use with `iced-x86` for decoding instructions.
    pub(super) fn view(&self, addr: PhysicalAddress) -> &[u8] {
        map_memory(
            addr,
            |offset| &self.system_ram[offset..],
            |offset| &self.io_channel_ram[offset..],
            |offset| {
                let rom_idx = offset / ROM_SIZE;
                let rom_offset = offset % ROM_SIZE;
                self.cassette_basic.as_ref().expect("Cassette BASIC")[rom_idx].view(rom_offset)
            },
            |offset| self.bios_rom.view(offset),
        )
    }

    /// Reads the byte at the given memory address.
    pub(super) fn read(&self, addr: PhysicalAddress) -> u8 {
        map_memory(
            addr,
            |offset| self.system_ram[offset],
            |offset| self.io_channel_ram[offset],
            |offset| {
                let rom_idx = offset / ROM_SIZE;
                let rom_offset = offset % ROM_SIZE;
                self.cassette_basic.as_ref().expect("Cassette BASIC")[rom_idx].read(rom_offset)
            },
            |offset| self.bios_rom.read(offset),
        )
    }

    /// Writes the given data to the given memory address.
    pub(super) fn write(&mut self, addr: PhysicalAddress, data: u8) {
        map_memory(
            addr,
            |offset| self.system_ram[offset] = data,
            |offset| self.io_channel_ram[offset] = data,
            |_| panic!("Cannot write to Cassette BASIC"),
            |_| panic!("Cannot write to BIOS"),
        )
    }
}