use crate::mmixal::MMixInstruction;
use std::collections::HashMap;
use tracing::debug;
use crate::encode::encode_instruction_bytes;
pub const MM: u8 = 0x98;
#[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>,
}
impl MmoGenerator {
pub fn new(instructions: Vec<(u64, MMixInstruction)>, labels: HashMap<String, u64>) -> Self {
Self {
instructions,
labels,
}
}
pub fn generate(&self) -> Vec<u8> {
debug!("Generating MMIX object code (.mmo format)");
let mut mmo = Vec::new();
self.emit_simple_record(&mut mmo, MmoRecordType::LopPre, 1);
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 = match current_loc {
None => true,
Some(loc) => {
addr != loc || pending_bytes.len() + bytes.len() > 252
}
};
if need_new_loc {
if !pending_bytes.is_empty() {
self.emit_lop_quote(&mut mmo, &pending_bytes);
pending_bytes.clear();
}
self.emit_lop_loc(&mut mmo, addr);
}
pending_bytes.extend_from_slice(&bytes);
current_loc = Some(addr + bytes.len() as u64);
}
if !pending_bytes.is_empty() {
self.emit_lop_quote(&mut mmo, &pending_bytes);
}
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_simple_record(&self, mmo: &mut Vec<u8>, lopcode: MmoRecordType, yz: u16) {
mmo.push(MM); mmo.push(lopcode as u8); mmo.push((yz >> 8) as u8); mmo.push((yz & 0xFF) as u8); }
fn emit_lop_quote(&self, mmo: &mut Vec<u8>, bytes: &[u8]) {
for chunk in bytes.chunks(1020) {
let chunk_tetras = chunk.len().div_ceil(4);
let yz = chunk_tetras as u16;
mmo.push(MM); mmo.push(MmoRecordType::LopQuote as u8); mmo.push((yz >> 8) as u8); mmo.push((yz & 0xFF) as u8);
mmo.extend_from_slice(chunk);
let padding = (4 - (chunk.len() % 4)) % 4;
for _ in 0..padding {
mmo.push(0);
}
}
}
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) {
mmo.push(MM); mmo.push(MmoRecordType::LopPost as u8); mmo.push(0x00); mmo.push(0xFF);
let high = (entry_point >> 32) as u32;
let low = (entry_point & 0xFFFF_FFFF) as u32;
mmo.extend_from_slice(&high.to_be_bytes());
mmo.extend_from_slice(&low.to_be_bytes());
}
}
pub struct MmoDecoder {
data: Vec<u8>,
}
impl MmoDecoder {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn decode<F>(&self, mut write_byte: F) -> u64
where
F: FnMut(u64, u8),
{
debug!("Decoding MMIX object code (.mmo format)");
let mut entry_point = 0x100u64; let mut i = 0;
let mut current_addr = 0u64;
while i < self.data.len() {
if i >= self.data.len() {
break;
}
if self.data[i] != MM {
debug!(
"Unexpected byte (not MM escape) at offset 0x{:X}: 0x{:02X}",
i, self.data[i]
);
i += 1;
continue;
}
i += 1;
if i >= self.data.len() {
break;
}
let lopcode_byte = self.data[i];
i += 1;
match MmoRecordType::try_from(lopcode_byte) {
Ok(MmoRecordType::LopQuote) => {
if i + 2 > self.data.len() {
break;
}
let yz = ((self.data[i] as usize) << 8) | (self.data[i + 1] as usize);
i += 2;
let byte_count = yz * 4;
debug!(
"lop_quote: loading {} bytes at 0x{:X}",
byte_count, current_addr
);
for offset in 0..byte_count {
if i + offset < self.data.len() {
write_byte(current_addr + offset as u64, self.data[i + offset]);
}
}
current_addr += byte_count as u64;
i += byte_count;
}
Ok(MmoRecordType::LopLoc) => {
if i + 2 > self.data.len() {
break;
}
let _yz = ((self.data[i] as u16) << 8) | (self.data[i + 1] as u16);
i += 2;
if i + 8 > self.data.len() {
break;
}
let high = u32::from_be_bytes([
self.data[i],
self.data[i + 1],
self.data[i + 2],
self.data[i + 3],
]);
let low = u32::from_be_bytes([
self.data[i + 4],
self.data[i + 5],
self.data[i + 6],
self.data[i + 7],
]);
current_addr = ((high as u64) << 32) | (low as u64);
debug!("lop_loc: set address to 0x{:X}", current_addr);
i += 8;
}
Ok(MmoRecordType::LopPre) => {
if i + 2 > self.data.len() {
break;
}
i += 2; }
Ok(MmoRecordType::LopPost) => {
if i + 2 > self.data.len() {
break;
}
let _y = self.data[i];
let start = self.data[i + 1] as usize;
i += 2;
let mut reg_index = start;
while reg_index <= 255 {
if i + 8 > self.data.len() {
i = self.data.len();
break;
}
let high = u32::from_be_bytes([
self.data[i],
self.data[i + 1],
self.data[i + 2],
self.data[i + 3],
]);
let low = u32::from_be_bytes([
self.data[i + 4],
self.data[i + 5],
self.data[i + 6],
self.data[i + 7],
]);
let value = ((high as u64) << 32) | (low as u64);
if reg_index == 255 {
entry_point = value;
}
i += 8;
reg_index += 1;
}
}
Ok(MmoRecordType::LopSkip) => {
if i + 2 > self.data.len() {
break;
}
let yz = ((self.data[i] as u64) << 8) | (self.data[i + 1] as u64);
current_addr += yz * 4; i += 2;
}
Ok(MmoRecordType::LopEnd) => {
break;
}
Ok(_) => {
debug!(
"Unhandled lopcode: {:?}",
MmoRecordType::try_from(lopcode_byte)
);
if i + 2 <= self.data.len() {
i += 2;
} else {
break;
}
}
Err(e) => {
debug!("Error: {}", e);
if i + 2 <= self.data.len() {
i += 2;
} else {
break;
}
}
}
}
debug!("Decoded .mmo file, entry point: 0x{:X}", entry_point);
entry_point
}
}
#[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 test_mmo_format_debug() {
let instructions = vec![(0x100, MMixInstruction::SET(1, 42))];
let labels = HashMap::new();
let generator = MmoGenerator::new(instructions, labels);
let mmo_data = generator.generate();
println!("Generated MMO bytes ({} total):", mmo_data.len());
for (i, &byte) in mmo_data.iter().enumerate() {
print!("{:02X} ", byte);
if (i + 1) % 16 == 0 {
println!();
}
}
println!();
let decoder = MmoDecoder::new(mmo_data);
let mut memory = HashMap::new();
let entry_point = decoder.decode(|addr, byte| {
println!("Loaded byte 0x{:02X} at address 0x{:X}", byte, addr);
memory.insert(addr, byte);
});
println!("Entry point: 0x{:X}", entry_point);
println!("Memory size: {}", memory.len());
}
#[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);
});
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);
});
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);
});
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);
});
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);
});
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 test_mmo_lop_quote_format() {
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();
let quote_pos = mmo_data
.windows(2)
.position(|w| w[0] == MM && w[1] == MmoRecordType::LopQuote as u8)
.expect("Should have QUOTE record");
assert_eq!(mmo_data[quote_pos], MM);
assert_eq!(mmo_data[quote_pos + 1], MmoRecordType::LopQuote as u8);
let yz = ((mmo_data[quote_pos + 2] as u16) << 8) | (mmo_data[quote_pos + 3] as u16);
assert_eq!(yz, 1);
}
#[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);
});
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
);
}
}
}
}