use crate::mmix::{MMix, SpecialReg};
use crate::mmixal::MMixInstruction;
use std::cell::RefCell;
use std::collections::HashMap;
use tracing::debug;
use crate::encode::encode_instruction_bytes;
pub const MM: u8 = 0x98;
pub const LOP_SPEC_DEBUG_STRING: u16 = 0x4442;
pub(crate) fn derive_rg(greg_inits: &[(u8, u64)]) -> u8 {
greg_inits
.iter()
.map(|&(reg, _)| reg)
.min()
.map_or(255, |min_reg| std::cmp::max(min_reg, 32))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[allow(clippy::enum_variant_names)]
pub enum MmoRecordType {
LopQuote = 0,
LopLoc = 1,
LopSkip = 2,
LopFixo = 3,
LopFixr = 4,
LopFixrx = 5,
LopFile = 6,
LopLine = 7,
LopSpec = 8,
LopPre = 9,
LopPost = 10,
LopStab = 11,
LopEnd = 12,
}
impl TryFrom<u8> for MmoRecordType {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x0 => Ok(MmoRecordType::LopQuote),
0x1 => Ok(MmoRecordType::LopLoc),
0x2 => Ok(MmoRecordType::LopSkip),
0x3 => Ok(MmoRecordType::LopFixo),
0x4 => Ok(MmoRecordType::LopFixr),
0x5 => Ok(MmoRecordType::LopFixrx),
0x6 => Ok(MmoRecordType::LopFile),
0x7 => Ok(MmoRecordType::LopLine),
0x8 => Ok(MmoRecordType::LopSpec),
0x9 => Ok(MmoRecordType::LopPre),
0xa => Ok(MmoRecordType::LopPost),
0xb => Ok(MmoRecordType::LopStab),
0xc => Ok(MmoRecordType::LopEnd),
_ => Err(format!("Unknown MMO lopcode: 0x{:02X}", value)),
}
}
}
pub struct MmoGenerator {
instructions: Vec<(u64, MMixInstruction)>,
labels: HashMap<String, u64>,
debug_strings: Vec<Vec<u8>>,
greg_inits: Vec<(u8, u64)>,
}
impl MmoGenerator {
pub fn new(instructions: Vec<(u64, MMixInstruction)>, labels: HashMap<String, u64>) -> Self {
Self {
instructions,
labels,
debug_strings: Vec::new(),
greg_inits: Vec::new(),
}
}
pub fn with_debug_strings(mut self, strings: Vec<Vec<u8>>) -> Self {
self.debug_strings = strings;
self
}
pub fn with_greg_inits(mut self, inits: Vec<(u8, u64)>) -> Self {
self.greg_inits = inits;
self
}
pub fn generate(&self) -> Vec<u8> {
debug!("Generating MMIX object code (.mmo format)");
let mut mmo = Vec::new();
self.emit_preamble(&mut mmo);
let mut sorted_instructions: Vec<_> = self.instructions.iter().collect();
sorted_instructions.sort_by_key(|(addr, _)| *addr);
let mut current_loc: Option<u64> = None;
let mut pending_bytes = Vec::new();
for (addr, instruction) in sorted_instructions {
let addr = *addr;
let bytes = encode_instruction_bytes(instruction);
let need_new_loc = current_loc != Some(addr);
if need_new_loc {
if !pending_bytes.is_empty() {
self.emit_data_tetras(&mut mmo, &pending_bytes);
pending_bytes.clear();
}
let aligned = addr & !3;
self.emit_lop_loc(&mut mmo, aligned);
pending_bytes.resize((addr - aligned) as usize, 0);
}
pending_bytes.extend_from_slice(&bytes);
current_loc = Some(addr.wrapping_add(bytes.len() as u64));
}
if !pending_bytes.is_empty() {
self.emit_data_tetras(&mut mmo, &pending_bytes);
}
for text in &self.debug_strings {
self.emit_debug_string(&mut mmo, text);
}
let entry_point = self
.labels
.get("Main")
.or_else(|| {
self.instructions
.iter()
.find(|(addr, _)| *addr < 0x2000000000000000)
.map(|(addr, _)| addr)
})
.copied()
.unwrap_or(0x100);
self.emit_lop_post(&mut mmo, entry_point);
debug!("Generated {} bytes of .mmo object code", mmo.len());
mmo
}
fn emit_preamble(&self, mmo: &mut Vec<u8>) {
mmo.push(MM);
mmo.push(MmoRecordType::LopPre as u8);
mmo.push(0x01); mmo.push(0x01); mmo.extend_from_slice(&0u32.to_be_bytes());
}
fn emit_tetra(mmo: &mut Vec<u8>, tetra: &[u8; 4]) {
if tetra[0] == MM {
mmo.push(MM);
mmo.push(MmoRecordType::LopQuote as u8);
mmo.push(0x00);
mmo.push(0x01);
}
mmo.extend_from_slice(tetra);
}
fn emit_data_tetras(&self, mmo: &mut Vec<u8>, bytes: &[u8]) {
for chunk in bytes.chunks(4) {
let mut tetra = [0u8; 4];
tetra[..chunk.len()].copy_from_slice(chunk);
Self::emit_tetra(mmo, &tetra);
}
}
fn emit_debug_string(&self, mmo: &mut Vec<u8>, text: &[u8]) {
mmo.push(MM);
mmo.push(MmoRecordType::LopSpec as u8);
mmo.push((LOP_SPEC_DEBUG_STRING >> 8) as u8);
mmo.push((LOP_SPEC_DEBUG_STRING & 0xFF) as u8);
let mut payload = (text.len() as u32).to_be_bytes().to_vec();
payload.extend_from_slice(text);
while !payload.len().is_multiple_of(4) {
payload.push(0);
}
for tetra in payload.as_chunks::<4>().0 {
Self::emit_tetra(mmo, tetra);
}
}
fn emit_lop_loc(&self, mmo: &mut Vec<u8>, addr: u64) {
mmo.push(MM);
mmo.push(MmoRecordType::LopLoc as u8);
mmo.push(0x00);
mmo.push(0x02);
let high = (addr >> 32) as u32;
mmo.extend_from_slice(&high.to_be_bytes());
let low = (addr & 0xFFFFFFFF) as u32;
mmo.extend_from_slice(&low.to_be_bytes());
}
fn emit_lop_post(&self, mmo: &mut Vec<u8>, entry_point: u64) {
let g = derive_rg(&self.greg_inits);
let mut values: HashMap<u8, u64> = self.greg_inits.iter().copied().collect();
values.insert(255, entry_point);
mmo.push(MM);
mmo.push(MmoRecordType::LopPost as u8);
mmo.push(0x00); mmo.push(g);
for reg in g..=255u8 {
let value = values.get(®).copied().unwrap_or(0);
mmo.extend_from_slice(&((value >> 32) as u32).to_be_bytes());
mmo.extend_from_slice(&(value as u32).to_be_bytes());
}
}
}
pub struct MmoDecoder {
data: Vec<u8>,
debug_strings: RefCell<Vec<Vec<u8>>>,
}
impl MmoDecoder {
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
debug_strings: RefCell::new(Vec::new()),
}
}
pub fn debug_strings(&self) -> Vec<Vec<u8>> {
self.debug_strings.borrow().clone()
}
fn be_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes(
*bytes
.first_chunk::<4>()
.expect("caller passes exactly 4 bytes"),
)
}
fn write_tetra<F: FnMut(u64, u8)>(write_byte: &mut F, data: &[u8], i: usize, addr: u64) {
for k in 0..4u64 {
write_byte(addr + k, data[i + k as usize]);
}
}
fn read_payload_tetra(data: &[u8], pos: usize) -> Result<Option<([u8; 4], usize)>, String> {
if pos + 4 > data.len() {
return Ok(None);
}
let tetra_at = |offset: usize| *data[offset..offset + 4].first_chunk::<4>().unwrap();
if data[pos] == MM {
let lopcode_byte = data[pos + 1];
let (y, z) = (data[pos + 2], data[pos + 3]);
if lopcode_byte != MmoRecordType::LopQuote as u8 || y != 0x00 || z != 0x01 {
return Err(Self::unsupported_lopcode(lopcode_byte, pos));
}
let quoted = pos + 4;
if quoted + 4 > data.len() {
return Ok(None);
}
Ok(Some((tetra_at(quoted), 8)))
} else {
Ok(Some((tetra_at(pos), 4)))
}
}
fn unsupported_lopcode(lopcode: u8, offset: usize) -> String {
format!(".mmo: unsupported lopcode #{lopcode:02x} at offset {offset:#x}")
}
fn data_past_end(offset: usize) -> String {
format!(".mmo: data past #FFFFFFFFFFFFFFFF at offset {offset:#x}")
}
fn truncated(offset: usize) -> String {
format!(".mmo: file ends inside a record at offset {offset:#x}")
}
fn parse_preamble(data: &[u8]) -> Result<(), String> {
if data.len() < 4 || data[0] != MM || data[1] != MmoRecordType::LopPre as u8 {
return Err(".mmo: file does not begin with lop_pre".to_string());
}
let (version, pre_z) = (data[2], data[3]);
if version != 1 {
return Err(format!(".mmo: lop_pre version {version}, expected 1"));
}
if pre_z != 1 {
return Err(Self::unsupported_lopcode(MmoRecordType::LopPre as u8, 0));
}
if data.len() < 8 {
return Err(Self::truncated(0));
}
Ok(())
}
fn parse_data_tetra<F: FnMut(u64, u8)>(
data: &[u8],
i: usize,
current_addr: u64,
write_byte: &mut F,
) -> Result<usize, String> {
if i + 4 > data.len() {
return Err(Self::truncated(i));
}
Self::write_tetra(write_byte, data, i, current_addr);
Ok(4)
}
fn parse_lop_quote<F: FnMut(u64, u8)>(
data: &[u8],
record_offset: usize,
y: u8,
z: u8,
current_addr: u64,
write_byte: &mut F,
) -> Result<usize, String> {
let yz = ((y as u16) << 8) | z as u16;
if yz != 1 {
return Err(format!(
".mmo: lop_quote count {yz}, expected 1 at offset {record_offset:#x}"
));
}
let i = record_offset + 4;
if i + 4 > data.len() {
return Err(Self::truncated(record_offset));
}
Self::write_tetra(write_byte, data, i, current_addr);
Ok(8)
}
fn parse_lop_loc(
data: &[u8],
record_offset: usize,
y: u8,
z: u8,
) -> Result<(u64, usize), String> {
if y != 0x00 || z != 0x02 {
return Err(Self::unsupported_lopcode(
MmoRecordType::LopLoc as u8,
record_offset,
));
}
let i = record_offset + 4;
if i + 8 > data.len() {
return Err(Self::truncated(record_offset));
}
let high = Self::be_u32(&data[i..i + 4]);
let low = Self::be_u32(&data[i + 4..i + 8]);
let addr = ((high as u64) << 32) | low as u64;
if !addr.is_multiple_of(4) {
return Err(Self::unsupported_lopcode(
MmoRecordType::LopLoc as u8,
record_offset,
));
}
Ok((addr, 12))
}
fn lop_spec_padded_len(byte_len: usize, bytes_left: usize) -> Option<usize> {
(byte_len <= bytes_left).then(|| byte_len.div_ceil(4) * 4)
}
fn parse_lop_spec(
&self,
data: &[u8],
record_offset: usize,
y: u8,
z: u8,
) -> Result<usize, String> {
let record_type = ((y as u16) << 8) | z as u16;
if record_type != LOP_SPEC_DEBUG_STRING {
return Err(Self::unsupported_lopcode(
MmoRecordType::LopSpec as u8,
record_offset,
));
}
let mut i = record_offset + 4;
let (len_tetra, consumed) = match Self::read_payload_tetra(data, i)? {
Some(pair) => pair,
None => return Err(Self::truncated(record_offset)),
};
i += consumed;
let byte_len = u32::from_be_bytes(len_tetra) as usize;
let Some(padded_len) = Self::lop_spec_padded_len(byte_len, data.len().saturating_sub(i))
else {
return Err(Self::truncated(record_offset));
};
let mut payload = Vec::with_capacity(padded_len);
let mut remaining = padded_len;
while remaining > 0 {
let (tetra, consumed) = match Self::read_payload_tetra(data, i)? {
Some(pair) => pair,
None => return Err(Self::truncated(record_offset)),
};
i += consumed;
payload.extend_from_slice(&tetra);
remaining -= 4;
}
payload.truncate(byte_len);
self.debug_strings.borrow_mut().push(payload);
Ok(i - record_offset)
}
fn parse_lop_post(
data: &[u8],
record_offset: usize,
y: u8,
z: u8,
) -> Result<(u64, u8, HashMap<u8, u64>), String> {
if y != 0x00 {
return Err(Self::unsupported_lopcode(
MmoRecordType::LopPost as u8,
record_offset,
));
}
let g = z;
if g < 32 {
return Err(format!(
".mmo: lop_post G={g}, below 32 at offset {record_offset:#x}"
));
}
let mut i = record_offset + 4;
let mut registers = HashMap::new();
let mut reg = g;
loop {
if i + 8 > data.len() {
return Err(Self::truncated(record_offset));
}
let high = Self::be_u32(&data[i..i + 4]);
let low = Self::be_u32(&data[i + 4..i + 8]);
registers.insert(reg, ((high as u64) << 32) | low as u64);
i += 8;
if reg == 255 {
break;
}
reg += 1;
}
let entry_point = registers.get(&255).copied().unwrap_or(0);
debug!("Decoded .mmo file, entry point: 0x{:X}", entry_point);
Ok((entry_point, g, registers))
}
fn parse<F>(&self, write_byte: &mut F) -> Result<(u64, u8, HashMap<u8, u64>), String>
where
F: FnMut(u64, u8),
{
debug!("Decoding MMIX object code (.mmo format)");
self.debug_strings.borrow_mut().clear();
let data = &self.data;
Self::parse_preamble(data)?;
let mut i = 8usize;
let mut current_addr = Some(0u64);
loop {
if i >= data.len() {
return Err(".mmo: no lop_post".to_string());
}
if data[i] != MM {
let Some(addr) = current_addr else {
return Err(Self::data_past_end(i));
};
let next = addr.checked_add(4);
let consumed = Self::parse_data_tetra(data, i, addr, write_byte)?;
current_addr = next;
i += consumed;
continue;
}
let record_offset = i;
if i + 4 > data.len() {
return Err(Self::truncated(record_offset));
}
let lopcode_byte = data[i + 1];
let (y, z) = (data[i + 2], data[i + 3]);
match MmoRecordType::try_from(lopcode_byte) {
Ok(MmoRecordType::LopQuote) => {
let Some(addr) = current_addr else {
return Err(Self::data_past_end(record_offset));
};
let next = addr.checked_add(4);
let consumed =
Self::parse_lop_quote(data, record_offset, y, z, addr, write_byte)?;
current_addr = next;
i = record_offset + consumed;
}
Ok(MmoRecordType::LopLoc) => {
let (addr, consumed) = Self::parse_lop_loc(data, record_offset, y, z)?;
current_addr = Some(addr);
i = record_offset + consumed;
}
Ok(MmoRecordType::LopSpec) => {
i = record_offset + self.parse_lop_spec(data, record_offset, y, z)?;
}
Ok(MmoRecordType::LopPost) => {
return Self::parse_lop_post(data, record_offset, y, z);
}
_ => return Err(Self::unsupported_lopcode(lopcode_byte, record_offset)),
}
}
}
pub fn decode<F>(&self, mut write_byte: F) -> Result<u64, String>
where
F: FnMut(u64, u8),
{
self.parse(&mut write_byte).map(|(entry, _, _)| entry)
}
pub fn load(&self, mmix: &mut MMix) -> Result<u64, String> {
let mut buffered: Vec<(u64, u8)> = Vec::new();
let mut collect = |addr: u64, byte: u8| buffered.push((addr, byte));
let (entry, g, registers) = self.parse(&mut collect)?;
for (addr, byte) in buffered {
mmix.write_loaded_byte(addr, byte);
}
mmix.set_debug_strings(self.debug_strings());
mmix.set_special(SpecialReg::RG, g as u64);
for reg in g..=255u8 {
mmix.set_register(reg, registers.get(®).copied().unwrap_or(0));
}
Ok(entry)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mmixal::MMixInstruction;
#[test]
fn test_record_type_enum() {
assert_eq!(MmoRecordType::try_from(0).unwrap(), MmoRecordType::LopQuote);
assert_eq!(MmoRecordType::try_from(1).unwrap(), MmoRecordType::LopLoc);
assert_eq!(MmoRecordType::try_from(2).unwrap(), MmoRecordType::LopSkip);
assert_eq!(MmoRecordType::try_from(9).unwrap(), MmoRecordType::LopPre);
assert_eq!(MmoRecordType::try_from(10).unwrap(), MmoRecordType::LopPost);
assert_eq!(MmoRecordType::try_from(12).unwrap(), MmoRecordType::LopEnd);
assert!(MmoRecordType::try_from(13).is_err());
assert!(MmoRecordType::try_from(255).is_err());
}
#[test]
fn test_mm_escape_code() {
assert_eq!(MM, 0x98);
}
#[test]
fn preamble_is_version_one_with_a_zero_timestamp() {
let mmo_data = MmoGenerator::new(Vec::new(), HashMap::new()).generate();
assert_eq!(&mmo_data[0..8], &[0x98, 0x09, 0x01, 0x01, 0, 0, 0, 0]);
}
#[test]
fn test_mmo_encode_decode_simple() {
let instructions = vec![(0x100, MMixInstruction::SETL(1, 42))];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
assert_eq!(mmo_data[0], MM);
assert_eq!(mmo_data[1], MmoRecordType::LopPre as u8);
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
let entry_point = decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(entry_point, 0x100);
assert_eq!(memory.get(&0x100), Some(&0xE3));
assert_eq!(memory.get(&0x101), Some(&0x01));
assert_eq!(memory.get(&0x102), Some(&0x00));
assert_eq!(memory.get(&0x103), Some(&0x2A));
}
#[test]
fn test_mmo_encode_decode_multiple_instructions() {
let instructions = vec![
(0x100, MMixInstruction::SETL(1, 10)),
(0x104, MMixInstruction::SETL(2, 20)),
(0x108, MMixInstruction::ADD(3, 1, 2)),
];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(memory.len(), 12); assert!(memory.contains_key(&0x100));
assert!(memory.contains_key(&0x104));
assert!(memory.contains_key(&0x108));
}
#[test]
fn test_mmo_encode_decode_non_contiguous() {
let instructions = vec![
(0x100, MMixInstruction::SET(1, 1)),
(0x200, MMixInstruction::SET(2, 2)),
(0x300, MMixInstruction::SET(3, 3)),
];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
let loc_count = mmo_data
.windows(2)
.filter(|w| w[0] == MM && w[1] == MmoRecordType::LopLoc as u8)
.count();
assert_eq!(loc_count, 3);
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert!(memory.contains_key(&0x100));
assert!(memory.contains_key(&0x200));
assert!(memory.contains_key(&0x300));
}
#[test]
fn test_mmo_encode_decode_with_main_label() {
let instructions = vec![(0x1000, MMixInstruction::SET(1, 99))];
let mut labels = HashMap::new();
labels.insert("Main".to_string(), 0x1000);
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
let entry_point = decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(entry_point, 0x1000);
}
#[test]
fn test_mmo_encode_decode_64bit_addresses() {
let instructions = vec![
(0x100, MMixInstruction::SET(1, 1)),
(0x2000000000000000, MMixInstruction::BYTE(65)), ];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert!(memory.contains_key(&0x100));
assert!(memory.contains_key(&0x2000000000000000));
assert_eq!(memory.get(&0x2000000000000000), Some(&65));
}
#[test]
fn test_mmo_lop_loc_format() {
let instructions = vec![(0x123456789ABCDEF0, MMixInstruction::SET(1, 1))];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
let loc_pos = mmo_data
.windows(2)
.position(|w| w[0] == MM && w[1] == MmoRecordType::LopLoc as u8)
.expect("Should have LOC record");
assert_eq!(mmo_data[loc_pos], MM);
assert_eq!(mmo_data[loc_pos + 1], MmoRecordType::LopLoc as u8);
assert_eq!(mmo_data[loc_pos + 2], 0x00); assert_eq!(mmo_data[loc_pos + 3], 0x02);
let high = u32::from_be_bytes([
mmo_data[loc_pos + 4],
mmo_data[loc_pos + 5],
mmo_data[loc_pos + 6],
mmo_data[loc_pos + 7],
]);
let low = u32::from_be_bytes([
mmo_data[loc_pos + 8],
mmo_data[loc_pos + 9],
mmo_data[loc_pos + 10],
mmo_data[loc_pos + 11],
]);
let reconstructed_addr = ((high as u64) << 32) | (low as u64);
assert_eq!(reconstructed_addr, 0x123456789ABCDEF0);
}
#[test]
fn a_partial_trailing_tetra_is_zero_padded_plain_data() {
let instructions = vec![
(0x100, MMixInstruction::BYTE(1)),
(0x101, MMixInstruction::BYTE(2)),
(0x102, MMixInstruction::BYTE(3)),
];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
assert!(
!mmo_data
.windows(2)
.any(|w| w[0] == MM && w[1] == MmoRecordType::LopQuote as u8),
"no byte here collides with MM, so no escape should appear"
);
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(memory.get(&0x100), Some(&1));
assert_eq!(memory.get(&0x101), Some(&2));
assert_eq!(memory.get(&0x102), Some(&3));
assert_eq!(memory.get(&0x103), Some(&0), "the trailing pad byte is 0");
}
#[test]
fn a_data_tetra_beginning_with_mm_is_escaped() {
let instructions = vec![
(0x100, MMixInstruction::BYTE(MM)),
(0x101, MMixInstruction::BYTE(0x01)),
(0x102, MMixInstruction::BYTE(0x02)),
(0x103, MMixInstruction::BYTE(0x03)),
];
let generator = MmoGenerator::new(instructions, HashMap::new());
let mmo_data = generator.generate();
assert!(
mmo_data
.windows(4)
.any(|w| w == [MM, MmoRecordType::LopQuote as u8, 0x00, 0x01])
);
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(memory.get(&0x100), Some(&MM));
assert_eq!(memory.get(&0x101), Some(&0x01));
assert_eq!(memory.get(&0x102), Some(&0x02));
assert_eq!(memory.get(&0x103), Some(&0x03));
}
#[test]
fn an_unaligned_byte_lands_at_its_address_after_a_round_trip() {
let instructions = vec![(0x103, MMixInstruction::BYTE(0xAB))];
let generator = MmoGenerator::new(instructions, HashMap::new());
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(memory.get(&0x103), Some(&0xAB));
}
#[test]
fn every_lop_loc_the_writer_emits_is_aligned() {
let instructions = vec![
(0x101, MMixInstruction::BYTE(1)),
(0x203, MMixInstruction::BYTE(2)),
];
let generator = MmoGenerator::new(instructions, HashMap::new());
let mmo_data = generator.generate();
let loc_positions: Vec<usize> = mmo_data
.windows(2)
.enumerate()
.filter(|(_, w)| w[0] == MM && w[1] == MmoRecordType::LopLoc as u8)
.map(|(i, _)| i)
.collect();
assert_eq!(loc_positions.len(), 2, "one lop_loc per unaligned BYTE");
for pos in loc_positions {
let high = u32::from_be_bytes(mmo_data[pos + 4..pos + 8].try_into().unwrap());
let low = u32::from_be_bytes(mmo_data[pos + 8..pos + 12].try_into().unwrap());
let addr = ((high as u64) << 32) | low as u64;
assert_eq!(addr % 4, 0, "lop_loc address {addr:#x} is not aligned");
}
}
#[test]
fn an_unaligned_loc_run_lands_at_its_addresses_on_both_paths() {
use crate::mmixal::MMixAssembler;
const SOURCE: &str = "\
\tLOC\t#1001
\tBYTE\t5
\tLOC\t#1006
\tBYTE\t1,2,3,4
\tLOC\t#100
Main\tTRAP\t0,Halt,0
";
let mut asm = MMixAssembler::new(SOURCE, "<test>");
asm.parse().expect("program must assemble");
let mut mms_mmix = MMix::new();
for (addr, inst) in &asm.instructions {
let bytes = asm.encode_instruction_bytes(inst);
for (offset, &byte) in bytes.iter().enumerate() {
mms_mmix.write_loaded_byte(addr + offset as u64, byte);
}
}
let mmo_data = MmoGenerator::new(asm.instructions.clone(), asm.labels.clone()).generate();
let decoder = MmoDecoder::new(mmo_data);
let mut mmo_mmix = MMix::new();
decoder
.load(&mut mmo_mmix)
.expect("well-formed object code");
for mmix in [&mms_mmix, &mmo_mmix] {
assert_eq!(mmix.read_byte(0x1001), 5);
assert_eq!(mmix.read_byte(0x1006), 1);
assert_eq!(mmix.read_byte(0x1007), 2);
assert_eq!(mmix.read_byte(0x1008), 3);
assert_eq!(mmix.read_byte(0x1009), 4);
}
}
#[test]
fn test_mmo_roundtrip() {
let instructions = vec![
(0x100, MMixInstruction::SETL(1, 42)),
(0x104, MMixInstruction::ADD(2, 1, 1)),
(0x108, MMixInstruction::TRAP(0, 0, 0)),
];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions.clone(), labels);
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
for (addr, inst) in &instructions {
let bytes = encode_instruction_bytes(inst);
for (offset, &expected_byte) in bytes.iter().enumerate() {
assert_eq!(
memory.get(&(addr + offset as u64)),
Some(&expected_byte),
"Mismatch at address 0x{:X} offset {}",
addr,
offset
);
}
}
}
#[test]
fn test_debug_strings_round_trip_through_lop_spec_records() {
let instructions = vec![(0x100, MMixInstruction::TRAP(0, 0, 0))];
let strings = vec![b"one".to_vec(), b"two, longer".to_vec()];
let generator =
MmoGenerator::new(instructions, HashMap::new()).with_debug_strings(strings.clone());
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(decoder.debug_strings(), strings);
assert!(memory.contains_key(&0x100));
}
#[test]
fn test_debug_string_payload_starting_with_mm_is_quoted() {
let text = vec![MM, 0x02, 0x03, 0x04];
let generator =
MmoGenerator::new(Vec::new(), HashMap::new()).with_debug_strings(vec![text.clone()]);
let mmo_data = generator.generate();
assert!(
mmo_data
.windows(4)
.any(|w| w == [MM, MmoRecordType::LopQuote as u8, 0x00, 0x01])
);
let decoder = MmoDecoder::new(mmo_data);
decoder.decode(|_, _| {}).expect("well-formed object code");
assert_eq!(decoder.debug_strings(), vec![text]);
}
#[test]
fn with_greg_inits_one_greg_shapes_the_postamble() {
let mmo_data = MmoGenerator::new(Vec::new(), HashMap::new())
.with_greg_inits(vec![(254, 1000)])
.generate();
let post_pos = mmo_data
.windows(2)
.position(|w| w[0] == MM && w[1] == MmoRecordType::LopPost as u8)
.expect("must have a lop_post record");
assert_eq!(&mmo_data[post_pos..post_pos + 4], &[0x98, 0x0A, 0x00, 0xFE]);
let reg254 = u64::from_be_bytes(mmo_data[post_pos + 4..post_pos + 12].try_into().unwrap());
let reg255 = u64::from_be_bytes(mmo_data[post_pos + 12..post_pos + 20].try_into().unwrap());
assert_eq!(reg254, 1000);
assert_eq!(reg255, 0x100, "no instructions: entry falls back to 0x100");
assert_eq!(
mmo_data.len(),
post_pos + 20,
"exactly two octabytes follow"
);
}
#[test]
fn load_applies_a_hand_built_object_files_full_contents() {
let mut data = vec![MM, MmoRecordType::LopPre as u8, 0x01, 0x01];
data.extend_from_slice(&[0, 0, 0, 0]); data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x00, 0x02]);
data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0x100u32.to_be_bytes()); data.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); data.extend_from_slice(&[MM, MmoRecordType::LopQuote as u8, 0x00, 0x01]); data.extend_from_slice(&[0x98, 0x01, 0x02, 0x03]); data.extend_from_slice(&[MM, MmoRecordType::LopPost as u8, 0x00, 254]);
data.extend_from_slice(&1234u64.to_be_bytes()); data.extend_from_slice(&0x100u64.to_be_bytes());
let decoder = MmoDecoder::new(data);
let mut mmix = MMix::new();
let entry = decoder.load(&mut mmix).expect("well-formed object code");
assert_eq!(entry, 0x100);
assert_eq!(mmix.get_special(SpecialReg::RG), 254);
assert_eq!(mmix.get_register(254), 1234);
assert_eq!(mmix.get_register(255), 0x100);
assert_eq!(mmix.get_special(SpecialReg::RL), 0);
assert_eq!(mmix.read_tetra(0x100), 0xAABBCCDD);
assert_eq!(mmix.read_tetra(0x104), 0x98010203);
}
#[test]
fn load_combines_two_bytes_assembled_to_one_address_by_xor() {
use crate::mmixal::MMixAssembler;
const SOURCE: &str = "\
\tLOC\t#1000
\tBYTE\t#0f
\tLOC\t#1000
\tBYTE\t#3c
\tLOC\t#100
Main\tTRAP\t0,Halt,0
";
let mut asm = MMixAssembler::new(SOURCE, "<test>");
asm.parse().expect("program must assemble");
let mmo_data = MmoGenerator::new(asm.instructions.clone(), asm.labels.clone()).generate();
let decoder = MmoDecoder::new(mmo_data);
let mut mmix = MMix::new();
decoder.load(&mut mmix).expect("well-formed object code");
assert_eq!(mmix.read_byte(0x1000), 0x33);
}
fn assert_machine_untouched(mmix: &MMix) {
assert_eq!(mmix.get_special(SpecialReg::RG), 32);
assert_eq!(mmix.get_register(255), 0);
assert_eq!(mmix.occupied().count(), 0);
}
fn valid_preamble() -> Vec<u8> {
vec![MM, MmoRecordType::LopPre as u8, 0x01, 0x01, 0, 0, 0, 0]
}
#[test]
fn decode_rejects_a_file_that_does_not_begin_with_lop_pre() {
let data = vec![0x00, 0x01, 0x02, 0x03];
let decoder = MmoDecoder::new(data);
assert_eq!(
decoder.decode(|_, _| {}).unwrap_err(),
".mmo: file does not begin with lop_pre"
);
let mut mmix = MMix::new();
assert_eq!(
decoder.load(&mut mmix).unwrap_err(),
".mmo: file does not begin with lop_pre"
);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_pre_0_3_13_preamble_version() {
let mut data = vec![MM, MmoRecordType::LopPre as u8, 0x00, 0x01];
data.extend_from_slice(&[MM, MmoRecordType::LopPost as u8, 0x00, 0xFF]);
data.extend_from_slice(&0x100u64.to_be_bytes());
let decoder = MmoDecoder::new(data);
assert_eq!(
decoder.decode(|_, _| {}).unwrap_err(),
".mmo: lop_pre version 0, expected 1"
);
let mut mmix = MMix::new();
assert_eq!(
decoder.load(&mut mmix).unwrap_err(),
".mmo: lop_pre version 0, expected 1"
);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_lop_quote_with_a_count_other_than_one() {
let mut data = valid_preamble();
let quote_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopQuote as u8, 0x00, 0x02]);
data.extend_from_slice(&[0u8; 8]);
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: lop_quote count 2, expected 1 at offset {quote_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_foreign_shaped_lop_loc() {
let mut data = valid_preamble();
let loc_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x20, 0x01]);
data.extend_from_slice(&0x100u32.to_be_bytes());
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: unsupported lopcode #01 at offset {loc_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_an_unaligned_lop_loc() {
let mut data = valid_preamble();
let loc_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x00, 0x02]);
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(&0x101u32.to_be_bytes());
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: unsupported lopcode #01 at offset {loc_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_postamble_g_below_32() {
let mut data = valid_preamble();
let post_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopPost as u8, 0x00, 10]);
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: lop_post G=10, below 32 at offset {post_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_file_that_ends_inside_a_record() {
let data = vec![MM, MmoRecordType::LopPre as u8, 0x01, 0x01];
let decoder = MmoDecoder::new(data);
let expected = ".mmo: file ends inside a record at offset 0x0";
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_file_with_no_postamble() {
let mut data = valid_preamble();
data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x00, 0x02]);
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(&0x100u32.to_be_bytes());
data.extend_from_slice(&[1, 2, 3, 4]); let decoder = MmoDecoder::new(data);
let expected = ".mmo: no lop_post";
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
fn boundary_item_prefix() -> Vec<u8> {
let mut data = valid_preamble();
data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x00, 0x02]);
data.extend_from_slice(&0xFFFFFFFFu32.to_be_bytes());
data.extend_from_slice(&0xFFFFFFFCu32.to_be_bytes());
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
data
}
fn boundary_item_postamble() -> [u8; 12] {
[0x98, 0x0A, 0x00, 0xFF, 0, 0, 0, 0, 0, 0, 0x01, 0x00]
}
#[test]
fn decode_loads_an_item_that_fills_the_last_four_bytes_exactly() {
let mut data = boundary_item_prefix();
data.extend_from_slice(&boundary_item_postamble());
let decoder = MmoDecoder::new(data);
let mut memory = HashMap::new();
let entry = decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(entry, 0x100);
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFC), Some(&0x01));
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFD), Some(&0x02));
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFE), Some(&0x03));
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFF), Some(&0x04));
}
#[test]
fn decode_rejects_a_plain_data_tetra_past_the_end() {
let mut data = boundary_item_prefix();
let past_end_offset = data.len();
data.extend_from_slice(&[0x05, 0x06, 0x07, 0x08]);
data.extend_from_slice(&boundary_item_postamble());
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: data past #FFFFFFFFFFFFFFFF at offset {past_end_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_an_escaped_data_tetra_past_the_end() {
let mut data = boundary_item_prefix();
let record_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopQuote as u8, 0x00, 0x01]);
data.extend_from_slice(&[0x05, 0x06, 0x07, 0x08]);
data.extend_from_slice(&boundary_item_postamble());
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: data past #FFFFFFFFFFFFFFFF at offset {record_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_a_lop_loc_clears_the_past_end_state() {
let mut data = boundary_item_prefix();
data.extend_from_slice(&[MM, MmoRecordType::LopLoc as u8, 0x00, 0x02]);
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(&0x100u32.to_be_bytes());
data.extend_from_slice(&[0x05, 0x06, 0x07, 0x08]);
data.extend_from_slice(&boundary_item_postamble());
let decoder = MmoDecoder::new(data);
let mut memory = HashMap::new();
let entry = decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
assert_eq!(entry, 0x100);
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFC), Some(&0x01));
assert_eq!(memory.get(&0xFFFFFFFFFFFFFFFF), Some(&0x04));
assert_eq!(memory.get(&0x100), Some(&0x05));
assert_eq!(memory.get(&0x103), Some(&0x08));
}
#[test]
fn generate_and_decode_round_trip_an_item_at_the_top_of_memory() {
let instructions = vec![(0xFFFFFFFFFFFFFFFC, MMixInstruction::SWYM(0, 0, 0))];
let generator = MmoGenerator::new(instructions.clone(), HashMap::new());
let mmo_data = generator.generate();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
decoder
.decode(|addr, byte| {
memory.insert(addr, byte);
})
.expect("well-formed object code");
let (addr, inst) = &instructions[0];
let bytes = encode_instruction_bytes(inst);
for (offset, &expected_byte) in bytes.iter().enumerate() {
assert_eq!(memory.get(&(addr + offset as u64)), Some(&expected_byte));
}
}
#[test]
fn decode_rejects_a_lop_spec_of_another_type() {
let mut data = valid_preamble();
let spec_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopSpec as u8, 0x12, 0x34]);
data.extend_from_slice(&[0, 0, 0, 4]);
data.extend_from_slice(&[9, 9, 9, 9]);
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: unsupported lopcode #08 at offset {spec_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_lop_spec_payload_tetra_with_a_malformed_escape() {
let mut data = valid_preamble();
data.extend_from_slice(&[MM, MmoRecordType::LopSpec as u8, 0x44, 0x42]);
data.extend_from_slice(&[0, 0, 0, 4]); let bad_offset = data.len();
data.extend_from_slice(&[0x98, 0x12, 0x34, 0x56]);
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: unsupported lopcode #12 at offset {bad_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_lop_spec_length_longer_than_the_file() {
let mut data = valid_preamble();
let spec_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopSpec as u8, 0x44, 0x42]);
data.extend_from_slice(&0xFFFFFFFFu32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: file ends inside a record at offset {spec_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn lop_spec_padded_len_is_none_when_the_file_cannot_hold_the_payload() {
assert_eq!(MmoDecoder::lop_spec_padded_len(1000, 20), None);
assert_eq!(MmoDecoder::lop_spec_padded_len(21, 20), None);
assert_eq!(MmoDecoder::lop_spec_padded_len(usize::MAX, 20), None);
assert_eq!(MmoDecoder::lop_spec_padded_len(usize::MAX, 0), None);
}
#[test]
fn lop_spec_padded_len_rounds_a_fitting_payload_up_to_a_tetra() {
assert_eq!(MmoDecoder::lop_spec_padded_len(0, 0), Some(0));
assert_eq!(MmoDecoder::lop_spec_padded_len(1, 4), Some(4));
assert_eq!(MmoDecoder::lop_spec_padded_len(4, 4), Some(4));
assert_eq!(MmoDecoder::lop_spec_padded_len(5, 8), Some(8));
assert_eq!(MmoDecoder::lop_spec_padded_len(20, 20), Some(20));
}
#[test]
fn decode_rejects_a_lop_pre_with_a_tetra_count_other_than_one() {
let mut data = vec![MM, MmoRecordType::LopPre as u8, 0x01, 0x02];
data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); data.extend_from_slice(&[MM, MmoRecordType::LopPost as u8, 0x00, 0xFF]);
data.extend_from_slice(&0x100u64.to_be_bytes());
let decoder = MmoDecoder::new(data);
let expected = ".mmo: unsupported lopcode #09 at offset 0x0";
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn decode_rejects_a_lop_post_with_a_nonzero_y() {
let mut data = valid_preamble();
let post_offset = data.len();
data.extend_from_slice(&[MM, MmoRecordType::LopPost as u8, 0x01, 0xFF]);
data.extend_from_slice(&0x100u64.to_be_bytes());
let decoder = MmoDecoder::new(data);
let expected = format!(".mmo: unsupported lopcode #0a at offset {post_offset:#x}");
assert_eq!(decoder.decode(|_, _| {}).unwrap_err(), expected);
let mut mmix = MMix::new();
assert_eq!(decoder.load(&mut mmix).unwrap_err(), expected);
assert_machine_untouched(&mmix);
}
#[test]
fn generate_with_no_greg_postamble_is_one_octabyte() {
let instructions = vec![(0x100, MMixInstruction::TRAP(0, 0, 0))];
let mmo_data = MmoGenerator::new(instructions, HashMap::new()).generate();
let post_pos = mmo_data
.windows(2)
.position(|w| w[0] == MM && w[1] == MmoRecordType::LopPost as u8)
.expect("must have a lop_post record");
assert_eq!(&mmo_data[post_pos..post_pos + 4], &[0x98, 0x0A, 0x00, 0xFF]);
assert_eq!(
mmo_data.len(),
post_pos + 12,
"exactly one octabyte follows"
);
let entry = u64::from_be_bytes(mmo_data[post_pos + 4..post_pos + 12].try_into().unwrap());
assert_eq!(entry, 0x100);
}
}