use std::collections::{BTreeMap, HashMap};
#[cfg(any(unix, windows))]
use std::io::{stderr, stdin, stdout};
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
use std::path::{Path, PathBuf};
use tracing::{debug, instrument};
use crate::mmix::{STACK_SEGMENT_START, TrapCode};
use pest_derive::Parser;
use regex::Regex;
const DATA_SEGMENT_START: u64 = 0x2000000000000000;
const POOL_SEGMENT_START: u64 = 0x4000000000000000;
#[cfg(unix)]
fn stdio_raw_identifiers() -> (u64, u64, u64) {
(
stdin().as_raw_fd() as u64,
stdout().as_raw_fd() as u64,
stderr().as_raw_fd() as u64,
)
}
#[cfg(windows)]
fn stdio_raw_identifiers() -> (u64, u64, u64) {
(
stdin().as_raw_handle() as usize as u64,
stdout().as_raw_handle() as usize as u64,
stderr().as_raw_handle() as usize as u64,
)
}
#[cfg(not(any(unix, windows)))]
fn stdio_raw_identifiers() -> (u64, u64, u64) {
(0, 1, 2)
}
#[derive(Parser)]
#[grammar = "mmixal.pest"]
struct MMixalParser;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymbolType {
Register(u8), Constant(u64), }
impl std::fmt::Display for SymbolType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SymbolType::Register(reg) => write!(f, "${}", reg),
SymbolType::Constant(val) => write!(f, "{}", val),
}
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, PartialEq)]
pub enum MMixInstruction {
SET(u8, u64), SETRR(u8, u8), SETL(u8, u16), SETH(u8, u16), SETMH(u8, u16), SETML(u8, u16), INCH(u8, u16), INCMH(u8, u16), INCML(u8, u16), ORH(u8, u16), ORMH(u8, u16), ORML(u8, u16), ORL(u8, u16), ANDNH(u8, u16), ANDNMH(u8, u16), ANDNML(u8, u16), ANDNL(u8, u16),
LDB(u8, u8, u8), LDBI(u8, u8, u8), LDBU(u8, u8, u8), LDBUI(u8, u8, u8), LDW(u8, u8, u8), LDWI(u8, u8, u8), LDWU(u8, u8, u8), LDWUI(u8, u8, u8), LDT(u8, u8, u8), LDTI(u8, u8, u8), LDTU(u8, u8, u8), LDTUI(u8, u8, u8), LDO(u8, u8, u8), LDOI(u8, u8, u8), LDOU(u8, u8, u8), LDOUI(u8, u8, u8), LDUNC(u8, u8, u8), LDUNCI(u8, u8, u8), LDHT(u8, u8, u8), LDHTI(u8, u8, u8), LDSF(u8, u8, u8), LDSFI(u8, u8, u8), LDVTS(u8, u8, u8), LDVTSI(u8, u8, u8), CSWAP(u8, u8, u8), CSWAPI(u8, u8, u8), LDA(u8, u8, u8), LDAI(u8, u8, u8),
STB(u8, u8, u8), STBI(u8, u8, u8), STBU(u8, u8, u8), STBUI(u8, u8, u8), STW(u8, u8, u8), STWI(u8, u8, u8), STWU(u8, u8, u8), STWUI(u8, u8, u8), STT(u8, u8, u8), STTI(u8, u8, u8), STTU(u8, u8, u8), STTUI(u8, u8, u8), STO(u8, u8, u8), STOI(u8, u8, u8), STOU(u8, u8, u8), STOUI(u8, u8, u8), STUNC(u8, u8, u8), STUNCI(u8, u8, u8), STCO(u8, u8, u8), STCOI(u8, u8, u8), STHT(u8, u8, u8), STHTI(u8, u8, u8), STSF(u8, u8, u8), STSFI(u8, u8, u8),
ADD(u8, u8, u8), ADDI(u8, u8, u8), ADDU(u8, u8, u8), ADDUI(u8, u8, u8), ADDU2(u8, u8, u8), ADDU2I(u8, u8, u8), ADDU4(u8, u8, u8), ADDU4I(u8, u8, u8), ADDU8(u8, u8, u8), ADDU8I(u8, u8, u8), ADDU16(u8, u8, u8), ADDU16I(u8, u8, u8), SUB(u8, u8, u8), SUBI(u8, u8, u8), SUBU(u8, u8, u8), SUBUI(u8, u8, u8), NEG(u8, u8, u8), NEGI(u8, u8, u8), NEGU(u8, u8, u8), NEGUI(u8, u8, u8),
MUL(u8, u8, u8), MULI(u8, u8, u8), MULU(u8, u8, u8), MULUI(u8, u8, u8), DIV(u8, u8, u8), DIVI(u8, u8, u8), DIVU(u8, u8, u8), DIVUI(u8, u8, u8),
FCMP(u8, u8, u8), FUN(u8, u8, u8), FEQL(u8, u8, u8), FCMPE(u8, u8, u8), FUNE(u8, u8, u8), FEQLE(u8, u8, u8), FADD(u8, u8, u8), FIX(u8, u8, u8), FSUB(u8, u8, u8), FIXU(u8, u8, u8), FLOT(u8, u8, u8), FLOTI(u8, u8, u8), FLOTU(u8, u8, u8), FLOTUI(u8, u8, u8), SFLOT(u8, u8, u8), SFLOTI(u8, u8, u8), SFLOTU(u8, u8, u8), SFLOTUI(u8, u8, u8), FMUL(u8, u8, u8), FDIV(u8, u8, u8), FREM(u8, u8, u8), FSQRT(u8, u8, u8), FINT(u8, u8, u8),
CMP(u8, u8, u8), CMPI(u8, u8, u8), CMPU(u8, u8, u8), CMPUI(u8, u8, u8),
INCL(u8, u16),
AND(u8, u8, u8), ANDI(u8, u8, u8), OR(u8, u8, u8), ORI(u8, u8, u8), XOR(u8, u8, u8), XORI(u8, u8, u8), ANDN(u8, u8, u8), ANDNI(u8, u8, u8), ORN(u8, u8, u8), ORNI(u8, u8, u8), NAND(u8, u8, u8), NANDI(u8, u8, u8), NOR(u8, u8, u8), NORI(u8, u8, u8), NXOR(u8, u8, u8), NXORI(u8, u8, u8), MUX(u8, u8, u8), MUXI(u8, u8, u8),
BDIF(u8, u8, u8), BDIFI(u8, u8, u8), WDIF(u8, u8, u8), WDIFI(u8, u8, u8), TDIF(u8, u8, u8), TDIFI(u8, u8, u8), ODIF(u8, u8, u8), ODIFI(u8, u8, u8), SADD(u8, u8, u8), SADDI(u8, u8, u8), MOR(u8, u8, u8), MORI(u8, u8, u8), MXOR(u8, u8, u8), MXORI(u8, u8, u8),
SL(u8, u8, u8), SLI(u8, u8, u8), SLU(u8, u8, u8), SLUI(u8, u8, u8), SR(u8, u8, u8), SRI(u8, u8, u8), SRU(u8, u8, u8), SRUI(u8, u8, u8),
JMP(u32), JMPB(u32), BN(u8, u16), BNB(u8, u16), BZ(u8, u16), BZB(u8, u16), BP(u8, u16), BPB(u8, u16), BOD(u8, u16), BODB(u8, u16), BNN(u8, u16), BNNB(u8, u16), BNZ(u8, u16), BNZB(u8, u16), BNP(u8, u16), BNPB(u8, u16), BEV(u8, u16), BEVB(u8, u16), PBN(u8, u8, u8), PBNB(u8, u8, u8), PBZ(u8, u8, u8), PBZB(u8, u8, u8), PBP(u8, u8, u8), PBPB(u8, u8, u8), PBOD(u8, u8, u8), PBODB(u8, u8, u8), PBNN(u8, u8, u8), PBNNB(u8, u8, u8), PBNZ(u8, u8, u8), PBNZB(u8, u8, u8), PBNP(u8, u8, u8), PBNPB(u8, u8, u8), PBEV(u8, u8, u8), PBEVB(u8, u8, u8),
CSN(u8, u8, u8), CSNI(u8, u8, u8), CSZ(u8, u8, u8), CSZI(u8, u8, u8), CSP(u8, u8, u8), CSPI(u8, u8, u8), CSOD(u8, u8, u8), CSODI(u8, u8, u8), CSNN(u8, u8, u8), CSNNI(u8, u8, u8), CSNZ(u8, u8, u8), CSNZI(u8, u8, u8), CSNP(u8, u8, u8), CSNPI(u8, u8, u8), CSEV(u8, u8, u8), CSEVI(u8, u8, u8),
ZSN(u8, u8, u8), ZSNI(u8, u8, u8), ZSZ(u8, u8, u8), ZSZI(u8, u8, u8), ZSP(u8, u8, u8), ZSPI(u8, u8, u8), ZSOD(u8, u8, u8), ZSODI(u8, u8, u8), ZSNN(u8, u8, u8), ZSNNI(u8, u8, u8), ZSNZ(u8, u8, u8), ZSNZI(u8, u8, u8), ZSNP(u8, u8, u8), ZSNPI(u8, u8, u8), ZSEV(u8, u8, u8), ZSEVI(u8, u8, u8),
TRAP(u8, u8, u8), TRIP(u8, u8, u8), PUSHJ(u8, u8, u8), PUSHJB(u8, u8, u8), PUSHGO(u8, u8, u8), PUSHGOI(u8, u8, u8), POP(u8, u8, u8), GO(u8, u8, u8), GOI(u8, u8, u8), GET(u8, u8), PUT(u8, u8), PUTI(u8, u8), SAVE(u8, u8), UNSAVE(u8, u8), RESUME(u8), SYNC(u8), SWYM(u8, u8, u8), PRELD(u8, u8, u8), PRELDI(u8, u8, u8), PREGO(u8, u8, u8), PREGOI(u8, u8, u8), PREST(u8, u8, u8), PRESTI(u8, u8, u8), SYNCD(u8, u8, u8), SYNCDI(u8, u8, u8), SYNCID(u8, u8, u8), SYNCIDI(u8, u8, u8), GETA(u8, u8, u8), GETAB(u8, u8, u8),
BYTE(u8), WYDE(u16), TETRA(u32), OCTA(u64),
HALT, }
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum Opcode {
TRAP = 0x00,
FCMP = 0x01,
FUN = 0x02,
FEQL = 0x03,
FADD = 0x04,
FIX = 0x05,
FSUB = 0x06,
FIXU = 0x07,
FLOT = 0x08,
FLOTI = 0x09,
FLOTU = 0x0A,
FLOTUI = 0x0B,
SFLOT = 0x0C,
SFLOTI = 0x0D,
SFLOTU = 0x0E,
SFLOTUI = 0x0F,
FMUL = 0x10,
FCMPE = 0x11,
FUNE = 0x12,
FEQLE = 0x13,
FDIV = 0x14,
FSQRT = 0x15,
FREM = 0x16,
FINT = 0x17,
MUL = 0x18,
MULI = 0x19,
MULU = 0x1A,
MULUI = 0x1B,
DIV = 0x1C,
DIVI = 0x1D,
DIVU = 0x1E,
DIVUI = 0x1F,
ADD = 0x20,
ADDI = 0x21,
ADDU = 0x22,
ADDUI = 0x23,
SUB = 0x24,
SUBI = 0x25,
SUBU = 0x26,
SUBUI = 0x27,
ADDU2 = 0x28,
ADDU2I = 0x29,
ADDU4 = 0x2A,
ADDU4I = 0x2B,
ADDU8 = 0x2C,
ADDU8I = 0x2D,
ADDU16 = 0x2E,
ADDU16I = 0x2F,
CMP = 0x30,
CMPI = 0x31,
CMPU = 0x32,
CMPUI = 0x33,
NEG = 0x34,
NEGI = 0x35,
NEGU = 0x36,
NEGUI = 0x37,
SL = 0x38,
SLI = 0x39,
SLU = 0x3A,
SLUI = 0x3B,
SR = 0x3C,
SRI = 0x3D,
SRU = 0x3E,
SRUI = 0x3F,
BN = 0x40,
BNB = 0x41,
BZ = 0x42,
BZB = 0x43,
BP = 0x44,
BPB = 0x45,
BOD = 0x46,
BODB = 0x47,
BNN = 0x48,
BNNB = 0x49,
BNZ = 0x4A,
BNZB = 0x4B,
BNP = 0x4C,
BNPB = 0x4D,
BEV = 0x4E,
BEVB = 0x4F,
PBN = 0x50,
PBNB = 0x51,
PBZ = 0x52,
PBZB = 0x53,
PBP = 0x54,
PBPB = 0x55,
PBOD = 0x56,
PBODB = 0x57,
PBNN = 0x58,
PBNNB = 0x59,
PBNZ = 0x5A,
PBNZB = 0x5B,
PBNP = 0x5C,
PBNPB = 0x5D,
PBEV = 0x5E,
PBEVB = 0x5F,
CSN = 0x60,
CSNI = 0x61,
CSZ = 0x62,
CSZI = 0x63,
CSP = 0x64,
CSPI = 0x65,
CSOD = 0x66,
CSODI = 0x67,
CSNN = 0x68,
CSNNI = 0x69,
CSNZ = 0x6A,
CSNZI = 0x6B,
CSNP = 0x6C,
CSNPI = 0x6D,
CSEV = 0x6E,
CSEVI = 0x6F,
ZSN = 0x70,
ZSNI = 0x71,
ZSZ = 0x72,
ZSZI = 0x73,
ZSP = 0x74,
ZSPI = 0x75,
ZSOD = 0x76,
ZSODI = 0x77,
ZSNN = 0x78,
ZSNNI = 0x79,
ZSNZ = 0x7A,
ZSNZI = 0x7B,
ZSNP = 0x7C,
ZSNPI = 0x7D,
ZSEV = 0x7E,
ZSEVI = 0x7F,
LDB = 0x80,
LDBI = 0x81,
LDBU = 0x82,
LDBUI = 0x83,
LDW = 0x84,
LDWI = 0x85,
LDWU = 0x86,
LDWUI = 0x87,
LDT = 0x88,
LDTI = 0x89,
LDTU = 0x8A,
LDTUI = 0x8B,
LDO = 0x8C,
LDOI = 0x8D,
LDOU = 0x8E,
LDOUI = 0x8F,
LDSF = 0x90,
LDSFI = 0x91,
LDHT = 0x92,
LDHTI = 0x93,
CSWAP = 0x94,
CSWAPI = 0x95,
LDUNC = 0x96,
LDUNCI = 0x97,
LDVTS = 0x98,
LDVTSI = 0x99,
PRELD = 0x9A,
PRELDI = 0x9B,
PREGO = 0x9C,
PREGOI = 0x9D,
GO = 0x9E,
GOI = 0x9F,
STB = 0xA0,
STBI = 0xA1,
STBU = 0xA2,
STBUI = 0xA3,
STW = 0xA4,
STWI = 0xA5,
STWU = 0xA6,
STWUI = 0xA7,
STT = 0xA8,
STTI = 0xA9,
STTU = 0xAA,
STTUI = 0xAB,
STO = 0xAC,
STOI = 0xAD,
STOU = 0xAE,
STOUI = 0xAF,
STSF = 0xB0,
STSFI = 0xB1,
STHT = 0xB2,
STHTI = 0xB3,
STCO = 0xB4,
STCOI = 0xB5,
STUNC = 0xB6,
STUNCI = 0xB7,
SYNCD = 0xB8,
SYNCDI = 0xB9,
PREST = 0xBA,
PRESTI = 0xBB,
SYNCID = 0xBC,
SYNCIDI = 0xBD,
PUSHGO = 0xBE,
PUSHGOI = 0xBF,
OR = 0xC0,
ORI = 0xC1,
ORN = 0xC2,
ORNI = 0xC3,
NOR = 0xC4,
NORI = 0xC5,
XOR = 0xC6,
XORI = 0xC7,
AND = 0xC8,
ANDI = 0xC9,
ANDN = 0xCA,
ANDNI = 0xCB,
NAND = 0xCC,
NANDI = 0xCD,
NXOR = 0xCE,
NXORI = 0xCF,
BDIF = 0xD0,
BDIFI = 0xD1,
WDIF = 0xD2,
WDIFI = 0xD3,
TDIF = 0xD4,
TDIFI = 0xD5,
ODIF = 0xD6,
ODIFI = 0xD7,
MUX = 0xD8,
MUXI = 0xD9,
SADD = 0xDA,
SADDI = 0xDB,
MOR = 0xDC,
MORI = 0xDD,
MXOR = 0xDE,
MXORI = 0xDF,
SETH = 0xE0,
SETMH = 0xE1,
SETML = 0xE2,
SETL = 0xE3,
INCH = 0xE4,
INCMH = 0xE5,
INCML = 0xE6,
INCL = 0xE7,
ORH = 0xE8,
ORMH = 0xE9,
ORML = 0xEA,
ORL = 0xEB,
ANDNH = 0xEC,
ANDNMH = 0xED,
ANDNML = 0xEE,
ANDNL = 0xEF,
JMP = 0xF0,
JMPB = 0xF1,
PUSHJ = 0xF2,
PUSHJB = 0xF3,
GETA = 0xF4,
GETAB = 0xF5,
PUT = 0xF6,
PUTI = 0xF7,
POP = 0xF8,
RESUME = 0xF9,
SAVE = 0xFA,
UNSAVE = 0xFB,
SYNC = 0xFC,
SWYM = 0xFD,
GET = 0xFE,
TRIP = 0xFF,
}
impl TryFrom<u8> for Opcode {
type Error = String;
#[allow(unreachable_patterns)]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Opcode::TRAP),
0x01 => Ok(Opcode::FCMP),
0x02 => Ok(Opcode::FUN),
0x03 => Ok(Opcode::FEQL),
0x04 => Ok(Opcode::FADD),
0x05 => Ok(Opcode::FIX),
0x06 => Ok(Opcode::FSUB),
0x07 => Ok(Opcode::FIXU),
0x08 => Ok(Opcode::FLOT),
0x09 => Ok(Opcode::FLOTI),
0x0A => Ok(Opcode::FLOTU),
0x0B => Ok(Opcode::FLOTUI),
0x0C => Ok(Opcode::SFLOT),
0x0D => Ok(Opcode::SFLOTI),
0x0E => Ok(Opcode::SFLOTU),
0x0F => Ok(Opcode::SFLOTUI),
0x10 => Ok(Opcode::FMUL),
0x11 => Ok(Opcode::FCMPE),
0x12 => Ok(Opcode::FUNE),
0x13 => Ok(Opcode::FEQLE),
0x14 => Ok(Opcode::FDIV),
0x15 => Ok(Opcode::FSQRT),
0x16 => Ok(Opcode::FREM),
0x17 => Ok(Opcode::FINT),
0x18 => Ok(Opcode::MUL),
0x19 => Ok(Opcode::MULI),
0x1A => Ok(Opcode::MULU),
0x1B => Ok(Opcode::MULUI),
0x1C => Ok(Opcode::DIV),
0x1D => Ok(Opcode::DIVI),
0x1E => Ok(Opcode::DIVU),
0x1F => Ok(Opcode::DIVUI),
0x20 => Ok(Opcode::ADD),
0x21 => Ok(Opcode::ADDI),
0x22 => Ok(Opcode::ADDU),
0x23 => Ok(Opcode::ADDUI),
0x24 => Ok(Opcode::SUB),
0x25 => Ok(Opcode::SUBI),
0x26 => Ok(Opcode::SUBU),
0x27 => Ok(Opcode::SUBUI),
0x28 => Ok(Opcode::ADDU2),
0x29 => Ok(Opcode::ADDU2I),
0x2A => Ok(Opcode::ADDU4),
0x2B => Ok(Opcode::ADDU4I),
0x2C => Ok(Opcode::ADDU8),
0x2D => Ok(Opcode::ADDU8I),
0x2E => Ok(Opcode::ADDU16),
0x2F => Ok(Opcode::ADDU16I),
0x30 => Ok(Opcode::CMP),
0x31 => Ok(Opcode::CMPI),
0x32 => Ok(Opcode::CMPU),
0x33 => Ok(Opcode::CMPUI),
0x34 => Ok(Opcode::NEG),
0x35 => Ok(Opcode::NEGI),
0x36 => Ok(Opcode::NEGU),
0x37 => Ok(Opcode::NEGUI),
0x38 => Ok(Opcode::SL),
0x39 => Ok(Opcode::SLI),
0x3A => Ok(Opcode::SLU),
0x3B => Ok(Opcode::SLUI),
0x3C => Ok(Opcode::SR),
0x3D => Ok(Opcode::SRI),
0x3E => Ok(Opcode::SRU),
0x3F => Ok(Opcode::SRUI),
0x40 => Ok(Opcode::BN),
0x41 => Ok(Opcode::BNB),
0x42 => Ok(Opcode::BZ),
0x43 => Ok(Opcode::BZB),
0x44 => Ok(Opcode::BP),
0x45 => Ok(Opcode::BPB),
0x46 => Ok(Opcode::BOD),
0x47 => Ok(Opcode::BODB),
0x48 => Ok(Opcode::BNN),
0x49 => Ok(Opcode::BNNB),
0x4A => Ok(Opcode::BNZ),
0x4B => Ok(Opcode::BNZB),
0x4C => Ok(Opcode::BNP),
0x4D => Ok(Opcode::BNPB),
0x4E => Ok(Opcode::BEV),
0x4F => Ok(Opcode::BEVB),
0x50 => Ok(Opcode::PBN),
0x51 => Ok(Opcode::PBNB),
0x52 => Ok(Opcode::PBZ),
0x53 => Ok(Opcode::PBZB),
0x54 => Ok(Opcode::PBP),
0x55 => Ok(Opcode::PBPB),
0x56 => Ok(Opcode::PBOD),
0x57 => Ok(Opcode::PBODB),
0x58 => Ok(Opcode::PBNN),
0x59 => Ok(Opcode::PBNNB),
0x5A => Ok(Opcode::PBNZ),
0x5B => Ok(Opcode::PBNZB),
0x5C => Ok(Opcode::PBNP),
0x5D => Ok(Opcode::PBNPB),
0x5E => Ok(Opcode::PBEV),
0x5F => Ok(Opcode::PBEVB),
0x60 => Ok(Opcode::CSN),
0x61 => Ok(Opcode::CSNI),
0x62 => Ok(Opcode::CSZ),
0x63 => Ok(Opcode::CSZI),
0x64 => Ok(Opcode::CSP),
0x65 => Ok(Opcode::CSPI),
0x66 => Ok(Opcode::CSOD),
0x67 => Ok(Opcode::CSODI),
0x68 => Ok(Opcode::CSNN),
0x69 => Ok(Opcode::CSNNI),
0x6A => Ok(Opcode::CSNZ),
0x6B => Ok(Opcode::CSNZI),
0x6C => Ok(Opcode::CSNP),
0x6D => Ok(Opcode::CSNPI),
0x6E => Ok(Opcode::CSEV),
0x6F => Ok(Opcode::CSEVI),
0x70 => Ok(Opcode::ZSN),
0x71 => Ok(Opcode::ZSNI),
0x72 => Ok(Opcode::ZSZ),
0x73 => Ok(Opcode::ZSZI),
0x74 => Ok(Opcode::ZSP),
0x75 => Ok(Opcode::ZSPI),
0x76 => Ok(Opcode::ZSOD),
0x77 => Ok(Opcode::ZSODI),
0x78 => Ok(Opcode::ZSNN),
0x79 => Ok(Opcode::ZSNNI),
0x7A => Ok(Opcode::ZSNZ),
0x7B => Ok(Opcode::ZSNZI),
0x7C => Ok(Opcode::ZSNP),
0x7D => Ok(Opcode::ZSNPI),
0x7E => Ok(Opcode::ZSEV),
0x7F => Ok(Opcode::ZSEVI),
0x80 => Ok(Opcode::LDB),
0x81 => Ok(Opcode::LDBI),
0x82 => Ok(Opcode::LDBU),
0x83 => Ok(Opcode::LDBUI),
0x84 => Ok(Opcode::LDW),
0x85 => Ok(Opcode::LDWI),
0x86 => Ok(Opcode::LDWU),
0x87 => Ok(Opcode::LDWUI),
0x88 => Ok(Opcode::LDT),
0x89 => Ok(Opcode::LDTI),
0x8A => Ok(Opcode::LDTU),
0x8B => Ok(Opcode::LDTUI),
0x8C => Ok(Opcode::LDO),
0x8D => Ok(Opcode::LDOI),
0x8E => Ok(Opcode::LDOU),
0x8F => Ok(Opcode::LDOUI),
0x90 => Ok(Opcode::LDSF),
0x91 => Ok(Opcode::LDSFI),
0x92 => Ok(Opcode::LDHT),
0x93 => Ok(Opcode::LDHTI),
0x94 => Ok(Opcode::CSWAP),
0x95 => Ok(Opcode::CSWAPI),
0x96 => Ok(Opcode::LDUNC),
0x97 => Ok(Opcode::LDUNCI),
0x98 => Ok(Opcode::LDVTS),
0x99 => Ok(Opcode::LDVTSI),
0x9A => Ok(Opcode::PRELD),
0x9B => Ok(Opcode::PRELDI),
0x9C => Ok(Opcode::PREGO),
0x9D => Ok(Opcode::PREGOI),
0x9E => Ok(Opcode::GO),
0x9F => Ok(Opcode::GOI),
0xA0 => Ok(Opcode::STB),
0xA1 => Ok(Opcode::STBI),
0xA2 => Ok(Opcode::STBU),
0xA3 => Ok(Opcode::STBUI),
0xA4 => Ok(Opcode::STW),
0xA5 => Ok(Opcode::STWI),
0xA6 => Ok(Opcode::STWU),
0xA7 => Ok(Opcode::STWUI),
0xA8 => Ok(Opcode::STT),
0xA9 => Ok(Opcode::STTI),
0xAA => Ok(Opcode::STTU),
0xAB => Ok(Opcode::STTUI),
0xAC => Ok(Opcode::STO),
0xAD => Ok(Opcode::STOI),
0xAE => Ok(Opcode::STOU),
0xAF => Ok(Opcode::STOUI),
0xB0 => Ok(Opcode::STSF),
0xB1 => Ok(Opcode::STSFI),
0xB2 => Ok(Opcode::STHT),
0xB3 => Ok(Opcode::STHTI),
0xB4 => Ok(Opcode::STCO),
0xB5 => Ok(Opcode::STCOI),
0xB6 => Ok(Opcode::STUNC),
0xB7 => Ok(Opcode::STUNCI),
0xB8 => Ok(Opcode::SYNCD),
0xB9 => Ok(Opcode::SYNCDI),
0xBA => Ok(Opcode::PREST),
0xBB => Ok(Opcode::PRESTI),
0xBC => Ok(Opcode::SYNCID),
0xBD => Ok(Opcode::SYNCIDI),
0xBE => Ok(Opcode::PUSHGO),
0xBF => Ok(Opcode::PUSHGOI),
0xC0 => Ok(Opcode::OR),
0xC1 => Ok(Opcode::ORI),
0xC2 => Ok(Opcode::ORN),
0xC3 => Ok(Opcode::ORNI),
0xC4 => Ok(Opcode::NOR),
0xC5 => Ok(Opcode::NORI),
0xC6 => Ok(Opcode::XOR),
0xC7 => Ok(Opcode::XORI),
0xC8 => Ok(Opcode::AND),
0xC9 => Ok(Opcode::ANDI),
0xCA => Ok(Opcode::ANDN),
0xCB => Ok(Opcode::ANDNI),
0xCC => Ok(Opcode::NAND),
0xCD => Ok(Opcode::NANDI),
0xCE => Ok(Opcode::NXOR),
0xCF => Ok(Opcode::NXORI),
0xD0 => Ok(Opcode::BDIF),
0xD1 => Ok(Opcode::BDIFI),
0xD2 => Ok(Opcode::WDIF),
0xD3 => Ok(Opcode::WDIFI),
0xD4 => Ok(Opcode::TDIF),
0xD5 => Ok(Opcode::TDIFI),
0xD6 => Ok(Opcode::ODIF),
0xD7 => Ok(Opcode::ODIFI),
0xD8 => Ok(Opcode::MUX),
0xD9 => Ok(Opcode::MUXI),
0xDA => Ok(Opcode::SADD),
0xDB => Ok(Opcode::SADDI),
0xDC => Ok(Opcode::MOR),
0xDD => Ok(Opcode::MORI),
0xDE => Ok(Opcode::MXOR),
0xDF => Ok(Opcode::MXORI),
0xE0 => Ok(Opcode::SETH),
0xE1 => Ok(Opcode::SETMH),
0xE2 => Ok(Opcode::SETML),
0xE3 => Ok(Opcode::SETL),
0xE4 => Ok(Opcode::INCH),
0xE5 => Ok(Opcode::INCMH),
0xE6 => Ok(Opcode::INCML),
0xE7 => Ok(Opcode::INCL),
0xE8 => Ok(Opcode::ORH),
0xE9 => Ok(Opcode::ORMH),
0xEA => Ok(Opcode::ORML),
0xEB => Ok(Opcode::ORL),
0xEC => Ok(Opcode::ANDNH),
0xED => Ok(Opcode::ANDNMH),
0xEE => Ok(Opcode::ANDNML),
0xEF => Ok(Opcode::ANDNL),
0xF0 => Ok(Opcode::JMP),
0xF1 => Ok(Opcode::JMPB),
0xF2 => Ok(Opcode::PUSHJ),
0xF3 => Ok(Opcode::PUSHJB),
0xF4 => Ok(Opcode::GETA),
0xF5 => Ok(Opcode::GETAB),
0xF6 => Ok(Opcode::PUT),
0xF7 => Ok(Opcode::PUTI),
0xF8 => Ok(Opcode::POP),
0xF9 => Ok(Opcode::RESUME),
0xFA => Ok(Opcode::SAVE),
0xFB => Ok(Opcode::UNSAVE),
0xFC => Ok(Opcode::SYNC),
0xFD => Ok(Opcode::SWYM),
0xFE => Ok(Opcode::GET),
0xFF => Ok(Opcode::TRIP),
_ => Err(format!("Invalid opcode: {:#04x}", value)),
}
}
}
#[derive(Debug, Clone, Copy)]
enum ZForm {
Reg(u8),
Imm(u8),
}
#[derive(Debug, Clone, Copy)]
struct RelativeField {
backward: bool,
field: u32,
}
#[derive(Clone)]
struct SourceUnit {
filename: String,
preprocessed: String,
original: String,
line_map: Vec<usize>,
}
pub struct MMixAssembler {
sources: Vec<SourceUnit>,
current_filename: String,
current_prefix: String,
pub labels: HashMap<String, u64>,
pub symbols: HashMap<String, SymbolType>, label_origins: HashMap<String, (String, usize)>,
symbol_origins: HashMap<String, (String, usize)>,
pub instructions: Vec<(u64, MMixInstruction)>,
current_addr: u64,
next_greg: u8, pub greg_inits: Vec<(u8, u64)>, current_unit_index: usize,
debug_info: BTreeMap<u64, SourceLoc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLoc {
pub file: String,
pub line: usize,
}
impl MMixAssembler {
fn preprocess_debug(source: &str) -> (String, Vec<usize>) {
let debug_re = Regex::new(r#"(?m)^([^\s]*\s+)?debug\s+"([^"]*)"\s*$"#).unwrap();
let mut result = String::new();
let mut line_map = Vec::new();
let mut stubs = Vec::new();
let mut counter = 0usize;
for (index, line) in source.lines().enumerate() {
let original_line = index + 1;
match debug_re.captures(line) {
Some(caps) => {
counter += 1;
let label = caps.get(1).map(|m| m.as_str().trim()).unwrap_or("");
let stub_label = format!("DbgStr_{counter:04}");
let ret_label = format!("DbgRet_{counter:04}");
Self::emit_debug_call(&mut result, label, &stub_label, &ret_label);
line_map.push(original_line);
line_map.push(original_line);
stubs.push((stub_label, caps[2].to_string(), ret_label));
}
None => {
result.push_str(line);
result.push('\n');
line_map.push(original_line);
}
}
}
if !stubs.is_empty() {
result.push('\n');
result.push_str("; debug subroutines generated by the preprocessor\n");
result.push_str("\tTRAP\t0,Halt,0\n");
for (stub_label, text, ret_label) in &stubs {
Self::emit_debug_stub(&mut result, stub_label, text, ret_label);
}
}
debug!("Preprocessed source:\n{}", result);
(result, line_map)
}
fn emit_debug_call(out: &mut String, label: &str, stub_label: &str, ret_label: &str) {
out.push_str(label);
out.push_str(&format!("\tJMP\t{stub_label}\n"));
out.push_str(&format!("{ret_label}\tSWYM\n"));
}
fn emit_debug_stub(out: &mut String, stub_label: &str, text: &str, ret_label: &str) {
out.push_str(&format!("{stub_label}\tSAVE\t$254,0\n"));
out.push_str(&format!("\tGETA\t$255,{stub_label}Str\n"));
out.push_str("\tTRAP\t0,Fputs,StdOut\n");
out.push_str("\tUNSAVE\t0,$254\n");
out.push_str(&format!("\tJMP\t{ret_label}\n"));
out.push_str(&format!("{stub_label}Str\tBYTE\t\"{text}\",#a,0\n"));
}
fn count_string_bytes(content: &str) -> usize {
Self::decode_byte_string(content).len()
}
fn decode_byte_string(content: &str) -> Vec<u8> {
let mut bytes = Vec::new();
let mut chars = content.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(next_ch) = chars.next() {
let decoded = match next_ch {
'n' => b'\n',
'r' => b'\r',
't' => b'\t',
'0' => 0u8,
'\\' => b'\\',
'\'' => b'\'',
'"' => b'"',
other => other as u8,
};
bytes.push(decoded);
} else {
bytes.push(b'\\');
}
} else {
bytes.push(ch as u8);
}
}
bytes
}
fn seed_predefined(symbols: &mut HashMap<String, SymbolType>, name: &str, ty: SymbolType) {
symbols.insert(name.to_string(), ty);
symbols.insert(format!(":{}", name), ty);
}
pub fn new(source: &str, filename: &str) -> Self {
let mut symbols = HashMap::new();
Self::seed_predefined(
&mut symbols,
"Data_Segment",
SymbolType::Constant(DATA_SEGMENT_START),
);
Self::seed_predefined(
&mut symbols,
"Pool_Segment",
SymbolType::Constant(POOL_SEGMENT_START),
);
Self::seed_predefined(
&mut symbols,
"Stack_Segment",
SymbolType::Constant(STACK_SEGMENT_START),
);
let (stdin_file_no, stdout_file_no, stderr_file_no) = stdio_raw_identifiers();
Self::seed_predefined(&mut symbols, "StdIn", SymbolType::Constant(stdin_file_no));
Self::seed_predefined(&mut symbols, "StdOut", SymbolType::Constant(stdout_file_no));
Self::seed_predefined(&mut symbols, "StdErr", SymbolType::Constant(stderr_file_no));
for (name, code) in [
("Halt", TrapCode::Halt),
("Trip", TrapCode::Trip),
("Fopen", TrapCode::Fopen),
("Fclose", TrapCode::Fclose),
("Fread", TrapCode::Fread),
("Fgets", TrapCode::Fgets),
("Fgetws", TrapCode::Fgetws),
("Fwrite", TrapCode::Fwrite),
("Fputs", TrapCode::Fputs),
("Fputc", TrapCode::Fputc),
("Fputws", TrapCode::Fputws),
("Fseek", TrapCode::Fseek),
("Ftell", TrapCode::Ftell),
("Time", TrapCode::Time),
] {
Self::seed_predefined(&mut symbols, name, SymbolType::Constant(code as u64));
}
for (name, num) in [
("rB", 0u64),
("rD", 1),
("rE", 2),
("rH", 3),
("rJ", 4),
("rM", 5),
("rR", 6),
("rBB", 7),
("rC", 8),
("rN", 9),
("rO", 10),
("rS", 11),
("rI", 12),
("rT", 13),
("rTT", 14),
("rK", 15),
("rQ", 16),
("rU", 17),
("rV", 18),
("rG", 19),
("rL", 20),
("rA", 21),
("rF", 22),
("rP", 23),
("rW", 24),
("rX", 25),
("rY", 26),
("rZ", 27),
("rWW", 28),
("rXX", 29),
("rYY", 30),
("rZZ", 31),
] {
Self::seed_predefined(&mut symbols, name, SymbolType::Constant(num));
}
for (name, mode) in [
("ROUND_CURRENT", 0u64),
("ROUND_OFF", 1),
("ROUND_UP", 2),
("ROUND_DOWN", 3),
("ROUND_NEAR", 4),
] {
Self::seed_predefined(&mut symbols, name, SymbolType::Constant(mode));
}
let (preprocessed_source, line_map) = Self::preprocess_debug(source);
Self {
sources: vec![SourceUnit {
filename: filename.to_string(),
preprocessed: preprocessed_source,
original: source.to_string(),
line_map,
}],
current_filename: filename.to_string(),
current_prefix: String::new(),
labels: HashMap::new(),
symbols,
label_origins: HashMap::new(),
symbol_origins: HashMap::new(),
instructions: Vec::new(),
current_addr: 0,
next_greg: 254, greg_inits: Vec::new(),
current_unit_index: 0,
debug_info: BTreeMap::new(),
}
}
pub fn add_source(&mut self, source: &str, filename: &str) {
let (preprocessed, line_map) = Self::preprocess_debug(source);
self.sources.push(SourceUnit {
filename: filename.to_string(),
preprocessed,
original: source.to_string(),
line_map,
});
}
fn qualify_name(&self, raw: &str) -> String {
if raw.starts_with(':') {
raw.to_string()
} else {
format!("{}{}", self.current_prefix, raw)
}
}
fn define_label(&mut self, raw: &str, addr: u64, line: usize) -> Result<(), String> {
let name = self.qualify_name(raw);
if let Some((prev_file, prev_line)) = self.label_origins.get(&name) {
return Err(format!(
"{}:{}: symbol '{}' redefined (first defined at {}:{})",
self.current_filename, line, name, prev_file, prev_line
));
}
if let Some((prev_file, prev_line)) = self.symbol_origins.get(&name) {
return Err(format!(
"{}:{}: symbol '{}' redefined (first defined at {}:{})",
self.current_filename, line, name, prev_file, prev_line
));
}
self.label_origins
.insert(name.clone(), (self.current_filename.clone(), line));
self.labels.insert(name, addr);
Ok(())
}
fn define_symbol(&mut self, raw: &str, ty: SymbolType, line: usize) -> Result<(), String> {
let name = self.qualify_name(raw);
if let Some((prev_file, prev_line)) = self.symbol_origins.get(&name) {
return Err(format!(
"{}:{}: symbol '{}' redefined (first defined at {}:{})",
self.current_filename, line, name, prev_file, prev_line
));
}
if let Some((prev_file, prev_line)) = self.label_origins.get(&name) {
return Err(format!(
"{}:{}: symbol '{}' redefined (first defined at {}:{})",
self.current_filename, line, name, prev_file, prev_line
));
}
self.symbol_origins
.insert(name.clone(), (self.current_filename.clone(), line));
self.symbols.insert(name, ty);
Ok(())
}
fn format_parse_error(
error: &pest::error::Error<Rule>,
filename: &str,
line_map: &[usize],
) -> String {
use pest::error::LineColLocation;
let (line, col) = match error.line_col {
LineColLocation::Pos((l, c)) => (l, c),
LineColLocation::Span((l, c), _) => (l, c),
};
let line = line_map.get(line - 1).copied().unwrap_or(line);
let expected_msg = match &error.variant {
pest::error::ErrorVariant::ParsingError { positives, .. } => {
if positives.is_empty() {
"valid MMIX instruction or directive".to_string()
} else {
let friendly: Vec<String> = positives
.iter()
.map(|r| match r {
Rule::instruction => "instruction".to_string(),
Rule::directive => "directive".to_string(),
Rule::directive_is => "IS directive (symbol definition)".to_string(),
Rule::directive_loc => "LOC directive".to_string(),
Rule::register => "register (e.g., $0, $1, $255)".to_string(),
Rule::expr_value => "number or expression".to_string(),
Rule::identifier => "label or symbol name".to_string(),
_ => format!("{:?}", r),
})
.collect();
if friendly.len() == 1 {
friendly[0].clone()
} else {
format!("one of: {}", friendly.join(", "))
}
}
}
pest::error::ErrorVariant::CustomError { message } => message.clone(),
};
format!(
"{}:{}:{}: syntax error: expected {}",
filename, line, col, expected_msg
)
}
#[instrument(skip(self))]
pub fn parse(&mut self) -> Result<(), String> {
debug!("Starting MMIXAL parsing (two-pass)");
match self.parse_two_pass() {
Ok(_) => {
debug!(
instruction_count = self.instructions.len(),
label_count = self.labels.len(),
symbol_count = self.symbols.len(),
"Parsing completed successfully"
);
Ok(())
}
Err(e) => Err(e),
}
}
#[instrument(skip(self))]
fn parse_two_pass(&mut self) -> Result<(), String> {
use pest::Parser;
let sources = self.sources.clone();
debug!("Pass 1: Collecting labels and symbols");
self.current_prefix.clear();
for (index, unit) in sources.iter().enumerate() {
self.current_filename = unit.filename.clone();
self.current_unit_index = index;
let pairs = MMixalParser::parse(Rule::program, &unit.preprocessed)
.map_err(|e| Self::format_parse_error(&e, &unit.filename, &unit.line_map))?;
for pair in pairs {
if pair.as_rule() == Rule::program {
for line_pair in pair.into_inner() {
if line_pair.as_rule() == Rule::line {
for stmt_pair in line_pair.into_inner() {
if stmt_pair.as_rule() == Rule::statement {
self.first_pass_statement(stmt_pair)?;
}
}
}
}
}
}
}
debug!(
"Pass 1 complete: {} labels, {} symbols",
self.labels.len(),
self.symbols.len()
);
let saved_addr = self.current_addr;
self.current_addr = 0;
self.current_prefix.clear();
debug!("Pass 2: Generating instructions");
for (index, unit) in sources.iter().enumerate() {
self.current_filename = unit.filename.clone();
self.current_unit_index = index;
let pairs = MMixalParser::parse(Rule::program, &unit.preprocessed)
.map_err(|e| Self::format_parse_error(&e, &unit.filename, &unit.line_map))?;
for pair in pairs {
if pair.as_rule() == Rule::program {
for line_pair in pair.into_inner() {
if line_pair.as_rule() == Rule::line {
for stmt_pair in line_pair.into_inner() {
if stmt_pair.as_rule() == Rule::statement {
self.second_pass_statement(stmt_pair)?;
}
}
}
}
}
}
}
self.current_addr = saved_addr;
Ok(())
}
#[instrument(skip(self, pair), fields(current_addr = format!("0x{:X}", self.current_addr)))]
fn first_pass_statement(&mut self, pair: pest::iterators::Pair<Rule>) -> Result<(), String> {
let mut pending_label: Option<(String, usize)> = None;
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::label_def => {
let (line, _) = inner_pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let ident = inner_pair.into_inner().next().unwrap();
pending_label = Some((ident.as_str().to_string(), line));
}
Rule::instruction => {
self.align_current_addr(Self::INSTRUCTION_ALIGNMENT);
let inst = self.peek_instruction_type(inner_pair)?;
let size = Self::instruction_size(&inst);
if let Some((raw, line)) = pending_label.take() {
self.define_label(&raw, self.current_addr, line)?;
}
self.current_addr += size;
}
Rule::directive => {
let directive_pair = inner_pair.into_inner().next().unwrap();
match directive_pair.as_rule() {
Rule::data_directive => {
let alignment = Self::data_directive_alignment(&directive_pair)?;
self.align_current_addr(alignment);
let size = self.data_directive_size(directive_pair.clone())?;
if let Some((raw, line)) = pending_label.take() {
self.define_label(&raw, self.current_addr, line)?;
}
self.current_addr += size;
}
Rule::loc_directive => {
self.parse_loc_directive(directive_pair)?;
if let Some((raw, line)) = pending_label.take() {
self.define_label(&raw, self.current_addr, line)?;
}
}
Rule::greg_directive => {
let allocated_reg = if self.next_greg == 0 {
return Err(
"Too many GREG directives - ran out of global registers"
.to_string(),
);
} else {
let reg = self.next_greg;
self.next_greg -= 1;
reg
};
if let Some((raw, line)) = pending_label.take() {
self.define_symbol(
&raw,
SymbolType::Register(allocated_reg),
line,
)?;
}
let mut greg_parts = directive_pair.clone().into_inner();
let _directive = greg_parts.next();
let value = self.parse_number(greg_parts.next().unwrap())?;
self.greg_inits.push((allocated_reg, value));
}
Rule::is_directive => {
self.parse_is_directive(directive_pair, true)?;
}
Rule::prefix_directive => {
self.parse_prefix_directive(directive_pair);
}
_ => {}
}
}
_ => {}
}
}
if let Some((raw, line)) = pending_label {
self.define_label(&raw, self.current_addr, line)?;
}
Ok(())
}
#[instrument(skip(self, pair), fields(current_addr = format!("0x{:X}", self.current_addr)))]
fn second_pass_statement(&mut self, pair: pest::iterators::Pair<Rule>) -> Result<(), String> {
let (line, _) = pair.line_col();
let mut label_name: Option<String> = None;
let mut inst: Option<MMixInstruction> = None;
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::label_def => {
let ident = inner_pair.into_inner().next().unwrap();
label_name = Some(ident.as_str().to_string());
}
Rule::instruction => {
self.align_current_addr(Self::INSTRUCTION_ALIGNMENT);
if let Some(raw) = label_name.take() {
let qualified = self.qualify_name(&raw);
self.labels.insert(qualified, self.current_addr);
}
inst = Some(self.parse_instruction(inner_pair)?);
}
Rule::directive => {
let directive_pair = inner_pair.into_inner().next().unwrap();
match directive_pair.as_rule() {
Rule::data_directive => {
let alignment = Self::data_directive_alignment(&directive_pair)?;
self.align_current_addr(alignment);
if let Some(raw) = label_name.take() {
let qualified = self.qualify_name(&raw);
self.labels.insert(qualified, self.current_addr);
}
let instructions = self.parse_data_directive(directive_pair)?;
for instruction in instructions {
let size = Self::instruction_size(&instruction);
self.record_debug_info(self.current_addr, line);
self.instructions.push((self.current_addr, instruction));
self.current_addr += size;
}
}
Rule::loc_directive => {
self.parse_loc_directive(directive_pair)?;
}
Rule::greg_directive => {
if let Some(raw) = label_name.take() {
let qualified = self.qualify_name(&raw);
if !self.symbols.contains_key(&qualified) {
return Err(format!(
"Internal error: GREG label '{}' not found in symbols from first pass",
qualified
));
}
}
}
Rule::is_directive => {
self.parse_is_directive(directive_pair, false)?;
}
Rule::prefix_directive => {
self.parse_prefix_directive(directive_pair);
}
_ => {}
}
}
_ => {}
}
}
if let Some(instruction) = inst {
let size = Self::instruction_size(&instruction);
debug!(inst = ?instruction, addr = format!("0x{:X}", self.current_addr), size, "Added instruction");
self.record_debug_info(self.current_addr, line);
self.instructions.push((self.current_addr, instruction));
self.current_addr += size;
}
if let Some(raw) = label_name {
let qualified = self.qualify_name(&raw);
self.labels.insert(qualified, self.current_addr);
}
Ok(())
}
fn original_line(&self, preprocessed: usize) -> Option<usize> {
self.sources[self.current_unit_index]
.line_map
.get(preprocessed - 1)
.copied()
}
fn record_debug_info(&mut self, addr: u64, line: usize) {
let Some(original_line) = self.original_line(line) else {
return;
};
let file = self.sources[self.current_unit_index].filename.clone();
self.debug_info.insert(
addr,
SourceLoc {
file,
line: original_line,
},
);
}
fn peek_instruction_type(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let inner = pair.into_inner().next().ok_or("Empty instruction")?;
match inner.as_rule() {
Rule::inst_set => Ok(MMixInstruction::SETRR(0, 0)),
Rule::inst_seti => Ok(MMixInstruction::SET(0, 0)), Rule::inst_setl_ri => Ok(MMixInstruction::SETL(0, 0)),
Rule::inst_seth_ri => Ok(MMixInstruction::SETH(0, 0)),
Rule::inst_setmh_ri => Ok(MMixInstruction::SETMH(0, 0)),
Rule::inst_setml_ri => Ok(MMixInstruction::SETML(0, 0)),
Rule::inst_incl_ri => Ok(MMixInstruction::INCL(0, 0)),
Rule::inst_lda_rri => Ok(MMixInstruction::LDA(0, 0, 0)),
Rule::inst_lda_ri => {
let mut parts = inner.clone().into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let _x = ops.next(); let addr_pair = ops.next().unwrap();
match self.parse_number(addr_pair) {
Ok(addr) if addr <= 0xFF => Ok(MMixInstruction::LDA(0, 0, 0)), _ => Ok(MMixInstruction::SET(0, 0)), }
}
Rule::inst_halt => Ok(MMixInstruction::HALT),
_ => Ok(MMixInstruction::ADDU(0, 0, 0)),
}
}
fn align_current_addr(&mut self, alignment: u64) {
self.current_addr = self.current_addr.next_multiple_of(alignment);
}
fn data_directive_alignment(pair: &pest::iterators::Pair<Rule>) -> Result<u64, String> {
let directive = pair
.clone()
.into_inner()
.next()
.ok_or("Empty data directive")?;
match directive.as_rule() {
Rule::directive_byte => Ok(1),
Rule::directive_wyde => Ok(2),
Rule::directive_tetra => Ok(4),
Rule::directive_octa => Ok(8),
_ => Err(format!("Unknown data directive: {:?}", directive.as_rule())),
}
}
fn data_directive_size(&self, pair: pest::iterators::Pair<Rule>) -> Result<u64, String> {
let mut parts = pair.clone().into_inner();
let directive = parts.next().ok_or("Empty data directive")?;
let unit_width = Self::data_directive_unit_width(directive.as_rule())?;
let values = parts.next().ok_or("Missing data values")?;
let mut unit_count = 0u64;
for value in values.into_inner() {
let first = value.into_inner().next().unwrap();
if first.as_rule() == Rule::string_literal {
let text = first.as_str();
let content = &text[1..text.len() - 1]; unit_count += Self::count_string_bytes(content) as u64;
} else {
unit_count += 1;
}
}
let total_size = unit_width * unit_count;
debug!(
"Data directive size: {} units x {} bytes = {} bytes",
unit_count, unit_width, total_size
);
Ok(total_size)
}
fn data_directive_unit_width(directive_kind: Rule) -> Result<u64, String> {
match directive_kind {
Rule::directive_byte => Ok(1),
Rule::directive_wyde => Ok(2),
Rule::directive_tetra => Ok(4),
Rule::directive_octa => Ok(8),
_ => Err(format!("Unknown data directive: {:?}", directive_kind)),
}
}
fn parse_instruction(
&mut self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let inner = pair.into_inner().next().ok_or("Empty instruction")?;
match inner.as_rule() {
Rule::inst_set => self.parse_inst_set(inner),
Rule::inst_seti => self.parse_inst_seti(inner),
Rule::inst_setl_ri => self.parse_inst_setl(inner),
Rule::inst_seth_ri => self.parse_inst_seth(inner),
Rule::inst_setmh_ri => self.parse_inst_setmh(inner),
Rule::inst_setml_ri => self.parse_inst_setml(inner),
Rule::inst_incl_ri => self.parse_inst_incl(inner),
Rule::inst_inch_ri => self.parse_inst_inch(inner),
Rule::inst_incmh_ri => self.parse_inst_incmh(inner),
Rule::inst_incml_ri => self.parse_inst_incml(inner),
Rule::inst_orh_ri => self.parse_inst_orh(inner),
Rule::inst_ormh_ri => self.parse_inst_ormh(inner),
Rule::inst_orml_ri => self.parse_inst_orml(inner),
Rule::inst_orl_ri => self.parse_inst_orl(inner),
Rule::inst_andnh_ri => self.parse_inst_andnh(inner),
Rule::inst_andnmh_ri => self.parse_inst_andnmh(inner),
Rule::inst_andnml_ri => self.parse_inst_andnml(inner),
Rule::inst_andnl_ri => self.parse_inst_andnl(inner),
Rule::inst_load_store_auto => self.parse_inst_load_store_auto(inner),
Rule::inst_load_store_rri => self.parse_inst_load_store_rri(inner),
Rule::inst_lda_auto => self.parse_inst_lda_auto(inner),
Rule::inst_lda_rri => self.parse_inst_lda_rri(inner),
Rule::inst_lda_ri => self.parse_inst_lda_ri(inner),
Rule::inst_arith_auto => self.parse_inst_arith_auto(inner),
Rule::inst_arith_rri => self.parse_inst_arith_rri(inner),
Rule::inst_flot_round => self.parse_inst_flot_round(inner),
Rule::inst_flot_auto => self.parse_inst_flot_auto(inner),
Rule::inst_float_round_rrz => self.parse_inst_float_round_rrz(inner),
Rule::inst_float_round_rr => self.parse_inst_float_round_rr(inner),
Rule::inst_float_round_rri => self.parse_inst_float_round_rri(inner),
Rule::inst_float_rri => self.parse_inst_float_rri(inner),
Rule::inst_float_rrr => self.parse_inst_float_rrr(inner),
Rule::inst_neg_auto => self.parse_inst_neg_auto(inner),
Rule::inst_neg_rri => self.parse_inst_neg_rri(inner),
Rule::inst_bitwise_auto => self.parse_inst_bitwise_auto(inner),
Rule::inst_bitwise_rri => self.parse_inst_bitwise_rri(inner),
Rule::inst_bitfiddle_auto => self.parse_inst_bitfiddle_auto(inner),
Rule::inst_bitfiddle_rri => self.parse_inst_bitfiddle_rri(inner),
Rule::inst_shift_auto => self.parse_inst_shift_auto(inner),
Rule::inst_shift_rri => self.parse_inst_shift_rri(inner),
Rule::inst_conditional_set_auto => self.parse_inst_conditional_set_auto(inner),
Rule::inst_conditional_set_rri => self.parse_inst_conditional_set_rri(inner),
Rule::inst_zero_or_set_auto => self.parse_inst_zero_or_set_auto(inner),
Rule::inst_zero_or_set_rri => self.parse_inst_zero_or_set_rri(inner),
Rule::inst_pbranch => self.parse_inst_pbranch(inner),
Rule::inst_branch => self.parse_inst_branch(inner),
Rule::inst_jmp => self.parse_inst_jmp(inner),
Rule::inst_geta => self.parse_inst_geta(inner),
Rule::inst_getab => self.parse_inst_getab(inner),
Rule::inst_pushj => self.parse_inst_pushj(inner),
Rule::inst_pushjb => self.parse_inst_pushjb(inner),
Rule::inst_go_auto => self.parse_inst_go_auto(inner),
Rule::inst_pushgo_rri => self.parse_inst_pushgo_rri(inner),
Rule::inst_pop => self.parse_inst_pop(inner),
Rule::inst_go_rri => self.parse_inst_go_rri(inner),
Rule::inst_get => self.parse_inst_get(inner),
Rule::inst_put_auto => self.parse_inst_put_auto(inner),
Rule::inst_puti => self.parse_inst_puti(inner),
Rule::inst_save => self.parse_inst_save(inner),
Rule::inst_unsave => self.parse_inst_unsave(inner),
Rule::inst_ldunc_rri => self.parse_inst_ldunc_rri(inner),
Rule::inst_stunc_rri => self.parse_inst_stunc_rri(inner),
Rule::inst_ldht_rri => self.parse_inst_ldht_rri(inner),
Rule::inst_stht_rri => self.parse_inst_stht_rri(inner),
Rule::inst_ldsf_rri => self.parse_inst_ldsf_rri(inner),
Rule::inst_stsf_rri => self.parse_inst_stsf_rri(inner),
Rule::inst_ldvts_rri => self.parse_inst_ldvts_rri(inner),
Rule::inst_cswap_rri => self.parse_inst_cswap_rri(inner),
Rule::inst_stco_auto => self.parse_inst_stco_auto(inner),
Rule::inst_stco_rri => self.parse_inst_stco_rri(inner),
Rule::inst_cache_auto => self.parse_inst_cache_auto(inner),
Rule::inst_preld_rri => self.parse_inst_preld_rri(inner),
Rule::inst_prego_rri => self.parse_inst_prego_rri(inner),
Rule::inst_prest_rri => self.parse_inst_prest_rri(inner),
Rule::inst_syncd_rri => self.parse_inst_syncd_rri(inner),
Rule::inst_syncid_rri => self.parse_inst_syncid_rri(inner),
Rule::inst_resume => self.parse_inst_resume(inner),
Rule::inst_trip => self.parse_inst_trip(inner),
Rule::inst_swym => self.parse_inst_swym(inner),
Rule::inst_sync => self.parse_inst_sync(inner),
Rule::inst_trap => self.parse_inst_trap(inner),
Rule::inst_halt => Ok(MMixInstruction::HALT),
_ => Err(format!(
"Unsupported instruction rule: {:?}",
inner.as_rule()
)),
}
}
fn parse_inst_set(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next(); let operands = parts.next().unwrap(); let mut ops = operands.into_inner();
let dest = self.parse_register(ops.next().unwrap())?;
self.lower_set_source(dest, ops.next().unwrap())
}
fn lower_set_source(
&self,
dest: u8,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let value_pair = if pair.as_rule() == Rule::register {
let inner = pair
.clone()
.into_inner()
.next()
.expect("register rule has at least one child");
if inner.as_rule() == Rule::register_num {
return Ok(MMixInstruction::SETRR(dest, self.parse_register(pair)?));
}
let qualified = self.qualify_name(inner.as_str());
if let Some(SymbolType::Register(r)) = self.symbols.get(&qualified).copied() {
return Ok(MMixInstruction::SETRR(dest, r));
}
inner
} else {
pair
};
let negative = value_pair
.clone()
.into_inner()
.next()
.is_some_and(|inner| inner.as_rule() == Rule::signed_number_literal);
let value = self.parse_number(value_pair)?;
if !negative && value > 0xFFFF {
return Err(format!(
"{}:{}:{}: immediate operand {} out of range 0..65535 for SET; use SETI for a wider constant",
self.current_filename, line, col, value
));
}
Ok(MMixInstruction::SETL(dest, value as u16))
}
fn parse_inst_seti(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next(); let operands = parts.next().unwrap(); let mut ops = operands.into_inner();
let dest_reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())?;
Ok(MMixInstruction::SET(dest_reg, val))
}
fn parse_inst_setl(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::SETL(reg, val))
}
fn parse_inst_seth(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::SETH(reg, val))
}
fn parse_inst_setmh(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::SETMH(reg, val))
}
fn parse_inst_setml(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::SETML(reg, val))
}
fn parse_inst_incl(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::INCL(reg, val))
}
fn parse_inst_inch(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::INCH(reg, val))
}
fn parse_inst_incmh(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::INCMH(reg, val))
}
fn parse_inst_incml(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::INCML(reg, val))
}
fn parse_inst_orh(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ORH(reg, val))
}
fn parse_inst_ormh(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ORMH(reg, val))
}
fn parse_inst_orml(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ORML(reg, val))
}
fn parse_inst_orl(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ORL(reg, val))
}
fn parse_inst_andnh(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ANDNH(reg, val))
}
fn parse_inst_andnmh(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ANDNMH(reg, val))
}
fn parse_inst_andnml(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ANDNML(reg, val))
}
fn parse_inst_andnl(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let reg = self.parse_register(ops.next().unwrap())?;
let val = self.parse_number(ops.next().unwrap())? as u16;
Ok(MMixInstruction::ANDNL(reg, val))
}
fn parse_inst_load_store_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("LDB", ZForm::Reg(z)) => Ok(MMixInstruction::LDB(x, y, z)),
("LDB", ZForm::Imm(z)) => Ok(MMixInstruction::LDBI(x, y, z)),
("LDBU", ZForm::Reg(z)) => Ok(MMixInstruction::LDBU(x, y, z)),
("LDBU", ZForm::Imm(z)) => Ok(MMixInstruction::LDBUI(x, y, z)),
("LDW", ZForm::Reg(z)) => Ok(MMixInstruction::LDW(x, y, z)),
("LDW", ZForm::Imm(z)) => Ok(MMixInstruction::LDWI(x, y, z)),
("LDWU", ZForm::Reg(z)) => Ok(MMixInstruction::LDWU(x, y, z)),
("LDWU", ZForm::Imm(z)) => Ok(MMixInstruction::LDWUI(x, y, z)),
("LDT", ZForm::Reg(z)) => Ok(MMixInstruction::LDT(x, y, z)),
("LDT", ZForm::Imm(z)) => Ok(MMixInstruction::LDTI(x, y, z)),
("LDTU", ZForm::Reg(z)) => Ok(MMixInstruction::LDTU(x, y, z)),
("LDTU", ZForm::Imm(z)) => Ok(MMixInstruction::LDTUI(x, y, z)),
("LDO", ZForm::Reg(z)) => Ok(MMixInstruction::LDO(x, y, z)),
("LDO", ZForm::Imm(z)) => Ok(MMixInstruction::LDOI(x, y, z)),
("LDOU", ZForm::Reg(z)) => Ok(MMixInstruction::LDOU(x, y, z)),
("LDOU", ZForm::Imm(z)) => Ok(MMixInstruction::LDOUI(x, y, z)),
("STB", ZForm::Reg(z)) => Ok(MMixInstruction::STB(x, y, z)),
("STB", ZForm::Imm(z)) => Ok(MMixInstruction::STBI(x, y, z)),
("STBU", ZForm::Reg(z)) => Ok(MMixInstruction::STBU(x, y, z)),
("STBU", ZForm::Imm(z)) => Ok(MMixInstruction::STBUI(x, y, z)),
("STW", ZForm::Reg(z)) => Ok(MMixInstruction::STW(x, y, z)),
("STW", ZForm::Imm(z)) => Ok(MMixInstruction::STWI(x, y, z)),
("STWU", ZForm::Reg(z)) => Ok(MMixInstruction::STWU(x, y, z)),
("STWU", ZForm::Imm(z)) => Ok(MMixInstruction::STWUI(x, y, z)),
("STT", ZForm::Reg(z)) => Ok(MMixInstruction::STT(x, y, z)),
("STT", ZForm::Imm(z)) => Ok(MMixInstruction::STTI(x, y, z)),
("STTU", ZForm::Reg(z)) => Ok(MMixInstruction::STTU(x, y, z)),
("STTU", ZForm::Imm(z)) => Ok(MMixInstruction::STTUI(x, y, z)),
("STO", ZForm::Reg(z)) => Ok(MMixInstruction::STO(x, y, z)),
("STO", ZForm::Imm(z)) => Ok(MMixInstruction::STOI(x, y, z)),
("STOU", ZForm::Reg(z)) => Ok(MMixInstruction::STOU(x, y, z)),
("STOU", ZForm::Imm(z)) => Ok(MMixInstruction::STOUI(x, y, z)),
("LDUNC", ZForm::Reg(z)) => Ok(MMixInstruction::LDUNC(x, y, z)),
("LDUNC", ZForm::Imm(z)) => Ok(MMixInstruction::LDUNCI(x, y, z)),
("STUNC", ZForm::Reg(z)) => Ok(MMixInstruction::STUNC(x, y, z)),
("STUNC", ZForm::Imm(z)) => Ok(MMixInstruction::STUNCI(x, y, z)),
("LDHT", ZForm::Reg(z)) => Ok(MMixInstruction::LDHT(x, y, z)),
("LDHT", ZForm::Imm(z)) => Ok(MMixInstruction::LDHTI(x, y, z)),
("STHT", ZForm::Reg(z)) => Ok(MMixInstruction::STHT(x, y, z)),
("STHT", ZForm::Imm(z)) => Ok(MMixInstruction::STHTI(x, y, z)),
("LDSF", ZForm::Reg(z)) => Ok(MMixInstruction::LDSF(x, y, z)),
("LDSF", ZForm::Imm(z)) => Ok(MMixInstruction::LDSFI(x, y, z)),
("STSF", ZForm::Reg(z)) => Ok(MMixInstruction::STSF(x, y, z)),
("STSF", ZForm::Imm(z)) => Ok(MMixInstruction::STSFI(x, y, z)),
("LDVTS", ZForm::Reg(z)) => Ok(MMixInstruction::LDVTS(x, y, z)),
("LDVTS", ZForm::Imm(z)) => Ok(MMixInstruction::LDVTSI(x, y, z)),
("CSWAP", ZForm::Reg(z)) => Ok(MMixInstruction::CSWAP(x, y, z)),
("CSWAP", ZForm::Imm(z)) => Ok(MMixInstruction::CSWAPI(x, y, z)),
_ => Err(format!("Unknown load/store instruction: {}", mnem)),
}
}
fn parse_inst_load_store_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"LDBI" => Ok(MMixInstruction::LDBI(x, y, z)),
"LDBUI" => Ok(MMixInstruction::LDBUI(x, y, z)),
"LDWI" => Ok(MMixInstruction::LDWI(x, y, z)),
"LDWUI" => Ok(MMixInstruction::LDWUI(x, y, z)),
"LDTI" => Ok(MMixInstruction::LDTI(x, y, z)),
"LDTUI" => Ok(MMixInstruction::LDTUI(x, y, z)),
"LDOI" => Ok(MMixInstruction::LDOI(x, y, z)),
"LDOUI" => Ok(MMixInstruction::LDOUI(x, y, z)),
"STBI" => Ok(MMixInstruction::STBI(x, y, z)),
"STBUI" => Ok(MMixInstruction::STBUI(x, y, z)),
"STWI" => Ok(MMixInstruction::STWI(x, y, z)),
"STWUI" => Ok(MMixInstruction::STWUI(x, y, z)),
"STTI" => Ok(MMixInstruction::STTI(x, y, z)),
"STTUI" => Ok(MMixInstruction::STTUI(x, y, z)),
"STOI" => Ok(MMixInstruction::STOI(x, y, z)),
"STOUI" => Ok(MMixInstruction::STOUI(x, y, z)),
_ => Err(format!("Unknown load/store instruction: {}", mnem.as_str())),
}
}
fn parse_inst_lda_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
match self.lower_z_operand(ops.next().unwrap(), "LDA")? {
ZForm::Reg(z) => Ok(MMixInstruction::LDA(x, y, z)),
ZForm::Imm(z) => Ok(MMixInstruction::LDAI(x, y, z)),
}
}
fn parse_inst_lda_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::LDAI)
}
fn parse_inst_lda_ri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let addr_value = self.parse_number(ops.next().unwrap())?;
match mnem.as_str().to_uppercase().as_str() {
"LDA" => {
if addr_value <= 0xFF {
Ok(MMixInstruction::LDA(x, 0, addr_value as u8))
} else {
debug!("LDA with large address {:#x} converted to SET", addr_value);
Ok(MMixInstruction::SET(x, addr_value))
}
}
"LDAI" => {
if addr_value <= 0xFF {
Ok(MMixInstruction::LDAI(x, 0, addr_value as u8))
} else {
Ok(MMixInstruction::SET(x, addr_value))
}
}
_ => Err(format!("Unknown LDA instruction: {}", mnem.as_str())),
}
}
fn parse_inst_arith_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("ADD", ZForm::Reg(z)) => Ok(MMixInstruction::ADD(x, y, z)),
("ADD", ZForm::Imm(z)) => Ok(MMixInstruction::ADDI(x, y, z)),
("ADDU", ZForm::Reg(z)) => Ok(MMixInstruction::ADDU(x, y, z)),
("ADDU", ZForm::Imm(z)) => Ok(MMixInstruction::ADDUI(x, y, z)),
("2ADDU", ZForm::Reg(z)) => Ok(MMixInstruction::ADDU2(x, y, z)),
("2ADDU", ZForm::Imm(z)) => Ok(MMixInstruction::ADDU2I(x, y, z)),
("4ADDU", ZForm::Reg(z)) => Ok(MMixInstruction::ADDU4(x, y, z)),
("4ADDU", ZForm::Imm(z)) => Ok(MMixInstruction::ADDU4I(x, y, z)),
("8ADDU", ZForm::Reg(z)) => Ok(MMixInstruction::ADDU8(x, y, z)),
("8ADDU", ZForm::Imm(z)) => Ok(MMixInstruction::ADDU8I(x, y, z)),
("16ADDU", ZForm::Reg(z)) => Ok(MMixInstruction::ADDU16(x, y, z)),
("16ADDU", ZForm::Imm(z)) => Ok(MMixInstruction::ADDU16I(x, y, z)),
("SUB", ZForm::Reg(z)) => Ok(MMixInstruction::SUB(x, y, z)),
("SUB", ZForm::Imm(z)) => Ok(MMixInstruction::SUBI(x, y, z)),
("SUBU", ZForm::Reg(z)) => Ok(MMixInstruction::SUBU(x, y, z)),
("SUBU", ZForm::Imm(z)) => Ok(MMixInstruction::SUBUI(x, y, z)),
("MUL", ZForm::Reg(z)) => Ok(MMixInstruction::MUL(x, y, z)),
("MUL", ZForm::Imm(z)) => Ok(MMixInstruction::MULI(x, y, z)),
("MULU", ZForm::Reg(z)) => Ok(MMixInstruction::MULU(x, y, z)),
("MULU", ZForm::Imm(z)) => Ok(MMixInstruction::MULUI(x, y, z)),
("DIV", ZForm::Reg(z)) => Ok(MMixInstruction::DIV(x, y, z)),
("DIV", ZForm::Imm(z)) => Ok(MMixInstruction::DIVI(x, y, z)),
("DIVU", ZForm::Reg(z)) => Ok(MMixInstruction::DIVU(x, y, z)),
("DIVU", ZForm::Imm(z)) => Ok(MMixInstruction::DIVUI(x, y, z)),
("CMP", ZForm::Reg(z)) => Ok(MMixInstruction::CMP(x, y, z)),
("CMP", ZForm::Imm(z)) => Ok(MMixInstruction::CMPI(x, y, z)),
("CMPU", ZForm::Reg(z)) => Ok(MMixInstruction::CMPU(x, y, z)),
("CMPU", ZForm::Imm(z)) => Ok(MMixInstruction::CMPUI(x, y, z)),
_ => Err(format!("Unknown arithmetic instruction: {}", mnem)),
}
}
fn parse_inst_arith_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"ADDI" => Ok(MMixInstruction::ADDI(x, y, z)),
"ADDUI" => Ok(MMixInstruction::ADDUI(x, y, z)),
"2ADDUI" => Ok(MMixInstruction::ADDU2I(x, y, z)),
"4ADDUI" => Ok(MMixInstruction::ADDU4I(x, y, z)),
"8ADDUI" => Ok(MMixInstruction::ADDU8I(x, y, z)),
"16ADDUI" => Ok(MMixInstruction::ADDU16I(x, y, z)),
"SUBI" => Ok(MMixInstruction::SUBI(x, y, z)),
"SUBUI" => Ok(MMixInstruction::SUBUI(x, y, z)),
"MULI" => Ok(MMixInstruction::MULI(x, y, z)),
"MULUI" => Ok(MMixInstruction::MULUI(x, y, z)),
"DIVI" => Ok(MMixInstruction::DIVI(x, y, z)),
"DIVUI" => Ok(MMixInstruction::DIVUI(x, y, z)),
"CMPI" => Ok(MMixInstruction::CMPI(x, y, z)),
"CMPUI" => Ok(MMixInstruction::CMPUI(x, y, z)),
_ => Err(format!("Unknown arithmetic instruction: {}", mnem.as_str())),
}
}
fn parse_inst_neg_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap().as_str().to_uppercase();
let x = self.parse_register(parts.next().unwrap())?;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.lower_z_operand(parts.next().unwrap(), &mnem)?;
match (mnem.as_str(), z) {
("NEG", ZForm::Reg(z)) => Ok(MMixInstruction::NEG(x, y, z)),
("NEG", ZForm::Imm(z)) => Ok(MMixInstruction::NEGI(x, y, z)),
("NEGU", ZForm::Reg(z)) => Ok(MMixInstruction::NEGU(x, y, z)),
("NEGU", ZForm::Imm(z)) => Ok(MMixInstruction::NEGUI(x, y, z)),
_ => Err(format!("Unknown NEG instruction: {}", mnem)),
}
}
fn parse_inst_neg_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let name = mnem.as_str().to_uppercase();
let x = self.parse_register(parts.next().unwrap())?;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z_operand = parts.next().unwrap();
let (line, col) = z_operand.line_col();
let line = self.original_line(line).unwrap_or(line);
let z = self.imm_byte(self.parse_number(z_operand)?, &name, line, col)?;
match name.as_str() {
"NEGI" => Ok(MMixInstruction::NEGI(x, y, z)),
"NEGUI" => Ok(MMixInstruction::NEGUI(x, y, z)),
_ => Err(format!("Unknown NEG instruction: {}", mnem.as_str())),
}
}
fn parse_inst_float_rrr(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_register(ops.next().unwrap())?;
match mnem.as_str().to_uppercase().as_str() {
"FCMP" => Ok(MMixInstruction::FCMP(x, y, z)),
"FUN" => Ok(MMixInstruction::FUN(x, y, z)),
"FEQL" => Ok(MMixInstruction::FEQL(x, y, z)),
"FCMPE" => Ok(MMixInstruction::FCMPE(x, y, z)),
"FUNE" => Ok(MMixInstruction::FUNE(x, y, z)),
"FEQLE" => Ok(MMixInstruction::FEQLE(x, y, z)),
"FADD" => Ok(MMixInstruction::FADD(x, y, z)),
"FSUB" => Ok(MMixInstruction::FSUB(x, y, z)),
"FMUL" => Ok(MMixInstruction::FMUL(x, y, z)),
"FDIV" => Ok(MMixInstruction::FDIV(x, y, z)),
"FREM" => Ok(MMixInstruction::FREM(x, y, z)),
_ => Err(format!(
"Unknown floating point instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_float_round_rrz(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let x = self.parse_register(parts.next().unwrap())?;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.parse_register(parts.next().unwrap())?;
match mnem.as_str().to_uppercase().as_str() {
"FIX" => Ok(MMixInstruction::FIX(x, y, z)),
"FIXU" => Ok(MMixInstruction::FIXU(x, y, z)),
"FSQRT" => Ok(MMixInstruction::FSQRT(x, y, z)),
"FINT" => Ok(MMixInstruction::FINT(x, y, z)),
_ => Err(format!(
"Unknown floating point instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_float_round_rr(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let z = self.parse_register(ops.next().unwrap())?;
match mnem.as_str().to_uppercase().as_str() {
"FIX" => Ok(MMixInstruction::FIX(x, 0, z)),
"FIXU" => Ok(MMixInstruction::FIXU(x, 0, z)),
"FSQRT" => Ok(MMixInstruction::FSQRT(x, 0, z)),
"FINT" => Ok(MMixInstruction::FINT(x, 0, z)),
_ => Err(format!(
"Unknown floating point instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_flot_round(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap().as_str().to_uppercase();
let x = self.parse_register(parts.next().unwrap())?;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.lower_z_operand(parts.next().unwrap(), &mnem)?;
match (mnem.as_str(), z) {
("FLOT", ZForm::Reg(z)) => Ok(MMixInstruction::FLOT(x, y, z)),
("FLOT", ZForm::Imm(z)) => Ok(MMixInstruction::FLOTI(x, y, z)),
("FLOTU", ZForm::Reg(z)) => Ok(MMixInstruction::FLOTU(x, y, z)),
("FLOTU", ZForm::Imm(z)) => Ok(MMixInstruction::FLOTUI(x, y, z)),
("SFLOT", ZForm::Reg(z)) => Ok(MMixInstruction::SFLOT(x, y, z)),
("SFLOT", ZForm::Imm(z)) => Ok(MMixInstruction::SFLOTI(x, y, z)),
("SFLOTU", ZForm::Reg(z)) => Ok(MMixInstruction::SFLOTU(x, y, z)),
("SFLOTU", ZForm::Imm(z)) => Ok(MMixInstruction::SFLOTUI(x, y, z)),
_ => Err(format!("Unknown float conversion instruction: {}", mnem)),
}
}
fn parse_inst_flot_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap().as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let z = self.lower_z_operand(ops.next().unwrap(), &mnem)?;
match (mnem.as_str(), z) {
("FLOT", ZForm::Reg(z)) => Ok(MMixInstruction::FLOT(x, 0, z)),
("FLOT", ZForm::Imm(z)) => Ok(MMixInstruction::FLOTI(x, 0, z)),
("FLOTU", ZForm::Reg(z)) => Ok(MMixInstruction::FLOTU(x, 0, z)),
("FLOTU", ZForm::Imm(z)) => Ok(MMixInstruction::FLOTUI(x, 0, z)),
("SFLOT", ZForm::Reg(z)) => Ok(MMixInstruction::SFLOT(x, 0, z)),
("SFLOT", ZForm::Imm(z)) => Ok(MMixInstruction::SFLOTI(x, 0, z)),
("SFLOTU", ZForm::Reg(z)) => Ok(MMixInstruction::SFLOTU(x, 0, z)),
("SFLOTU", ZForm::Imm(z)) => Ok(MMixInstruction::SFLOTUI(x, 0, z)),
_ => Err(format!("Unknown float conversion instruction: {}", mnem)),
}
}
fn parse_inst_float_round_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let x = self.parse_register(parts.next().unwrap())?;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.parse_number(parts.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"FLOTI" => Ok(MMixInstruction::FLOTI(x, y, z)),
"FLOTUI" => Ok(MMixInstruction::FLOTUI(x, y, z)),
"SFLOTI" => Ok(MMixInstruction::SFLOTI(x, y, z)),
"SFLOTUI" => Ok(MMixInstruction::SFLOTUI(x, y, z)),
_ => Err(format!(
"Unknown floating point immediate instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_float_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"FLOTI" => Ok(MMixInstruction::FLOTI(x, 0, z)),
"FLOTUI" => Ok(MMixInstruction::FLOTUI(x, 0, z)),
"SFLOTI" => Ok(MMixInstruction::SFLOTI(x, 0, z)),
"SFLOTUI" => Ok(MMixInstruction::SFLOTUI(x, 0, z)),
_ => Err(format!(
"Unknown floating point immediate instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_bitwise_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("AND", ZForm::Reg(z)) => Ok(MMixInstruction::AND(x, y, z)),
("AND", ZForm::Imm(z)) => Ok(MMixInstruction::ANDI(x, y, z)),
("OR", ZForm::Reg(z)) => Ok(MMixInstruction::OR(x, y, z)),
("OR", ZForm::Imm(z)) => Ok(MMixInstruction::ORI(x, y, z)),
("XOR", ZForm::Reg(z)) => Ok(MMixInstruction::XOR(x, y, z)),
("XOR", ZForm::Imm(z)) => Ok(MMixInstruction::XORI(x, y, z)),
("ANDN", ZForm::Reg(z)) => Ok(MMixInstruction::ANDN(x, y, z)),
("ANDN", ZForm::Imm(z)) => Ok(MMixInstruction::ANDNI(x, y, z)),
("ORN", ZForm::Reg(z)) => Ok(MMixInstruction::ORN(x, y, z)),
("ORN", ZForm::Imm(z)) => Ok(MMixInstruction::ORNI(x, y, z)),
("NAND", ZForm::Reg(z)) => Ok(MMixInstruction::NAND(x, y, z)),
("NAND", ZForm::Imm(z)) => Ok(MMixInstruction::NANDI(x, y, z)),
("NOR", ZForm::Reg(z)) => Ok(MMixInstruction::NOR(x, y, z)),
("NOR", ZForm::Imm(z)) => Ok(MMixInstruction::NORI(x, y, z)),
("NXOR", ZForm::Reg(z)) => Ok(MMixInstruction::NXOR(x, y, z)),
("NXOR", ZForm::Imm(z)) => Ok(MMixInstruction::NXORI(x, y, z)),
("MUX", ZForm::Reg(z)) => Ok(MMixInstruction::MUX(x, y, z)),
("MUX", ZForm::Imm(z)) => Ok(MMixInstruction::MUXI(x, y, z)),
_ => Err(format!("Unknown bitwise instruction: {}", mnem)),
}
}
fn parse_inst_bitwise_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"ANDI" => Ok(MMixInstruction::ANDI(x, y, z)),
"ORI" => Ok(MMixInstruction::ORI(x, y, z)),
"XORI" => Ok(MMixInstruction::XORI(x, y, z)),
"ANDNI" => Ok(MMixInstruction::ANDNI(x, y, z)),
"ORNI" => Ok(MMixInstruction::ORNI(x, y, z)),
"NANDI" => Ok(MMixInstruction::NANDI(x, y, z)),
"NORI" => Ok(MMixInstruction::NORI(x, y, z)),
"NXORI" => Ok(MMixInstruction::NXORI(x, y, z)),
"MUXI" => Ok(MMixInstruction::MUXI(x, y, z)),
_ => Err(format!("Unknown bitwise instruction: {}", mnem.as_str())),
}
}
fn parse_inst_bitfiddle_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("BDIF", ZForm::Reg(z)) => Ok(MMixInstruction::BDIF(x, y, z)),
("BDIF", ZForm::Imm(z)) => Ok(MMixInstruction::BDIFI(x, y, z)),
("WDIF", ZForm::Reg(z)) => Ok(MMixInstruction::WDIF(x, y, z)),
("WDIF", ZForm::Imm(z)) => Ok(MMixInstruction::WDIFI(x, y, z)),
("TDIF", ZForm::Reg(z)) => Ok(MMixInstruction::TDIF(x, y, z)),
("TDIF", ZForm::Imm(z)) => Ok(MMixInstruction::TDIFI(x, y, z)),
("ODIF", ZForm::Reg(z)) => Ok(MMixInstruction::ODIF(x, y, z)),
("ODIF", ZForm::Imm(z)) => Ok(MMixInstruction::ODIFI(x, y, z)),
("SADD", ZForm::Reg(z)) => Ok(MMixInstruction::SADD(x, y, z)),
("SADD", ZForm::Imm(z)) => Ok(MMixInstruction::SADDI(x, y, z)),
("MOR", ZForm::Reg(z)) => Ok(MMixInstruction::MOR(x, y, z)),
("MOR", ZForm::Imm(z)) => Ok(MMixInstruction::MORI(x, y, z)),
("MXOR", ZForm::Reg(z)) => Ok(MMixInstruction::MXOR(x, y, z)),
("MXOR", ZForm::Imm(z)) => Ok(MMixInstruction::MXORI(x, y, z)),
_ => Err(format!("Unknown bit fiddling instruction: {}", mnem)),
}
}
fn parse_inst_bitfiddle_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"BDIFI" => Ok(MMixInstruction::BDIFI(x, y, z)),
"WDIFI" => Ok(MMixInstruction::WDIFI(x, y, z)),
"TDIFI" => Ok(MMixInstruction::TDIFI(x, y, z)),
"ODIFI" => Ok(MMixInstruction::ODIFI(x, y, z)),
"SADDI" => Ok(MMixInstruction::SADDI(x, y, z)),
"MORI" => Ok(MMixInstruction::MORI(x, y, z)),
"MXORI" => Ok(MMixInstruction::MXORI(x, y, z)),
_ => Err(format!(
"Unknown bit fiddling instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_shift_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("SL", ZForm::Reg(z)) => Ok(MMixInstruction::SL(x, y, z)),
("SL", ZForm::Imm(z)) => Ok(MMixInstruction::SLI(x, y, z)),
("SLU", ZForm::Reg(z)) => Ok(MMixInstruction::SLU(x, y, z)),
("SLU", ZForm::Imm(z)) => Ok(MMixInstruction::SLUI(x, y, z)),
("SR", ZForm::Reg(z)) => Ok(MMixInstruction::SR(x, y, z)),
("SR", ZForm::Imm(z)) => Ok(MMixInstruction::SRI(x, y, z)),
("SRU", ZForm::Reg(z)) => Ok(MMixInstruction::SRU(x, y, z)),
("SRU", ZForm::Imm(z)) => Ok(MMixInstruction::SRUI(x, y, z)),
_ => Err(format!("Unknown shift instruction: {}", mnem)),
}
}
fn parse_inst_shift_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"SLI" => Ok(MMixInstruction::SLI(x, y, z)),
"SLUI" => Ok(MMixInstruction::SLUI(x, y, z)),
"SRI" => Ok(MMixInstruction::SRI(x, y, z)),
"SRUI" => Ok(MMixInstruction::SRUI(x, y, z)),
_ => Err(format!("Unknown shift instruction: {}", mnem.as_str())),
}
}
fn parse_inst_conditional_set_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("CSN", ZForm::Reg(z)) => Ok(MMixInstruction::CSN(x, y, z)),
("CSN", ZForm::Imm(z)) => Ok(MMixInstruction::CSNI(x, y, z)),
("CSZ", ZForm::Reg(z)) => Ok(MMixInstruction::CSZ(x, y, z)),
("CSZ", ZForm::Imm(z)) => Ok(MMixInstruction::CSZI(x, y, z)),
("CSP", ZForm::Reg(z)) => Ok(MMixInstruction::CSP(x, y, z)),
("CSP", ZForm::Imm(z)) => Ok(MMixInstruction::CSPI(x, y, z)),
("CSOD", ZForm::Reg(z)) => Ok(MMixInstruction::CSOD(x, y, z)),
("CSOD", ZForm::Imm(z)) => Ok(MMixInstruction::CSODI(x, y, z)),
("CSNN", ZForm::Reg(z)) => Ok(MMixInstruction::CSNN(x, y, z)),
("CSNN", ZForm::Imm(z)) => Ok(MMixInstruction::CSNNI(x, y, z)),
("CSNZ", ZForm::Reg(z)) => Ok(MMixInstruction::CSNZ(x, y, z)),
("CSNZ", ZForm::Imm(z)) => Ok(MMixInstruction::CSNZI(x, y, z)),
("CSNP", ZForm::Reg(z)) => Ok(MMixInstruction::CSNP(x, y, z)),
("CSNP", ZForm::Imm(z)) => Ok(MMixInstruction::CSNPI(x, y, z)),
("CSEV", ZForm::Reg(z)) => Ok(MMixInstruction::CSEV(x, y, z)),
("CSEV", ZForm::Imm(z)) => Ok(MMixInstruction::CSEVI(x, y, z)),
_ => Err(format!("Unknown conditional set instruction: {}", mnem)),
}
}
fn parse_inst_conditional_set_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"CSNI" => Ok(MMixInstruction::CSNI(x, y, z)),
"CSZI" => Ok(MMixInstruction::CSZI(x, y, z)),
"CSPI" => Ok(MMixInstruction::CSPI(x, y, z)),
"CSODI" => Ok(MMixInstruction::CSODI(x, y, z)),
"CSNNI" => Ok(MMixInstruction::CSNNI(x, y, z)),
"CSNZI" => Ok(MMixInstruction::CSNZI(x, y, z)),
"CSNPI" => Ok(MMixInstruction::CSNPI(x, y, z)),
"CSEVI" => Ok(MMixInstruction::CSEVI(x, y, z)),
_ => Err(format!(
"Unknown conditional set immediate instruction: {}",
mnem.as_str()
)),
}
}
fn parse_inst_zero_or_set_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem_pair = parts.next().unwrap();
let mnem = mnem_pair.as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z_pair = ops.next().unwrap();
let z = self.lower_z_operand(z_pair, &mnem)?;
match (mnem.as_str(), z) {
("ZSN", ZForm::Reg(z)) => Ok(MMixInstruction::ZSN(x, y, z)),
("ZSN", ZForm::Imm(z)) => Ok(MMixInstruction::ZSNI(x, y, z)),
("ZSZ", ZForm::Reg(z)) => Ok(MMixInstruction::ZSZ(x, y, z)),
("ZSZ", ZForm::Imm(z)) => Ok(MMixInstruction::ZSZI(x, y, z)),
("ZSP", ZForm::Reg(z)) => Ok(MMixInstruction::ZSP(x, y, z)),
("ZSP", ZForm::Imm(z)) => Ok(MMixInstruction::ZSPI(x, y, z)),
("ZSOD", ZForm::Reg(z)) => Ok(MMixInstruction::ZSOD(x, y, z)),
("ZSOD", ZForm::Imm(z)) => Ok(MMixInstruction::ZSODI(x, y, z)),
("ZSNN", ZForm::Reg(z)) => Ok(MMixInstruction::ZSNN(x, y, z)),
("ZSNN", ZForm::Imm(z)) => Ok(MMixInstruction::ZSNNI(x, y, z)),
("ZSNZ", ZForm::Reg(z)) => Ok(MMixInstruction::ZSNZ(x, y, z)),
("ZSNZ", ZForm::Imm(z)) => Ok(MMixInstruction::ZSNZI(x, y, z)),
("ZSNP", ZForm::Reg(z)) => Ok(MMixInstruction::ZSNP(x, y, z)),
("ZSNP", ZForm::Imm(z)) => Ok(MMixInstruction::ZSNPI(x, y, z)),
("ZSEV", ZForm::Reg(z)) => Ok(MMixInstruction::ZSEV(x, y, z)),
("ZSEV", ZForm::Imm(z)) => Ok(MMixInstruction::ZSEVI(x, y, z)),
_ => Err(format!("Unknown zero or set instruction: {}", mnem)),
}
}
fn parse_inst_zero_or_set_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
match mnem.as_str().to_uppercase().as_str() {
"ZSNI" => Ok(MMixInstruction::ZSNI(x, y, z)),
"ZSZI" => Ok(MMixInstruction::ZSZI(x, y, z)),
"ZSPI" => Ok(MMixInstruction::ZSPI(x, y, z)),
"ZSODI" => Ok(MMixInstruction::ZSODI(x, y, z)),
"ZSNNI" => Ok(MMixInstruction::ZSNNI(x, y, z)),
"ZSNZI" => Ok(MMixInstruction::ZSNZI(x, y, z)),
"ZSNPI" => Ok(MMixInstruction::ZSNPI(x, y, z)),
"ZSEVI" => Ok(MMixInstruction::ZSEVI(x, y, z)),
_ => Err(format!(
"Unknown zero or set immediate instruction: {}",
mnem.as_str()
)),
}
}
fn relative_field(
&self,
mnemonic: &str,
target: u64,
bits: u32,
(line, col): (usize, usize),
absolute_alternative: &str,
) -> Result<RelativeField, String> {
let delta = target.wrapping_sub(self.current_addr) as i64;
if delta % 4 != 0 {
return Err(format!(
"{}:{}:{}: {} target 0x{:X} is not 4-byte aligned relative to the current instruction (byte delta {})",
self.current_filename, line, col, mnemonic, target, delta
));
}
let tetras = delta / 4;
if let Some(forward_sibling) = mnemonic.strip_suffix('B')
&& tetras >= 0
{
return Err(format!(
"{}:{}:{}: {} target 0x{:X} is not behind the current instruction ({} only encodes backward addresses; use {} instead)",
self.current_filename, line, col, mnemonic, target, mnemonic, forward_sibling
));
}
let span = 1i64 << bits;
let out_of_range = |direction: &str, reach_tetras: i64| {
format!(
"{}:{}:{}: {} target 0x{:X} is out of range: {} byte delta {} exceeds {}'s {}-byte {} reach{}",
self.current_filename,
line,
col,
mnemonic,
target,
direction,
delta.unsigned_abs(),
mnemonic,
reach_tetras * 4,
direction,
absolute_alternative
)
};
if tetras >= 0 {
if tetras >= span {
return Err(out_of_range("forward", span - 1));
}
return Ok(RelativeField {
backward: false,
field: tetras as u32,
});
}
if -tetras > span {
return Err(out_of_range("backward", span));
}
Ok(RelativeField {
backward: true,
field: (span + tetras) as u32,
})
}
fn parse_inst_branch(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let target = self.parse_number(ops.next().unwrap())?;
type Branch = fn(u8, u16) -> MMixInstruction;
let mnem = mnem.as_str().to_uppercase();
let (forward, backward): (Branch, Branch) = match mnem.as_str() {
"BN" | "BNB" => (MMixInstruction::BN, MMixInstruction::BNB),
"BZ" | "BZB" => (MMixInstruction::BZ, MMixInstruction::BZB),
"BP" | "BPB" => (MMixInstruction::BP, MMixInstruction::BPB),
"BOD" | "BODB" => (MMixInstruction::BOD, MMixInstruction::BODB),
"BNN" | "BNNB" => (MMixInstruction::BNN, MMixInstruction::BNNB),
"BNZ" | "BNZB" => (MMixInstruction::BNZ, MMixInstruction::BNZB),
"BNP" | "BNPB" => (MMixInstruction::BNP, MMixInstruction::BNPB),
"BEV" | "BEVB" => (MMixInstruction::BEV, MMixInstruction::BEVB),
_ => return Err(format!("Unknown branch instruction: {mnem}")),
};
let resolved = self.relative_field(&mnem, target, 16, (line, col), "")?;
let field = resolved.field as u16;
Ok(if resolved.backward {
backward(x, field)
} else {
forward(x, field)
})
}
fn parse_inst_jmp(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let target = self.parse_number(ops.next().unwrap())?;
let mnem = mnem.map_or_else(|| "JMP".to_string(), |m| m.as_str().to_uppercase());
let resolved = self.relative_field(&mnem, target, 24, (line, col), " (use GO instead)")?;
Ok(if resolved.backward {
MMixInstruction::JMPB(resolved.field)
} else {
MMixInstruction::JMP(resolved.field)
})
}
fn parse_inst_pbranch(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let target = self.parse_number(ops.next().unwrap())?;
type ProbableBranch = fn(u8, u8, u8) -> MMixInstruction;
let mnem = mnem.as_str().to_uppercase();
let (forward, backward): (ProbableBranch, ProbableBranch) = match mnem.as_str() {
"PBN" | "PBNB" => (MMixInstruction::PBN, MMixInstruction::PBNB),
"PBZ" | "PBZB" => (MMixInstruction::PBZ, MMixInstruction::PBZB),
"PBP" | "PBPB" => (MMixInstruction::PBP, MMixInstruction::PBPB),
"PBOD" | "PBODB" => (MMixInstruction::PBOD, MMixInstruction::PBODB),
"PBNN" | "PBNNB" => (MMixInstruction::PBNN, MMixInstruction::PBNNB),
"PBNZ" | "PBNZB" => (MMixInstruction::PBNZ, MMixInstruction::PBNZB),
"PBNP" | "PBNPB" => (MMixInstruction::PBNP, MMixInstruction::PBNPB),
"PBEV" | "PBEVB" => (MMixInstruction::PBEV, MMixInstruction::PBEVB),
_ => return Err(format!("Unknown probable branch instruction: {mnem}")),
};
let resolved = self.relative_field(&mnem, target, 16, (line, col), "")?;
let y = (resolved.field >> 8) as u8;
let z = (resolved.field & 0xFF) as u8;
Ok(if resolved.backward {
backward(x, y, z)
} else {
forward(x, y, z)
})
}
fn parse_inst_geta(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let _mnem = parts.next(); let operand = parts.next().unwrap();
let mut operand_parts = operand.into_inner();
let reg_pair = operand_parts.next().unwrap();
let addr_pair = operand_parts.next().unwrap();
let x = self.parse_register(reg_pair)?;
let addr = self.parse_number(addr_pair)?;
debug!(
"GETA: current_addr=0x{:X}, target_addr=0x{:X}",
self.current_addr, addr
);
let resolved = self.relative_field(
"GETA",
addr,
16,
(line, col),
" (use LDA for longer-range addresses)",
)?;
let y = (resolved.field >> 8) as u8;
let z = (resolved.field & 0xFF) as u8;
debug!(
"GETA: backward={}, field=0x{:X}, y=0x{:X}, z=0x{:X}",
resolved.backward, resolved.field, y, z
);
Ok(if resolved.backward {
MMixInstruction::GETAB(x, y, z)
} else {
MMixInstruction::GETA(x, y, z)
})
}
fn parse_inst_getab(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let _mnem = parts.next(); let operand = parts.next().unwrap();
let mut operand_parts = operand.into_inner();
let reg_pair = operand_parts.next().unwrap();
let addr_pair = operand_parts.next().unwrap();
let x = self.parse_register(reg_pair)?;
let addr = self.parse_number(addr_pair)?;
let resolved = self.relative_field(
"GETAB",
addr,
16,
(line, col),
" (use LDA for longer-range addresses)",
)?;
let y = (resolved.field >> 8) as u8;
let z = (resolved.field & 0xFF) as u8;
Ok(MMixInstruction::GETAB(x, y, z))
}
fn parse_inst_trap(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::TRAP(x, y, z))
}
fn parse_rri<F>(
&self,
pair: pest::iterators::Pair<Rule>,
f: F,
) -> Result<MMixInstruction, String>
where
F: FnOnce(u8, u8, u8) -> MMixInstruction,
{
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.parse_number(ops.next().unwrap())? as u8;
Ok(f(x, y, z))
}
fn parse_inst_pushj(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operand = parts.next().unwrap();
let mut ops = operand.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let addr = self.parse_number(ops.next().unwrap())?;
let resolved = self.relative_field("PUSHJ", addr, 16, (line, col), "")?;
let y = (resolved.field >> 8) as u8;
let z = (resolved.field & 0xFF) as u8;
Ok(if resolved.backward {
MMixInstruction::PUSHJB(x, y, z)
} else {
MMixInstruction::PUSHJ(x, y, z)
})
}
fn parse_inst_pushjb(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let mut parts = pair.into_inner();
let _mnem = parts.next();
let operand = parts.next().unwrap();
let mut ops = operand.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let addr = self.parse_number(ops.next().unwrap())?;
let resolved = self.relative_field("PUSHJB", addr, 16, (line, col), "")?;
let y = (resolved.field >> 8) as u8;
let z = (resolved.field & 0xFF) as u8;
Ok(MMixInstruction::PUSHJB(x, y, z))
}
fn parse_inst_go_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap().as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.lower_z_operand(ops.next().unwrap(), &mnem)?;
match (mnem.as_str(), z) {
("GO", ZForm::Reg(z)) => Ok(MMixInstruction::GO(x, y, z)),
("GO", ZForm::Imm(z)) => Ok(MMixInstruction::GOI(x, y, z)),
("PUSHGO", ZForm::Reg(z)) => Ok(MMixInstruction::PUSHGO(x, y, z)),
("PUSHGO", ZForm::Imm(z)) => Ok(MMixInstruction::PUSHGOI(x, y, z)),
_ => Err(format!("Unknown GO instruction: {}", mnem)),
}
}
fn parse_inst_pushgo_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::PUSHGOI)
}
fn parse_inst_pop(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let yz = self.parse_number(parts.next().unwrap())? as u16;
let y = ((yz >> 8) & 0xFF) as u8;
let z = (yz & 0xFF) as u8;
Ok(MMixInstruction::POP(x, y, z))
}
fn parse_inst_go_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::GOI)
}
fn parse_inst_get(&self, pair: pest::iterators::Pair<Rule>) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_register(parts.next().unwrap())?;
let z = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::GET(x, z))
}
fn parse_inst_put_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
match self.lower_z_operand(parts.next().unwrap(), "PUT")? {
ZForm::Reg(z) => Ok(MMixInstruction::PUT(x, z)),
ZForm::Imm(z) => Ok(MMixInstruction::PUTI(x, z)),
}
}
fn parse_inst_puti(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let z = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::PUTI(x, z))
}
fn parse_inst_save(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x_pair = parts.next().ok_or("Missing X register in SAVE")?;
let x = self.parse_register(x_pair)?;
let z_pair = parts.next().ok_or("Missing Z value in SAVE")?;
let z = self.parse_number(z_pair)? as u8;
Ok(MMixInstruction::SAVE(x, z))
}
fn parse_inst_unsave(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x_pair = parts.next().ok_or("Missing X value in UNSAVE")?;
let x = self.parse_number(x_pair)? as u8;
let z_pair = parts.next().ok_or("Missing Z register in UNSAVE")?;
let z = self.parse_register(z_pair)?;
Ok(MMixInstruction::UNSAVE(x, z))
}
fn parse_inst_ldunc_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::LDUNCI)
}
fn parse_inst_stunc_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::STUNCI)
}
fn parse_inst_ldht_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::LDHTI)
}
fn parse_inst_stht_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::STHTI)
}
fn parse_inst_ldsf_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::LDSFI)
}
fn parse_inst_stsf_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::STSFI)
}
fn parse_inst_ldvts_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::LDVTSI)
}
fn parse_inst_cswap_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::CSWAPI)
}
fn parse_inst_stco_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let y = self.parse_register(parts.next().unwrap())?;
match self.lower_z_operand(parts.next().unwrap(), "STCO")? {
ZForm::Reg(z) => Ok(MMixInstruction::STCO(x, y, z)),
ZForm::Imm(z) => Ok(MMixInstruction::STCOI(x, y, z)),
}
}
fn parse_inst_stco_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let y = self.parse_register(parts.next().unwrap())?;
let z = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::STCOI(x, y, z))
}
fn parse_inst_cache_auto(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let mnem = parts.next().unwrap().as_str().to_uppercase();
let operands = parts.next().unwrap();
let mut ops = operands.into_inner();
let x = self.parse_register(ops.next().unwrap())?;
let y = self.parse_register(ops.next().unwrap())?;
let z = self.lower_z_operand(ops.next().unwrap(), &mnem)?;
match (mnem.as_str(), z) {
("PRELD", ZForm::Reg(z)) => Ok(MMixInstruction::PRELD(x, y, z)),
("PRELD", ZForm::Imm(z)) => Ok(MMixInstruction::PRELDI(x, y, z)),
("PREGO", ZForm::Reg(z)) => Ok(MMixInstruction::PREGO(x, y, z)),
("PREGO", ZForm::Imm(z)) => Ok(MMixInstruction::PREGOI(x, y, z)),
("PREST", ZForm::Reg(z)) => Ok(MMixInstruction::PREST(x, y, z)),
("PREST", ZForm::Imm(z)) => Ok(MMixInstruction::PRESTI(x, y, z)),
("SYNCD", ZForm::Reg(z)) => Ok(MMixInstruction::SYNCD(x, y, z)),
("SYNCD", ZForm::Imm(z)) => Ok(MMixInstruction::SYNCDI(x, y, z)),
("SYNCID", ZForm::Reg(z)) => Ok(MMixInstruction::SYNCID(x, y, z)),
("SYNCID", ZForm::Imm(z)) => Ok(MMixInstruction::SYNCIDI(x, y, z)),
_ => Err(format!("Unknown cache control instruction: {}", mnem)),
}
}
fn parse_inst_preld_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::PRELDI)
}
fn parse_inst_prego_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::PREGOI)
}
fn parse_inst_prest_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::PRESTI)
}
fn parse_inst_syncd_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::SYNCDI)
}
fn parse_inst_syncid_rri(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
self.parse_rri(pair, MMixInstruction::SYNCIDI)
}
fn parse_inst_resume(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let xyz = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::RESUME(xyz))
}
fn parse_inst_trip(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let x = self.parse_number(parts.next().unwrap())? as u8;
let y = self.parse_number(parts.next().unwrap())? as u8;
let z = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::TRIP(x, y, z))
}
fn parse_inst_swym(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let (x, y, z) = match parts.next() {
Some(first) => (
self.parse_number(first)? as u8,
self.parse_number(parts.next().unwrap())? as u8,
self.parse_number(parts.next().unwrap())? as u8,
),
None => (0, 0, 0),
};
Ok(MMixInstruction::SWYM(x, y, z))
}
fn parse_inst_sync(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<MMixInstruction, String> {
let mut parts = pair.into_inner();
let _mnem = parts.next();
let xyz = self.parse_number(parts.next().unwrap())? as u8;
Ok(MMixInstruction::SYNC(xyz))
}
fn data_directive_unit(directive_kind: Rule, value: u64) -> Result<MMixInstruction, String> {
match directive_kind {
Rule::directive_byte => Ok(MMixInstruction::BYTE(value as u8)),
Rule::directive_wyde => Ok(MMixInstruction::WYDE(value as u16)),
Rule::directive_tetra => Ok(MMixInstruction::TETRA(value as u32)),
Rule::directive_octa => Ok(MMixInstruction::OCTA(value)),
_ => Err(format!("Unknown data directive: {:?}", directive_kind)),
}
}
fn parse_data_directive(
&mut self,
pair: pest::iterators::Pair<Rule>,
) -> Result<Vec<MMixInstruction>, String> {
let mut parts = pair.into_inner();
let directive_kind = parts.next().unwrap().as_rule();
let values_pair = parts.next().unwrap();
let mut result = Vec::new();
for value in values_pair.into_inner() {
let actual_value = value.into_inner().next().unwrap();
if actual_value.as_rule() == Rule::string_literal {
let s = actual_value.as_str();
let s = &s[1..s.len() - 1]; for b in Self::decode_byte_string(s) {
result.push(Self::data_directive_unit(directive_kind, b as u64)?);
}
} else {
let val = self.parse_number(actual_value)?;
result.push(Self::data_directive_unit(directive_kind, val)?);
}
}
Ok(result)
}
fn parse_loc_directive(&mut self, pair: pest::iterators::Pair<Rule>) -> Result<(), String> {
let mut parts = pair.into_inner();
let _directive = parts.next(); let addr = self.parse_number(parts.next().unwrap())?;
self.current_addr = addr;
Ok(())
}
fn parse_is_directive(
&mut self,
pair: pest::iterators::Pair<Rule>,
checking: bool,
) -> Result<(), String> {
let mut parts = pair.into_inner();
let lhs = parts.next().unwrap();
let (line, _) = lhs.line_col();
let line = self.original_line(line).unwrap_or(line);
let raw_name = lhs.as_str().to_string();
let _is_keyword = parts.next(); let value_pair = parts.next().unwrap();
let symbol_type = match value_pair.as_rule() {
Rule::register => {
let reg = self.parse_register(value_pair)?;
SymbolType::Register(reg)
}
Rule::expr_value => {
let value = self.parse_number(value_pair)?;
SymbolType::Constant(value)
}
_ => return Err("IS directive requires register or expr_value".to_string()),
};
if checking {
self.define_symbol(&raw_name, symbol_type, line)?;
} else {
let qualified = self.qualify_name(&raw_name);
self.symbols.insert(qualified, symbol_type);
}
Ok(())
}
fn parse_prefix_directive(&mut self, pair: pest::iterators::Pair<Rule>) {
let mut parts = pair.into_inner();
let _directive = parts.next(); let arg = parts.next().expect("prefix_arg required by grammar");
self.current_prefix = arg.as_str().to_string();
}
fn lower_z_operand(
&self,
pair: pest::iterators::Pair<Rule>,
mnem: &str,
) -> Result<ZForm, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
match pair.as_rule() {
Rule::register => {
let inner = pair
.clone()
.into_inner()
.next()
.expect("register rule has at least one child");
match inner.as_rule() {
Rule::register_num => {
let n = self.parse_register(pair)?;
Ok(ZForm::Reg(n))
}
Rule::symbol => {
let raw = inner.as_str();
let qualified = self.qualify_name(raw);
if let Some(&sym) = self.symbols.get(&qualified) {
match sym {
SymbolType::Register(r) => Ok(ZForm::Reg(r)),
SymbolType::Constant(v) => self.imm_in_range(v, mnem, line, col),
}
} else if let Some(&addr) = self.labels.get(&qualified) {
self.imm_in_range(addr, mnem, line, col)
} else {
Err(format!(
"{}:{}:{}: Undefined symbol '{}' in third operand of {}",
self.current_filename, line, col, qualified, mnem
))
}
}
other => Err(format!(
"{}:{}:{}: Unexpected register inner rule {:?} for {}",
self.current_filename, line, col, other, mnem
)),
}
}
Rule::expr_value => {
let v = self.parse_number(pair)?;
self.imm_in_range(v, mnem, line, col)
}
other => Err(format!(
"{}:{}:{}: Unexpected Z operand rule {:?} for {}",
self.current_filename, line, col, other, mnem
)),
}
}
fn imm_byte(&self, v: u64, mnem: &str, line: usize, col: usize) -> Result<u8, String> {
if v <= 0xFF {
Ok(v as u8)
} else {
Err(format!(
"{}:{}:{}: immediate operand {} out of range 0..255 for {}",
self.current_filename, line, col, v, mnem
))
}
}
fn imm_in_range(&self, v: u64, mnem: &str, line: usize, col: usize) -> Result<ZForm, String> {
self.imm_byte(v, mnem, line, col).map(ZForm::Imm)
}
fn parse_register(&self, pair: pest::iterators::Pair<Rule>) -> Result<u8, String> {
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
let text = if pair.as_rule() == Rule::register {
if let Some(inner) = pair.clone().into_inner().next() {
inner.as_str()
} else {
pair.as_str()
}
} else {
pair.as_str()
};
if !text.starts_with('$') {
let qualified = self.qualify_name(text);
if let Some(&symbol_type) = self.symbols.get(&qualified) {
match symbol_type {
SymbolType::Register(reg) => return Ok(reg),
SymbolType::Constant(value) => {
return Err(format!(
"{}:{}:{}: Symbol '{}' is a numeric constant ({}), cannot use as register",
self.current_filename, line, col, qualified, value
));
}
}
}
return Err(format!(
"{}:{}:{}: Undefined symbol '{}' (expected register like $0 or register alias)",
self.current_filename, line, col, qualified
));
}
text[1..]
.parse::<u8>()
.map_err(|e| format!("Line {}:{}: Invalid register number: {}", line, col, e))
}
fn parse_number(&self, pair: pest::iterators::Pair<Rule>) -> Result<u64, String> {
let rule = pair.as_rule();
let (line, col) = pair.line_col();
let line = self.original_line(line).unwrap_or(line);
if rule == Rule::expr_value || rule == Rule::number_literal || rule == Rule::operand_imm {
let inner = pair.into_inner().next().unwrap();
return self.parse_number(inner);
}
let text = pair.as_str();
match rule {
Rule::at_symbol => {
Ok(self.current_addr)
}
Rule::signed_number_literal => {
let mut inner = pair.into_inner();
let unsigned = self.parse_number(inner.next().unwrap())?;
let signed = -(unsigned as i128);
Ok(signed as u64)
}
Rule::char_literal => {
let inner = &text[1..text.len() - 1];
let ch = if let Some(rest) = inner.strip_prefix('\\') {
match rest {
"n" => '\n',
"r" => '\r',
"t" => '\t',
"0" => '\0',
"\\" => '\\',
"'" => '\'',
_ => {
return Err(format!(
"Line {}:{}: Unsupported escape in char literal: \\{}",
line, col, rest
));
}
}
} else {
if inner.chars().count() != 1 {
return Err(format!(
"Line {}:{}: Invalid char literal (must be one byte): '{}'",
line, col, inner
));
}
inner.chars().next().unwrap()
};
if !ch.is_ascii() {
return Err(format!(
"Line {}:{}: Char literal must be ASCII byte, got {:?}",
line, col, ch
));
}
Ok(ch as u8 as u64)
}
Rule::hex_literal => {
let hex_str = if let Some(stripped) = text.strip_prefix('#') {
stripped
} else if let Some(stripped) =
text.strip_prefix("0x").or_else(|| text.strip_prefix("0X"))
{
stripped
} else {
text
};
u64::from_str_radix(hex_str, 16)
.map_err(|e| format!("Line {}:{}: Invalid hex number: {}", line, col, e))
}
Rule::oct_literal => u64::from_str_radix(&text[1..], 8)
.map_err(|e| format!("Line {}:{}: Invalid octal number: {}", line, col, e)),
Rule::dec_literal => text
.parse::<u64>()
.map_err(|e| format!("Line {}:{}: Invalid decimal number: {}", line, col, e)),
Rule::symbol | Rule::identifier | Rule::global_id => {
let qualified = self.qualify_name(text);
if let Some(&symbol_type) = self.symbols.get(&qualified) {
match symbol_type {
SymbolType::Constant(value) => Ok(value),
SymbolType::Register(reg) => Err(format!(
"{}:{}:{}: Symbol '{}' is a register alias (${}), cannot use as immediate value",
self.current_filename, line, col, qualified, reg
)),
}
} else if let Some(&label_addr) = self.labels.get(&qualified) {
Ok(label_addr)
} else {
Err(format!(
"{}:{}:{}: Undefined symbol: {}",
self.current_filename, line, col, qualified
))
}
}
_ => Err(format!(
"Line {}:{}: Expected number, got: {:?}",
line, col, rule
)),
}
}
const INSTRUCTION_ALIGNMENT: u64 = 4;
fn instruction_size(inst: &MMixInstruction) -> u64 {
match inst {
MMixInstruction::SET(_, _) => 16,
MMixInstruction::SETRR(_, _) => 4, MMixInstruction::BYTE(_) => 1,
MMixInstruction::WYDE(_) => 2,
MMixInstruction::TETRA(_) => 4,
MMixInstruction::OCTA(_) => 8,
_ => 4,
}
}
pub fn encode_instruction_bytes(&self, instruction: &MMixInstruction) -> Vec<u8> {
crate::encode::encode_instruction_bytes(instruction)
}
pub fn generate_object_code(&self) -> Vec<u8> {
crate::mmo::MmoGenerator::new(self.instructions.clone(), self.labels.clone()).generate()
}
pub fn source_loc(&self, addr: u64) -> Option<&SourceLoc> {
let (&start, loc) = self.debug_info.range(..=addr).next_back()?;
(addr - start < self.emitted_size(start)?).then_some(loc)
}
fn emitted_size(&self, addr: u64) -> Option<u64> {
self.instructions
.iter()
.find(|(at, _)| *at == addr)
.map(|(_, inst)| Self::instruction_size(inst))
}
pub fn addr_for_line(&self, file: &str, line: usize) -> Option<u64> {
self.debug_info
.iter()
.find(|(_, loc)| loc.file == file && loc.line == line)
.map(|(&addr, _)| addr)
}
pub fn source_text(&self, file: &str, line: usize) -> Option<&str> {
let unit = self.sources.iter().find(|u| u.filename == file)?;
let index = line.checked_sub(1)?;
unit.original.lines().nth(index)
}
pub fn resolve_includes<R>(
root_source: &str,
root_filename: &str,
base_dir: &Path,
read: &R,
) -> Result<Vec<(String, String)>, String>
where
R: Fn(&Path) -> std::io::Result<String>,
{
let mut chain = Vec::new();
Self::resolve_includes_chain(root_source, root_filename, base_dir, read, &mut chain)
}
fn resolve_includes_chain<R>(
root_source: &str,
root_filename: &str,
base_dir: &Path,
read: &R,
chain: &mut Vec<PathBuf>,
) -> Result<Vec<(String, String)>, String>
where
R: Fn(&Path) -> std::io::Result<String>,
{
let mut units = Vec::new();
let mut segment = String::new();
let mut segment_start_line: usize = 1;
let mut current_line: usize = 0;
for raw_line in root_source.split_inclusive('\n') {
current_line += 1;
let content = raw_line.strip_suffix('\n').unwrap_or(raw_line);
let Some(operand) = Self::parse_include_operand(content) else {
segment.push_str(raw_line);
continue;
};
if !segment.trim().is_empty() {
units.push((
root_filename.to_string(),
Self::pad_source(&segment, segment_start_line),
));
}
segment.clear();
let target = Self::normalize_lexically(&base_dir.join(&operand));
if chain.contains(&target) {
let mut names: Vec<String> =
chain.iter().map(|p| p.display().to_string()).collect();
names.push(target.display().to_string());
return Err(format!("include cycle detected: {}", names.join(" -> ")));
}
let included_source = read(&target).map_err(|err| {
format!("cannot read included file '{}': {}", target.display(), err)
})?;
let included_filename = target.display().to_string();
let included_base_dir = target
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(""));
chain.push(target);
let included_units = Self::resolve_includes_chain(
&included_source,
&included_filename,
&included_base_dir,
read,
chain,
)?;
chain.pop();
units.extend(included_units);
segment_start_line = current_line + 1;
}
if !segment.trim().is_empty() {
units.push((
root_filename.to_string(),
Self::pad_source(&segment, segment_start_line),
));
}
Ok(units)
}
fn parse_include_operand(line: &str) -> Option<String> {
let without_comment = match line.find([';', '%']) {
Some(idx) => &line[..idx],
None => line,
};
let trimmed = without_comment.trim();
let mut parts = trimmed.splitn(2, |c: char| c.is_whitespace());
let keyword = parts.next()?;
if !keyword
.trim_start_matches('.')
.eq_ignore_ascii_case("include")
{
return None;
}
let operand = parts.next().unwrap_or("").trim();
if operand.is_empty() {
return None;
}
let unquoted = if operand.len() >= 2 && operand.starts_with('"') && operand.ends_with('"') {
&operand[1..operand.len() - 1]
} else {
operand
};
Some(unquoted.to_string())
}
fn pad_source(text: &str, start_line: usize) -> String {
let padding = start_line.saturating_sub(1);
let mut padded = String::with_capacity(text.len() + padding);
for _ in 0..padding {
padded.push('\n');
}
padded.push_str(text);
padded
}
fn normalize_lexically(path: &Path) -> PathBuf {
use std::path::Component;
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !result.pop() {
result.push("..");
}
}
other => result.push(other.as_os_str()),
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mnemonic_boundary_guard_rejects_prefix_match() {
use pest::Parser;
assert!(
MMixalParser::parse(Rule::mnemonic_get, "GETA").is_err(),
"mnemonic_get must not match a bare prefix of GETA"
);
assert!(
MMixalParser::parse(Rule::mnemonic_set, "SETL").is_err(),
"mnemonic_set must not match a bare prefix of SETL"
);
assert!(
MMixalParser::parse(Rule::mnemonic_sync, "SYNCD").is_err(),
"mnemonic_sync must not match a bare prefix of SYNCD"
);
}
#[test]
fn test_boundary_guard_rejects_zero_whitespace_before_operand() {
let source = "a IS $1\nb IS $2\nc IS $3\nMain ADDa,b,c";
let mut asm = MMixAssembler::new(source, "<test>");
assert!(
asm.parse().is_err(),
"ADDa,b,c must be rejected now that the mnemonic requires a \
word boundary before its operand"
);
}
#[test]
fn test_boundary_guard_accepts_mnemonic_prefixed_label() {
let source = "HaltLoop ADD $1,$2,$3\n HALT";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {source:?}: {e}"));
assert_eq!(
asm.labels.get("HaltLoop"),
Some(&0),
"HaltLoop must resolve to address 0"
);
}
#[test]
fn test_keyword_boundary_admits_underscore_in_label() {
let source = "Halt_Loop SETL $1,1\n\
Swym_x SETL $2,2\n\
Resume_x SETL $3,3\n\
Loc_Start SETL $4,4\n\
Greg_Base SETL $5,5\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {source:?}: {e}"));
for (name, address) in [
("Halt_Loop", 0u64),
("Swym_x", 4),
("Resume_x", 8),
("Loc_Start", 12),
("Greg_Base", 16),
] {
assert_eq!(
asm.labels.get(name),
Some(&address),
"{name} must be claimed as a label at {address}"
);
}
}
#[test]
fn test_swym_carries_its_operands() {
let source = "SWYM 1,2,3\nSWYM";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {source:?}: {e}"));
assert_eq!(asm.instructions.len(), 2, "both SWYM forms must assemble");
assert_eq!(asm.instructions[0].1, MMixInstruction::SWYM(1, 2, 3));
assert_eq!(asm.instructions[1].1, MMixInstruction::SWYM(0, 0, 0));
}
#[test]
fn test_swym_rejects_a_partial_operand_list() {
let mut asm = MMixAssembler::new("SWYM 1,2", "<test>");
assert!(
asm.parse().is_err(),
"SWYM takes zero or three operands, never two"
);
}
#[test]
fn test_parse_simple_label() {
let mut asm = MMixAssembler::new("LOOP: HALT", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("LOOP"), Some(&0));
assert_eq!(asm.instructions.len(), 1);
}
#[test]
fn test_parse_octa_directive() {
let mut asm = MMixAssembler::new("OCTA #123456789ABCDEF0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions.len(), 1);
assert_eq!(
asm.instructions[0].1,
MMixInstruction::OCTA(0x123456789ABCDEF0)
);
}
#[test]
fn test_parse_node_structure() {
let mut asm = MMixAssembler::new("NODE: OCTA 42\n OCTA 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("NODE"), Some(&0));
assert_eq!(asm.instructions.len(), 2);
}
#[test]
fn test_parse_seti() {
let mut asm = MMixAssembler::new("SETI $2, 10", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SET(2, 10));
}
#[test]
fn test_parse_set_register() {
let mut asm = MMixAssembler::new("SET $1, $7", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SETRR(1, 7));
}
#[test]
fn test_parse_negative_literal_seti() {
let mut asm = MMixAssembler::new("SETI $1, -1", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SET(1, u64::MAX));
}
#[test]
fn test_parse_negative_literal_8bit_wrap() {
let mut asm = MMixAssembler::new("ADDI $1, $2, -1", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADDI(1, 2, 0xFF));
}
#[test]
fn test_decode_byte_string_decodes_escapes() {
assert_eq!(
MMixAssembler::decode_byte_string("a\\nb"),
vec![b'a', b'\n', b'b']
);
}
#[test]
fn test_byte_escape_pass1_pass2_agree() {
let mut asm = MMixAssembler::new("OCTA LABEL\nBYTE \"a\\nb\",0\nLABEL BYTE 7", "<test>");
asm.parse().unwrap();
let bytes: Vec<_> = asm.instructions[1..5]
.iter()
.map(|(addr, inst)| (*addr, inst.clone()))
.collect();
assert_eq!(
bytes,
vec![
(8, MMixInstruction::BYTE(b'a')),
(9, MMixInstruction::BYTE(b'\n')),
(10, MMixInstruction::BYTE(b'b')),
(11, MMixInstruction::BYTE(0)),
]
);
assert_eq!(asm.instructions[0].1, MMixInstruction::OCTA(12));
}
#[test]
fn test_wyde_list_mixes_numbers_and_string_pass1_pass2_agree() {
let mut asm = MMixAssembler::new(
"OCTA Next\nBYTE 1\nList WYDE 10,\"ab\",20\nNext BYTE 99",
"<test>",
);
asm.parse().unwrap();
assert_eq!(asm.labels.get("List"), Some(&10));
let list: Vec<_> = asm.instructions[2..6]
.iter()
.map(|(addr, inst)| (*addr, inst.clone()))
.collect();
assert_eq!(
list,
vec![
(10, MMixInstruction::WYDE(10)),
(12, MMixInstruction::WYDE(b'a' as u16)),
(14, MMixInstruction::WYDE(b'b' as u16)),
(16, MMixInstruction::WYDE(20)),
]
);
assert_eq!(asm.labels.get("Next"), Some(&18));
assert_eq!(asm.instructions[0].1, MMixInstruction::OCTA(18));
}
#[test]
fn test_tetra_list_mixes_numbers_and_string_pass1_pass2_agree() {
let mut asm = MMixAssembler::new(
"OCTA Next\nBYTE 1\nList TETRA 10,\"ab\",20\nNext BYTE 99",
"<test>",
);
asm.parse().unwrap();
assert_eq!(asm.labels.get("List"), Some(&12));
let list: Vec<_> = asm.instructions[2..6]
.iter()
.map(|(addr, inst)| (*addr, inst.clone()))
.collect();
assert_eq!(
list,
vec![
(12, MMixInstruction::TETRA(10)),
(16, MMixInstruction::TETRA(b'a' as u32)),
(20, MMixInstruction::TETRA(b'b' as u32)),
(24, MMixInstruction::TETRA(20)),
]
);
assert_eq!(asm.labels.get("Next"), Some(&28));
assert_eq!(asm.instructions[0].1, MMixInstruction::OCTA(28));
}
#[test]
fn test_octa_list_mixes_numbers_and_string_pass1_pass2_agree() {
let mut asm = MMixAssembler::new(
"OCTA Next\nBYTE 1\nList OCTA 10,\"ab\",20\nNext BYTE 99",
"<test>",
);
asm.parse().unwrap();
assert_eq!(asm.labels.get("List"), Some(&16));
let list: Vec<_> = asm.instructions[2..6]
.iter()
.map(|(addr, inst)| (*addr, inst.clone()))
.collect();
assert_eq!(
list,
vec![
(16, MMixInstruction::OCTA(10)),
(24, MMixInstruction::OCTA(b'a' as u64)),
(32, MMixInstruction::OCTA(b'b' as u64)),
(40, MMixInstruction::OCTA(20)),
]
);
assert_eq!(asm.labels.get("Next"), Some(&48));
assert_eq!(asm.instructions[0].1, MMixInstruction::OCTA(48));
}
#[test]
fn test_byte_string_no_auto_terminator() {
let mut asm = MMixAssembler::new("BYTE \"Hi\"", "<test>");
asm.parse().unwrap();
let bytes: Vec<_> = asm
.instructions
.iter()
.map(|(addr, inst)| (*addr, inst.clone()))
.collect();
assert_eq!(
bytes,
vec![
(0, MMixInstruction::BYTE(b'H')),
(1, MMixInstruction::BYTE(b'i')),
]
);
}
#[test]
fn test_octa_label_rounds_up_after_byte() {
let mut asm = MMixAssembler::new("BYTE 1\nDATA: OCTA 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("DATA"), Some(&8));
}
#[test]
fn test_wyde_and_tetra_labels_round_to_their_widths() {
let mut asm = MMixAssembler::new("BYTE 1\nDATA: WYDE 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("DATA"), Some(&2));
let mut asm = MMixAssembler::new("BYTE 1\nDATA: TETRA 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("DATA"), Some(&4));
}
#[test]
fn test_instruction_label_rounds_up_after_byte() {
let mut asm = MMixAssembler::new("BYTE 1\nCODE: HALT", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("CODE"), Some(&4));
}
#[test]
fn test_forward_reference_resolves_to_aligned_address() {
let mut asm = MMixAssembler::new("JMP DATA\nBYTE 1\nDATA: OCTA 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::JMP(2));
}
#[test]
fn test_two_debug_directives_assemble() {
let source = " LOC #100\nMain debug \"one\"\n debug \"two\"\n TRAP 0,Halt,0\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
}
#[test]
fn test_multibyte_byte_string_is_not_aligned() {
let mut asm = MMixAssembler::new("BYTE 1\nTEXT: BYTE \"abcd\"", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("TEXT"), Some(&1));
}
#[test]
fn test_loc_sets_the_counter_exactly() {
let mut asm = MMixAssembler::new("LOC #101\nHERE: BYTE 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("HERE"), Some(&0x101));
}
#[test]
fn test_standalone_label_line_is_not_rounded() {
let mut asm = MMixAssembler::new("BYTE 1\nDATA\nOCTA #FF", "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get("DATA"), Some(&1));
assert_eq!(
asm.instructions
.last()
.map(|(addr, inst)| (*addr, inst.clone())),
Some((8, MMixInstruction::OCTA(0xFF)))
);
}
#[test]
fn test_lda_rri_pass1_size_matches_pass2() {
let mut asm = MMixAssembler::new("JMP LABEL\nLDA $1,$2,4\nLABEL: HALT", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::JMP(2));
}
#[test]
fn test_branch_offset_beyond_i16_bytes_not_truncated() {
let source = "BZ $0,LABEL\nLOC #10000\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let expected_offset = 0x10000i64 / 4;
assert_eq!(
asm.instructions[0].1,
MMixInstruction::BZ(0, expected_offset as u16)
);
}
#[test]
fn test_jmp_backward_emits_jmpb() {
let source = "LOC #100\nBACK: HALT\nJMP BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::JMPB(0xFFFFFF));
}
#[test]
fn test_geta_offset_beyond_range_errors() {
let source = "GETA $0,LABEL\nLOC #40000\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("GETA"));
assert!(err.contains("out of range"));
}
#[test]
fn test_geta_misaligned_target_errors() {
let source = "GETA $0,LABEL\nBYTE 1\nLABEL\nOCTA 0";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("not 4-byte aligned"), "{err}");
}
#[test]
fn test_geta_offset_within_range_succeeds() {
let source = "GETA $0,LABEL\nLOC #3FFFC\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let MMixInstruction::GETA(_, y, z) = asm.instructions[0].1 else {
panic!("expected GETA instruction");
};
let field = ((y as u16) << 8) | z as u16;
assert_eq!(field, 65535);
}
#[test]
fn test_getab_forward_target_errors() {
let source = "GETAB $0,LABEL\nLOC #100\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
assert!(asm.parse().is_err());
}
#[test]
fn test_getab_backward_target_encodes_knuth_field() {
let source = "LOC #100\nBACK: HALT\nGETAB $0,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::GETAB(0, 0xFF, 0xFF));
}
#[test]
fn test_geta_forward_reach_extends_past_i16() {
let source = "GETA $0,LABEL\nLOC #20004\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::GETA(0, 0x80, 0x01));
}
#[test]
fn test_forward_mnemonic_at_backward_target_emits_backward_sibling() {
let source = "LOC #100\nBACK: HALT\nBNP $1,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::BNPB(1, 0xFFFF));
}
#[test]
fn test_pushj_at_backward_target_emits_pushjb() {
let source = "LOC #100\nBACK: HALT\nPUSHJ $1,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(
asm.instructions[1].1,
MMixInstruction::PUSHJB(1, 0xFF, 0xFF)
);
}
#[test]
fn test_pbranch_at_backward_target_emits_knuth_field() {
let source = "LOC #100\nBACK: HALT\nPBZ $1,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::PBZB(1, 0xFF, 0xFF));
let explicit = "LOC #100\nBACK: HALT\nPBZB $1,BACK";
let mut asm = MMixAssembler::new(explicit, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::PBZB(1, 0xFF, 0xFF));
}
#[test]
fn test_backward_mnemonic_at_forward_target_names_forward_sibling() {
let source = "BZB $1,LABEL\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("BZB"), "{err}");
assert!(err.contains("use BZ instead"), "{err}");
}
#[test]
fn test_zero_displacement_takes_the_forward_opcode() {
let source = "HERE: BZ $1,HERE";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::BZ(1, 0));
}
#[test]
fn test_branch_backward_reaches_65536_tetras() {
let source = "LOC #0\nBACK: HALT\nLOC #40000\nBZ $1,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::BZB(1, 0));
}
#[test]
fn test_branch_beyond_reach_errors() {
let forward = "BZ $1,LABEL\nLOC #40000\nLABEL: HALT";
let mut asm = MMixAssembler::new(forward, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("out of range"), "{err}");
let backward = "LOC #0\nBACK: HALT\nLOC #40004\nBZ $1,BACK";
let mut asm = MMixAssembler::new(backward, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("out of range"), "{err}");
}
#[test]
fn test_branch_misaligned_target_errors() {
let source = "BZ $1,LABEL\nBYTE 1\nLABEL\nOCTA 0";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("not 4-byte aligned"), "{err}");
}
#[test]
fn test_jmp_misaligned_target_errors() {
let source = "JMP LABEL\nBYTE 1\nLABEL\nOCTA 0";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("not 4-byte aligned"), "{err}");
}
#[test]
fn test_jmp_beyond_reach_errors() {
let forward = "JMP LABEL\nLOC #4000000\nLABEL: HALT";
let mut asm = MMixAssembler::new(forward, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("out of range"), "{err}");
let backward = "LOC #0\nBACK: HALT\nLOC #4000004\nJMP BACK";
let mut asm = MMixAssembler::new(backward, "<test>");
let err = asm.parse().unwrap_err();
assert!(err.contains("out of range"), "{err}");
}
#[test]
fn test_incl_takes_a_16_bit_immediate() {
let mut asm = MMixAssembler::new("INCL $1,#203", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::INCL(1, 0x203));
}
#[test]
fn test_parse_char_literal_immediate() {
let mut asm = MMixAssembler::new("ANDI $1, $2, 'A'", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ANDI(1, 2, 65));
}
#[test]
fn test_parse_char_literal_escape_newline() {
let mut asm = MMixAssembler::new("ANDI $1, $2, '\\n'", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ANDI(1, 2, 10));
}
#[test]
fn test_parse_char_literal_non_ascii_error() {
let mut asm = MMixAssembler::new("ANDI $1, $2, 'Ä€'", "<test>");
assert!(asm.parse().is_err());
}
#[test]
fn test_parse_char_literal_multi_char_error() {
let mut asm = MMixAssembler::new("ANDI $1, $2, 'AB'", "<test>");
assert!(asm.parse().is_err());
}
#[test]
fn test_parse_and() {
let mut asm = MMixAssembler::new("AND $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::AND(1, 2, 3));
}
#[test]
fn test_parse_andi() {
let mut asm = MMixAssembler::new("ANDI $1, $2, #FF", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ANDI(1, 2, 0xFF));
}
#[test]
fn test_parse_or() {
let mut asm = MMixAssembler::new("OR $10, $20, $30", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::OR(10, 20, 30));
}
#[test]
fn test_parse_xor() {
let mut asm = MMixAssembler::new("XOR $5, $6, $7", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::XOR(5, 6, 7));
}
#[test]
fn test_parse_andn() {
let mut asm = MMixAssembler::new("ANDN $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ANDN(1, 2, 3));
}
#[test]
fn test_parse_nand() {
let mut asm = MMixAssembler::new("NAND $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::NAND(1, 2, 3));
}
#[test]
fn test_parse_nor() {
let mut asm = MMixAssembler::new("NOR $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::NOR(1, 2, 3));
}
#[test]
fn test_parse_nxor() {
let mut asm = MMixAssembler::new("NXOR $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::NXOR(1, 2, 3));
}
#[test]
fn test_parse_mux() {
let mut asm = MMixAssembler::new("MUX $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::MUX(1, 2, 3));
}
#[test]
fn test_parse_bdif() {
let mut asm = MMixAssembler::new("BDIF $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::BDIF(1, 2, 3));
}
#[test]
fn test_parse_bdifi() {
let mut asm = MMixAssembler::new("BDIFI $1, $2, #10", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::BDIFI(1, 2, 0x10));
}
#[test]
fn test_parse_wdif() {
let mut asm = MMixAssembler::new("WDIF $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::WDIF(1, 2, 3));
}
#[test]
fn test_parse_wdifi() {
let mut asm = MMixAssembler::new("WDIFI $1, $2, 100", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::WDIFI(1, 2, 100));
}
#[test]
fn test_parse_tdif() {
let mut asm = MMixAssembler::new("TDIF $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::TDIF(1, 2, 3));
}
#[test]
fn test_parse_tdifi() {
let mut asm = MMixAssembler::new("TDIFI $1, $2, 50", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::TDIFI(1, 2, 50));
}
#[test]
fn test_parse_odif() {
let mut asm = MMixAssembler::new("ODIF $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ODIF(1, 2, 3));
}
#[test]
fn test_parse_odifi() {
let mut asm = MMixAssembler::new("ODIFI $1, $2, 255", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ODIFI(1, 2, 255));
}
#[test]
fn test_parse_sadd() {
let mut asm = MMixAssembler::new("SADD $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SADD(1, 2, 3));
}
#[test]
fn test_parse_saddi() {
let mut asm = MMixAssembler::new("SADDI $1, $2, 0", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SADDI(1, 2, 0));
}
#[test]
fn test_parse_mor() {
let mut asm = MMixAssembler::new("MOR $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::MOR(1, 2, 3));
}
#[test]
fn test_parse_mori() {
let mut asm = MMixAssembler::new("MORI $1, $2, 128", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::MORI(1, 2, 128));
}
#[test]
fn test_parse_mxor() {
let mut asm = MMixAssembler::new("MXOR $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::MXOR(1, 2, 3));
}
#[test]
fn test_parse_mxori() {
let mut asm = MMixAssembler::new("MXORI $1, $2, 64", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::MXORI(1, 2, 64));
}
#[test]
fn test_parse_sl() {
let mut asm = MMixAssembler::new("SL $3, $1, $2", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SL(3, 1, 2));
}
#[test]
fn test_parse_sli() {
let mut asm = MMixAssembler::new("SLI $3, $1, 8", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SLI(3, 1, 8));
}
#[test]
fn test_parse_slu() {
let mut asm = MMixAssembler::new("SLU $10, $20, $30", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SLU(10, 20, 30));
}
#[test]
fn test_parse_slui() {
let mut asm = MMixAssembler::new("SLUI $1, $2, 16", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SLUI(1, 2, 16));
}
#[test]
fn test_parse_sr() {
let mut asm = MMixAssembler::new("SR $5, $6, $7", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SR(5, 6, 7));
}
#[test]
fn test_parse_sri() {
let mut asm = MMixAssembler::new("SRI $3, $1, 4", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SRI(3, 1, 4));
}
#[test]
fn test_parse_sru() {
let mut asm = MMixAssembler::new("SRU $8, $9, $10", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SRU(8, 9, 10));
}
#[test]
fn test_parse_srui() {
let mut asm = MMixAssembler::new("SRUI $3, $1, 1", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SRUI(3, 1, 1));
}
#[test]
fn test_parse_fcmpe() {
let mut asm = MMixAssembler::new("FCMPE $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FCMPE(1, 2, 3));
}
#[test]
fn test_parse_fune() {
let mut asm = MMixAssembler::new("FUNE $4, $5, $6", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FUNE(4, 5, 6));
}
#[test]
fn test_parse_feqle() {
let mut asm = MMixAssembler::new("FEQLE $7, $8, $9", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FEQLE(7, 8, 9));
}
#[test]
fn test_parse_fcmp_after_fcmpe_added() {
let mut asm = MMixAssembler::new("FCMP $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FCMP(1, 2, 3));
}
#[test]
fn test_parse_feql_after_feqle_added() {
let mut asm = MMixAssembler::new("FEQL $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FEQL(1, 2, 3));
}
#[test]
fn test_parse_fun_after_fune_added() {
let mut asm = MMixAssembler::new("FUN $1, $2, $3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::FUN(1, 2, 3));
}
#[test]
fn test_global_symbol_label_and_operand() {
let src = "\
LOC #100\n\
Main BNZ $1,:Foo\n\
:Foo HALT\n";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(asm.labels.get(":Foo").copied(), Some(0x104));
assert_eq!(asm.labels.get("Main").copied(), Some(0x100));
assert!(!asm.labels.contains_key("Foo"));
}
#[test]
fn test_multi_source_main_calls_lib() {
let main_src = "\
LOC #100\n\
Main PUSHJ $0,:Lib\n\
HALT\n";
let lib_src = "\
LOC #200\n\
:Lib POP 0,0\n";
let mut asm = MMixAssembler::new(main_src, "main.mms");
asm.add_source(lib_src, "lib.mms");
asm.parse().unwrap();
assert_eq!(asm.labels.get("Main").copied(), Some(0x100));
assert_eq!(asm.labels.get(":Lib").copied(), Some(0x200));
let addrs: Vec<u64> = asm.instructions.iter().map(|(a, _)| *a).collect();
assert!(addrs.contains(&0x100));
assert!(addrs.contains(&0x200));
}
#[test]
fn test_multi_source_main_redefined() {
let a = "\
LOC #100\n\
Main HALT\n";
let b = "\
LOC #200\n\
Main HALT\n";
let mut asm = MMixAssembler::new(a, "a.mms");
asm.add_source(b, "b.mms");
let err = asm.parse().expect_err("expected redefinition error");
assert!(err.contains("'Main'"), "err: {}", err);
assert!(err.contains("a.mms"), "err: {}", err);
assert!(err.contains("b.mms"), "err: {}", err);
assert!(err.contains("redefined"), "err: {}", err);
}
#[test]
fn test_multi_source_global_symbol_redefined() {
let a = "\
LOC #100\n\
:Foo HALT\n";
let b = "\
LOC #200\n\
:Foo HALT\n";
let mut asm = MMixAssembler::new(a, "a.mms");
asm.add_source(b, "b.mms");
let err = asm.parse().expect_err("expected redefinition error");
assert!(err.contains("':Foo'"), "err: {}", err);
assert!(err.contains("a.mms"), "err: {}", err);
assert!(err.contains("b.mms"), "err: {}", err);
}
#[test]
fn test_redefinition_across_label_and_is() {
let src = "\
LOC #100\n\
Foo HALT\n\
Foo IS 5\n";
let mut asm = MMixAssembler::new(src, "<test>");
let err = asm.parse().expect_err("expected redefinition error");
assert!(err.contains("'Foo'"), "err: {}", err);
assert!(err.contains("redefined"), "err: {}", err);
}
#[test]
fn test_prefix_qualifies_unqualified_symbol() {
let src = "\
PREFIX Sub_\n\
Bar IS 5\n";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(
asm.symbols.get("Sub_Bar").copied(),
Some(SymbolType::Constant(5))
);
assert!(!asm.symbols.contains_key("Bar"));
}
#[test]
fn test_prefix_colon_opts_out() {
let src = "\
PREFIX Sub_\n\
:Foo IS 9\n";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(
asm.symbols.get(":Foo").copied(),
Some(SymbolType::Constant(9))
);
assert!(!asm.symbols.contains_key("Sub_:Foo"));
}
#[test]
fn test_prefix_persists_across_files() {
let a = "PREFIX P_\n";
let b = "\
LOC #100\n\
Bar HALT\n";
let mut asm = MMixAssembler::new(a, "a.mms");
asm.add_source(b, "b.mms");
asm.parse().unwrap();
assert_eq!(asm.labels.get("P_Bar").copied(), Some(0x100));
assert!(!asm.labels.contains_key("Bar"));
}
#[test]
fn test_prefix_reset_to_global() {
let src = "\
PREFIX P_\n\
Bar IS 1\n\
PREFIX :\n\
Baz IS 2\n";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(
asm.symbols.get("P_Bar").copied(),
Some(SymbolType::Constant(1))
);
assert_eq!(
asm.symbols.get(":Baz").copied(),
Some(SymbolType::Constant(2))
);
}
#[test]
fn test_auto_arith_register_form_unchanged() {
let mut asm = MMixAssembler::new("ADD $1,$2,$3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADD(1, 2, 3));
}
#[test]
fn test_auto_arith_immediate_swap() {
let mut asm = MMixAssembler::new("ADD $1,$2,5", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADDI(1, 2, 5));
}
#[test]
fn test_set_immediate_selects_setl() {
let mut asm = MMixAssembler::new("SET $1,5", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SETL(1, 5));
}
#[test]
fn test_set_accepts_a_wyde_wide_immediate() {
let mut asm = MMixAssembler::new("SET $1,20000", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SETL(1, 20000));
}
#[test]
fn test_set_rejects_an_immediate_above_a_wyde() {
let mut asm = MMixAssembler::new("SET $1,#10000", "<test>");
let err = asm.parse().expect_err("expected out-of-range error");
assert!(
err.contains("SETI"),
"error should name SETI as the wide form, got: {err}"
);
}
#[test]
fn test_set_rejects_a_wide_symbol_operand() {
let mut asm = MMixAssembler::new("C IS #12345\nSET $1,C", "<test>");
let err = asm.parse().expect_err("expected out-of-range error");
assert!(
err.contains("SETI"),
"error should name SETI as the wide form, got: {err}"
);
}
#[test]
fn test_seti_accepts_an_immediate_above_a_wyde() {
let mut asm = MMixAssembler::new("SETI $1,#10000", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SET(1, 0x10000));
}
#[test]
fn test_set_negative_literal_wraps_to_its_wyde() {
let mut asm = MMixAssembler::new("SET $1,-1", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SETL(1, 0xFFFF));
}
#[test]
fn test_set_symbol_register_alias_still_copies() {
let mut asm = MMixAssembler::new("N IS $7\nSET $1,N", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SETRR(1, 7));
}
#[test]
fn test_set_is_one_tetra_and_seti_is_four() {
let mut set = MMixAssembler::new("SET $1,5", "<test>");
set.parse().unwrap();
let mut seti = MMixAssembler::new("SETI $1,5", "<test>");
seti.parse().unwrap();
assert_eq!(
set.encode_instruction_bytes(&set.instructions[0].1).len(),
4
);
assert_eq!(
seti.encode_instruction_bytes(&seti.instructions[0].1).len(),
16
);
}
#[test]
fn test_auto_load_store_immediate_swap() {
let mut base = MMixAssembler::new("LDO $1,$2,0\nSTO $1,$2,0", "<test>");
base.parse().unwrap();
let mut explicit = MMixAssembler::new("LDOI $1,$2,0\nSTOI $1,$2,0", "<test>");
explicit.parse().unwrap();
assert_eq!(base.instructions[0].1, explicit.instructions[0].1);
assert_eq!(base.instructions[1].1, explicit.instructions[1].1);
assert_eq!(base.instructions[0].1, MMixInstruction::LDOI(1, 2, 0));
assert_eq!(base.instructions[1].1, MMixInstruction::STOI(1, 2, 0));
}
#[test]
fn test_auto_load_store_register_form_unchanged() {
let mut asm = MMixAssembler::new("LDO $1,$2,$3\nSTBU $1,$2,$3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::LDO(1, 2, 3));
assert_eq!(asm.instructions[1].1, MMixInstruction::STBU(1, 2, 3));
}
#[test]
fn test_auto_load_store_immediate_out_of_range() {
let mut asm = MMixAssembler::new("LDO $1,$2,300", "<test>");
let err = asm.parse().expect_err("expected out-of-range error");
assert!(
err.contains("out of range 0..255"),
"error should mention range, got: {err}"
);
}
#[test]
fn test_auto_arith_explicit_addi_still_works() {
let mut asm = MMixAssembler::new("ADDI $1,$2,5", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADDI(1, 2, 5));
}
#[test]
fn test_auto_arith_addi_rejects_register_z() {
let mut asm = MMixAssembler::new("ADDI $1,$2,$3", "<test>");
assert!(asm.parse().is_err(), "ADDI with $Z should not parse");
}
#[test]
fn test_auto_arith_immediate_out_of_range() {
let mut asm = MMixAssembler::new("ADD $1,$2,300", "<test>");
let err = asm.parse().expect_err("expected out-of-range error");
assert!(
err.contains("out of range 0..255"),
"error should mention range, got: {err}"
);
}
#[test]
fn test_auto_arith_symbol_resolves_to_immediate() {
let src = "K IS 7\nADD $1,$2,K";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADDI(1, 2, 7));
}
#[test]
fn test_auto_arith_symbol_resolves_to_register_alias() {
let src = "R IS $4\nADD $1,$2,R";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ADD(1, 2, 4));
}
#[test]
fn test_auto_bitwise_and_with_hex_immediate() {
let mut asm = MMixAssembler::new("AND $1,$2,#FF", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ANDI(1, 2, 0xFF));
}
#[test]
fn test_auto_shift_sr_with_decimal_immediate() {
let mut asm = MMixAssembler::new("SR $1,$2,3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::SRI(1, 2, 3));
}
#[test]
fn test_auto_bitfiddle_bdif_register_form() {
let mut asm = MMixAssembler::new("BDIF $1,$2,$3", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::BDIF(1, 2, 3));
}
#[test]
fn test_auto_conditional_set_csz_with_immediate() {
let mut asm = MMixAssembler::new("CSZ $1,$2,7", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::CSZI(1, 2, 7));
}
#[test]
fn test_auto_zero_or_set_zsp_with_immediate() {
let mut asm = MMixAssembler::new("ZSP $1,$2,1", "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[0].1, MMixInstruction::ZSPI(1, 2, 1));
}
#[test]
fn test_auto_immediate_byte_identical_to_explicit_i() {
let auto_src = "\
ADD $1,$2,5
SUBU $3,$4,#10
AND $5,$6,#FF
OR $7,$8,1
SR $1,$2,3
SLU $3,$4,16
BDIF $5,$6,7
SADD $7,$8,255
CSZ $1,$2,42
CSNN $3,$4,1
ZSP $5,$6,8
ZSEV $7,$8,128
";
let explicit_src = "\
ADDI $1,$2,5
SUBUI $3,$4,#10
ANDI $5,$6,#FF
ORI $7,$8,1
SRI $1,$2,3
SLUI $3,$4,16
BDIFI $5,$6,7
SADDI $7,$8,255
CSZI $1,$2,42
CSNNI $3,$4,1
ZSPI $5,$6,8
ZSEVI $7,$8,128
";
let mut auto_asm = MMixAssembler::new(auto_src, "<auto>");
auto_asm.parse().unwrap();
let mut explicit_asm = MMixAssembler::new(explicit_src, "<explicit>");
explicit_asm.parse().unwrap();
assert_eq!(
auto_asm.instructions.len(),
explicit_asm.instructions.len(),
"auto and explicit forms produced different instruction counts"
);
for (i, (auto, explicit)) in auto_asm
.instructions
.iter()
.zip(explicit_asm.instructions.iter())
.enumerate()
{
let auto_bytes = auto_asm.encode_instruction_bytes(&auto.1);
let explicit_bytes = explicit_asm.encode_instruction_bytes(&explicit.1);
assert_eq!(
auto_bytes, explicit_bytes,
"instruction {i}: auto form {:?} encoded to {:?}, explicit form {:?} encoded to {:?}",
auto.1, auto_bytes, explicit.1, explicit_bytes
);
}
}
fn assert_first_instruction(src: &str, expected: MMixInstruction) {
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {src:?}: {e}"));
assert!(
!asm.instructions.is_empty(),
"no instructions produced for {src:?}"
);
assert_eq!(
asm.instructions[0].1, expected,
"wrong instruction for {src:?}"
);
}
fn assert_last_instruction_matches(src: &str, predicate: impl Fn(&MMixInstruction) -> bool) {
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {src:?}: {e}"));
let last = asm
.instructions
.last()
.unwrap_or_else(|| panic!("no instructions produced for {src:?}"));
assert!(
predicate(&last.1),
"wrong instruction variant for {src:?}: got {:?}",
last.1
);
}
fn assert_first_instruction_matches(src: &str, predicate: impl Fn(&MMixInstruction) -> bool) {
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {src:?}: {e}"));
assert!(
!asm.instructions.is_empty(),
"no instructions produced for {src:?}"
);
assert!(
predicate(&asm.instructions[0].1),
"wrong instruction variant for {src:?}: got {:?}",
asm.instructions[0].1
);
}
fn assert_parse_error_contains(src: &str, needle: &str) {
let mut asm = MMixAssembler::new(src, "<test>");
let err = asm
.parse()
.err()
.unwrap_or_else(|| panic!("expected error for {src:?}, but parse succeeded"));
assert!(
err.contains(needle),
"error for {src:?} missing {needle:?}: got {err}"
);
}
#[test]
fn test_auto_arith_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("ADD $1,$2,$3", MMixInstruction::ADD(1, 2, 3)),
("ADD $1,$2,5", MMixInstruction::ADDI(1, 2, 5)),
("ADDU $1,$2,$3", MMixInstruction::ADDU(1, 2, 3)),
("ADDU $1,$2,5", MMixInstruction::ADDUI(1, 2, 5)),
("2ADDU $1,$2,$3", MMixInstruction::ADDU2(1, 2, 3)),
("2ADDU $1,$2,5", MMixInstruction::ADDU2I(1, 2, 5)),
("4ADDU $1,$2,$3", MMixInstruction::ADDU4(1, 2, 3)),
("4ADDU $1,$2,5", MMixInstruction::ADDU4I(1, 2, 5)),
("8ADDU $1,$2,$3", MMixInstruction::ADDU8(1, 2, 3)),
("8ADDU $1,$2,5", MMixInstruction::ADDU8I(1, 2, 5)),
("16ADDU $1,$2,$3", MMixInstruction::ADDU16(1, 2, 3)),
("16ADDU $1,$2,5", MMixInstruction::ADDU16I(1, 2, 5)),
("SUB $1,$2,$3", MMixInstruction::SUB(1, 2, 3)),
("SUB $1,$2,5", MMixInstruction::SUBI(1, 2, 5)),
("SUBU $1,$2,$3", MMixInstruction::SUBU(1, 2, 3)),
("SUBU $1,$2,5", MMixInstruction::SUBUI(1, 2, 5)),
("MUL $1,$2,$3", MMixInstruction::MUL(1, 2, 3)),
("MUL $1,$2,5", MMixInstruction::MULI(1, 2, 5)),
("MULU $1,$2,$3", MMixInstruction::MULU(1, 2, 3)),
("MULU $1,$2,5", MMixInstruction::MULUI(1, 2, 5)),
("DIV $1,$2,$3", MMixInstruction::DIV(1, 2, 3)),
("DIV $1,$2,5", MMixInstruction::DIVI(1, 2, 5)),
("DIVU $1,$2,$3", MMixInstruction::DIVU(1, 2, 3)),
("DIVU $1,$2,5", MMixInstruction::DIVUI(1, 2, 5)),
("CMP $1,$2,$3", MMixInstruction::CMP(1, 2, 3)),
("CMP $1,$2,5", MMixInstruction::CMPI(1, 2, 5)),
("CMPU $1,$2,$3", MMixInstruction::CMPU(1, 2, 3)),
("CMPU $1,$2,5", MMixInstruction::CMPUI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_bitwise_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("AND $1,$2,$3", MMixInstruction::AND(1, 2, 3)),
("AND $1,$2,5", MMixInstruction::ANDI(1, 2, 5)),
("OR $1,$2,$3", MMixInstruction::OR(1, 2, 3)),
("OR $1,$2,5", MMixInstruction::ORI(1, 2, 5)),
("XOR $1,$2,$3", MMixInstruction::XOR(1, 2, 3)),
("XOR $1,$2,5", MMixInstruction::XORI(1, 2, 5)),
("ANDN $1,$2,$3", MMixInstruction::ANDN(1, 2, 3)),
("ANDN $1,$2,5", MMixInstruction::ANDNI(1, 2, 5)),
("ORN $1,$2,$3", MMixInstruction::ORN(1, 2, 3)),
("ORN $1,$2,5", MMixInstruction::ORNI(1, 2, 5)),
("NAND $1,$2,$3", MMixInstruction::NAND(1, 2, 3)),
("NAND $1,$2,5", MMixInstruction::NANDI(1, 2, 5)),
("NOR $1,$2,$3", MMixInstruction::NOR(1, 2, 3)),
("NOR $1,$2,5", MMixInstruction::NORI(1, 2, 5)),
("NXOR $1,$2,$3", MMixInstruction::NXOR(1, 2, 3)),
("NXOR $1,$2,5", MMixInstruction::NXORI(1, 2, 5)),
("MUX $1,$2,$3", MMixInstruction::MUX(1, 2, 3)),
("MUX $1,$2,5", MMixInstruction::MUXI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_bitfiddle_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("BDIF $1,$2,$3", MMixInstruction::BDIF(1, 2, 3)),
("BDIF $1,$2,5", MMixInstruction::BDIFI(1, 2, 5)),
("WDIF $1,$2,$3", MMixInstruction::WDIF(1, 2, 3)),
("WDIF $1,$2,5", MMixInstruction::WDIFI(1, 2, 5)),
("TDIF $1,$2,$3", MMixInstruction::TDIF(1, 2, 3)),
("TDIF $1,$2,5", MMixInstruction::TDIFI(1, 2, 5)),
("ODIF $1,$2,$3", MMixInstruction::ODIF(1, 2, 3)),
("ODIF $1,$2,5", MMixInstruction::ODIFI(1, 2, 5)),
("SADD $1,$2,$3", MMixInstruction::SADD(1, 2, 3)),
("SADD $1,$2,5", MMixInstruction::SADDI(1, 2, 5)),
("MOR $1,$2,$3", MMixInstruction::MOR(1, 2, 3)),
("MOR $1,$2,5", MMixInstruction::MORI(1, 2, 5)),
("MXOR $1,$2,$3", MMixInstruction::MXOR(1, 2, 3)),
("MXOR $1,$2,5", MMixInstruction::MXORI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_shift_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("SL $1,$2,$3", MMixInstruction::SL(1, 2, 3)),
("SL $1,$2,5", MMixInstruction::SLI(1, 2, 5)),
("SLU $1,$2,$3", MMixInstruction::SLU(1, 2, 3)),
("SLU $1,$2,5", MMixInstruction::SLUI(1, 2, 5)),
("SR $1,$2,$3", MMixInstruction::SR(1, 2, 3)),
("SR $1,$2,5", MMixInstruction::SRI(1, 2, 5)),
("SRU $1,$2,$3", MMixInstruction::SRU(1, 2, 3)),
("SRU $1,$2,5", MMixInstruction::SRUI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_conditional_set_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("CSN $1,$2,$3", MMixInstruction::CSN(1, 2, 3)),
("CSN $1,$2,5", MMixInstruction::CSNI(1, 2, 5)),
("CSZ $1,$2,$3", MMixInstruction::CSZ(1, 2, 3)),
("CSZ $1,$2,5", MMixInstruction::CSZI(1, 2, 5)),
("CSP $1,$2,$3", MMixInstruction::CSP(1, 2, 3)),
("CSP $1,$2,5", MMixInstruction::CSPI(1, 2, 5)),
("CSOD $1,$2,$3", MMixInstruction::CSOD(1, 2, 3)),
("CSOD $1,$2,5", MMixInstruction::CSODI(1, 2, 5)),
("CSNN $1,$2,$3", MMixInstruction::CSNN(1, 2, 3)),
("CSNN $1,$2,5", MMixInstruction::CSNNI(1, 2, 5)),
("CSNZ $1,$2,$3", MMixInstruction::CSNZ(1, 2, 3)),
("CSNZ $1,$2,5", MMixInstruction::CSNZI(1, 2, 5)),
("CSNP $1,$2,$3", MMixInstruction::CSNP(1, 2, 3)),
("CSNP $1,$2,5", MMixInstruction::CSNPI(1, 2, 5)),
("CSEV $1,$2,$3", MMixInstruction::CSEV(1, 2, 3)),
("CSEV $1,$2,5", MMixInstruction::CSEVI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_zero_or_set_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("ZSN $1,$2,$3", MMixInstruction::ZSN(1, 2, 3)),
("ZSN $1,$2,5", MMixInstruction::ZSNI(1, 2, 5)),
("ZSZ $1,$2,$3", MMixInstruction::ZSZ(1, 2, 3)),
("ZSZ $1,$2,5", MMixInstruction::ZSZI(1, 2, 5)),
("ZSP $1,$2,$3", MMixInstruction::ZSP(1, 2, 3)),
("ZSP $1,$2,5", MMixInstruction::ZSPI(1, 2, 5)),
("ZSOD $1,$2,$3", MMixInstruction::ZSOD(1, 2, 3)),
("ZSOD $1,$2,5", MMixInstruction::ZSODI(1, 2, 5)),
("ZSNN $1,$2,$3", MMixInstruction::ZSNN(1, 2, 3)),
("ZSNN $1,$2,5", MMixInstruction::ZSNNI(1, 2, 5)),
("ZSNZ $1,$2,$3", MMixInstruction::ZSNZ(1, 2, 3)),
("ZSNZ $1,$2,5", MMixInstruction::ZSNZI(1, 2, 5)),
("ZSNP $1,$2,$3", MMixInstruction::ZSNP(1, 2, 3)),
("ZSNP $1,$2,5", MMixInstruction::ZSNPI(1, 2, 5)),
("ZSEV $1,$2,$3", MMixInstruction::ZSEV(1, 2, 3)),
("ZSEV $1,$2,5", MMixInstruction::ZSEVI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
fn first_instruction(src: &str) -> MMixInstruction {
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {src:?}: {e}"));
asm.instructions
.first()
.unwrap_or_else(|| panic!("no instructions produced for {src:?}"))
.1
.clone()
}
#[test]
fn test_auto_extended_load_store_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("LDUNC $1,$2,$3", MMixInstruction::LDUNC(1, 2, 3)),
("LDUNC $1,$2,5", MMixInstruction::LDUNCI(1, 2, 5)),
("STUNC $1,$2,$3", MMixInstruction::STUNC(1, 2, 3)),
("STUNC $1,$2,5", MMixInstruction::STUNCI(1, 2, 5)),
("LDHT $1,$2,$3", MMixInstruction::LDHT(1, 2, 3)),
("LDHT $1,$2,5", MMixInstruction::LDHTI(1, 2, 5)),
("STHT $1,$2,$3", MMixInstruction::STHT(1, 2, 3)),
("STHT $1,$2,5", MMixInstruction::STHTI(1, 2, 5)),
("LDSF $1,$2,$3", MMixInstruction::LDSF(1, 2, 3)),
("LDSF $1,$2,5", MMixInstruction::LDSFI(1, 2, 5)),
("STSF $1,$2,$3", MMixInstruction::STSF(1, 2, 3)),
("STSF $1,$2,5", MMixInstruction::STSFI(1, 2, 5)),
("LDVTS $1,$2,$3", MMixInstruction::LDVTS(1, 2, 3)),
("LDVTS $1,$2,5", MMixInstruction::LDVTSI(1, 2, 5)),
("CSWAP $1,$2,$3", MMixInstruction::CSWAP(1, 2, 3)),
("CSWAP $1,$2,5", MMixInstruction::CSWAPI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_cache_and_go_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("PRELD $1,$2,$3", MMixInstruction::PRELD(1, 2, 3)),
("PRELD $1,$2,5", MMixInstruction::PRELDI(1, 2, 5)),
("PREGO $1,$2,$3", MMixInstruction::PREGO(1, 2, 3)),
("PREGO $1,$2,5", MMixInstruction::PREGOI(1, 2, 5)),
("PREST $1,$2,$3", MMixInstruction::PREST(1, 2, 3)),
("PREST $1,$2,5", MMixInstruction::PRESTI(1, 2, 5)),
("SYNCD $1,$2,$3", MMixInstruction::SYNCD(1, 2, 3)),
("SYNCD $1,$2,5", MMixInstruction::SYNCDI(1, 2, 5)),
("SYNCID $1,$2,$3", MMixInstruction::SYNCID(1, 2, 3)),
("SYNCID $1,$2,5", MMixInstruction::SYNCIDI(1, 2, 5)),
("GO $1,$2,$3", MMixInstruction::GO(1, 2, 3)),
("GO $1,$2,5", MMixInstruction::GOI(1, 2, 5)),
("PUSHGO $1,$2,$3", MMixInstruction::PUSHGO(1, 2, 3)),
("PUSHGO $1,$2,5", MMixInstruction::PUSHGOI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_float_conversion_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("FLOT $1,0,$3", MMixInstruction::FLOT(1, 0, 3)),
("FLOT $1,0,5", MMixInstruction::FLOTI(1, 0, 5)),
("FLOTU $1,0,$3", MMixInstruction::FLOTU(1, 0, 3)),
("FLOTU $1,0,5", MMixInstruction::FLOTUI(1, 0, 5)),
("SFLOT $1,0,$3", MMixInstruction::SFLOT(1, 0, 3)),
("SFLOT $1,0,5", MMixInstruction::SFLOTI(1, 0, 5)),
("SFLOTU $1,0,$3", MMixInstruction::SFLOTU(1, 0, 3)),
("SFLOTU $1,0,5", MMixInstruction::SFLOTUI(1, 0, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_auto_irregular_operand_shapes_full_coverage() {
let cases: &[(&str, MMixInstruction)] = &[
("STCO 5,$2,$3", MMixInstruction::STCO(5, 2, 3)),
("STCO 5,$2,7", MMixInstruction::STCOI(5, 2, 7)),
("NEG $1,0,$3", MMixInstruction::NEG(1, 0, 3)),
("NEG $1,0,7", MMixInstruction::NEGI(1, 0, 7)),
("NEGU $1,0,$3", MMixInstruction::NEGU(1, 0, 3)),
("NEGU $1,0,7", MMixInstruction::NEGUI(1, 0, 7)),
("PUT rA,$1", MMixInstruction::PUT(21, 1)),
("PUT rA,7", MMixInstruction::PUTI(21, 7)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_base_spelling_agrees_with_legacy_immediate_spelling() {
let pairs: &[(&str, &str)] = &[
("LDHT $1,$2,5", "LDHTI $1,$2,5"),
("LDSF $1,$2,5", "LDSFI $1,$2,5"),
("LDUNC $1,$2,5", "LDUNCI $1,$2,5"),
("LDVTS $1,$2,5", "LDVTSI $1,$2,5"),
("STHT $1,$2,5", "STHTI $1,$2,5"),
("STSF $1,$2,5", "STSFI $1,$2,5"),
("STUNC $1,$2,5", "STUNCI $1,$2,5"),
("CSWAP $1,$2,5", "CSWAPI $1,$2,5"),
("PREGO $1,$2,5", "PREGOI $1,$2,5"),
("PRELD $1,$2,5", "PRELDI $1,$2,5"),
("PREST $1,$2,5", "PRESTI $1,$2,5"),
("SYNCD $1,$2,5", "SYNCDI $1,$2,5"),
("SYNCID $1,$2,5", "SYNCIDI $1,$2,5"),
("GO $1,$2,5", "GOI $1,$2,5"),
("PUSHGO $1,$2,5", "PUSHGOI $1,$2,5"),
("FLOT $1,0,5", "FLOTI $1,0,5"),
("FLOTU $1,0,5", "FLOTUI $1,0,5"),
("SFLOT $1,0,5", "SFLOTI $1,0,5"),
("SFLOTU $1,0,5", "SFLOTUI $1,0,5"),
("STCO 5,$2,7", "STCOI 5,$2,7"),
("NEG $1,0,7", "NEGI $1,0,7"),
("NEGU $1,0,7", "NEGUI $1,0,7"),
("PUT rA,7", "PUTI rA,7"),
];
for (base, legacy) in pairs {
assert_eq!(
first_instruction(base),
first_instruction(legacy),
"{base:?} must emit what {legacy:?} emits"
);
}
}
#[test]
fn test_no_previously_accepted_operand_form_narrowed() {
let forms: &[&str] = &[
"LDHT $1,$2,$3",
"LDHTI $1,$2,5",
"LDSF $1,$2,$3",
"LDSFI $1,$2,5",
"LDUNC $1,$2,$3",
"LDUNCI $1,$2,5",
"LDVTS $1,$2,$3",
"LDVTSI $1,$2,5",
"STHT $1,$2,$3",
"STHTI $1,$2,5",
"STSF $1,$2,$3",
"STSFI $1,$2,5",
"STUNC $1,$2,$3",
"STUNCI $1,$2,5",
"CSWAP $1,$2,$3",
"CSWAPI $1,$2,5",
"PREGO $1,$2,$3",
"PREGOI $1,$2,5",
"PRELD $1,$2,$3",
"PRELDI $1,$2,5",
"PREST $1,$2,$3",
"PRESTI $1,$2,5",
"SYNCD $1,$2,$3",
"SYNCDI $1,$2,5",
"SYNCID $1,$2,$3",
"SYNCIDI $1,$2,5",
"GO $1,$2,$3",
"GOI $1,$2,5",
"PUSHGO $1,$2,$3",
"PUSHGOI $1,$2,5",
"FLOT $1,0,$3",
"FLOTI $1,0,5",
"FLOTU $1,0,$3",
"FLOTUI $1,0,5",
"SFLOT $1,0,$3",
"SFLOTI $1,0,5",
"SFLOTU $1,0,$3",
"SFLOTUI $1,0,5",
"STCO 5,$2,$3",
"STCOI 5,$2,7",
"NEG $1,0,$3",
"NEGI $1,0,7",
"NEGU $1,0,$3",
"NEGUI $1,0,7",
"PUT rA,$1",
"PUTI rA,7",
"LDA $1,$2,3",
"LDAI $1,$2,3",
];
for src in forms {
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("{src:?} no longer assembles: {e}"));
}
}
#[test]
fn test_flot_rejects_register_in_y_slot() {
let mut asm = MMixAssembler::new("FLOT $1,$2,$3", "<test>");
let err = asm
.parse()
.expect_err("a register in FLOT's Y slot must no longer parse");
assert_eq!(err, "<test>:1:11: syntax error: expected EOI");
}
#[test]
fn test_round_mode_symbols_resolve_to_documented_values() {
let asm = MMixAssembler::new("", "<test>");
for (name, value) in [
("ROUND_CURRENT", 0u64),
("ROUND_OFF", 1),
("ROUND_UP", 2),
("ROUND_DOWN", 3),
] {
assert_eq!(
asm.symbols.get(name).copied(),
Some(SymbolType::Constant(value)),
"{name} must resolve to {value}"
);
}
}
#[test]
fn test_lda_selects_addus_two_opcodes() {
assert_first_instruction("LDA $1,$2,$3", MMixInstruction::LDA(1, 2, 3));
assert_first_instruction("LDA $1,$2,3", MMixInstruction::LDAI(1, 2, 3));
assert_first_instruction("LDAI $1,$2,3", MMixInstruction::LDAI(1, 2, 3));
}
#[test]
fn test_lda_emits_the_same_bytes_as_addu() {
let asm = MMixAssembler::new("", "<test>");
for (lda, addu) in [
("LDA $1,$2,$3", "ADDU $1,$2,$3"),
("LDA $1,$2,3", "ADDU $1,$2,3"),
] {
assert_eq!(
asm.encode_instruction_bytes(&first_instruction(lda)),
asm.encode_instruction_bytes(&first_instruction(addu)),
"{lda:?} must emit the same bytes as {addu:?}"
);
}
}
#[test]
fn test_stco_rejects_a_register_x_operand() {
let mut asm = MMixAssembler::new("STCO $1,$2,$3", "<test>");
assert!(asm.parse().is_err(), "STCO's X operand is not a register");
}
#[test]
fn test_jmpb_backward_target_encodes_knuth_field() {
let source = "LOC #100\nBACK: HALT\nJMPB BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
assert_eq!(asm.instructions[1].1, MMixInstruction::JMPB(0xFFFFFF));
}
#[test]
fn test_jmpb_forward_target_errors() {
let source = "JMPB LABEL\nLOC #100\nLABEL: HALT";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm.parse().expect_err("JMPB encodes only backward targets");
assert!(
err.contains("use JMP instead"),
"error should name JMP as the forward form, got: {err}"
);
}
#[test]
fn test_prefix_robust_arith_signed_vs_unsigned() {
assert_first_instruction("ADD $1,$2,$3", MMixInstruction::ADD(1, 2, 3));
assert_first_instruction("ADDU $1,$2,$3", MMixInstruction::ADDU(1, 2, 3));
assert_first_instruction("ADDI $1,$2,7", MMixInstruction::ADDI(1, 2, 7));
assert_first_instruction("ADDUI $1,$2,7", MMixInstruction::ADDUI(1, 2, 7));
assert_first_instruction("ADD $1,$2,7", MMixInstruction::ADDI(1, 2, 7));
assert_first_instruction("ADDU $1,$2,7", MMixInstruction::ADDUI(1, 2, 7));
}
#[test]
fn test_prefix_robust_numeric_arith() {
assert_first_instruction("2ADDU $1,$2,$3", MMixInstruction::ADDU2(1, 2, 3));
assert_first_instruction("4ADDU $1,$2,$3", MMixInstruction::ADDU4(1, 2, 3));
assert_first_instruction("8ADDU $1,$2,$3", MMixInstruction::ADDU8(1, 2, 3));
assert_first_instruction("16ADDU $1,$2,$3", MMixInstruction::ADDU16(1, 2, 3));
assert_first_instruction("2ADDU $1,$2,5", MMixInstruction::ADDU2I(1, 2, 5));
assert_first_instruction("4ADDU $1,$2,5", MMixInstruction::ADDU4I(1, 2, 5));
assert_first_instruction("8ADDU $1,$2,5", MMixInstruction::ADDU8I(1, 2, 5));
assert_first_instruction("16ADDU $1,$2,5", MMixInstruction::ADDU16I(1, 2, 5));
assert_first_instruction("2ADDUI $1,$2,5", MMixInstruction::ADDU2I(1, 2, 5));
assert_first_instruction("4ADDUI $1,$2,5", MMixInstruction::ADDU4I(1, 2, 5));
assert_first_instruction("8ADDUI $1,$2,5", MMixInstruction::ADDU8I(1, 2, 5));
assert_first_instruction("16ADDUI $1,$2,5", MMixInstruction::ADDU16I(1, 2, 5));
}
#[test]
fn test_prefix_robust_bitwise_and_andn() {
assert_first_instruction("AND $1,$2,$3", MMixInstruction::AND(1, 2, 3));
assert_first_instruction("ANDN $1,$2,$3", MMixInstruction::ANDN(1, 2, 3));
assert_first_instruction("AND $1,$2,7", MMixInstruction::ANDI(1, 2, 7));
assert_first_instruction("ANDN $1,$2,7", MMixInstruction::ANDNI(1, 2, 7));
assert_first_instruction("ANDI $1,$2,7", MMixInstruction::ANDI(1, 2, 7));
assert_first_instruction("ANDNI $1,$2,7", MMixInstruction::ANDNI(1, 2, 7));
assert_first_instruction("ANDNH $1,5", MMixInstruction::ANDNH(1, 5));
assert_first_instruction("ANDNMH $1,5", MMixInstruction::ANDNMH(1, 5));
assert_first_instruction("ANDNML $1,5", MMixInstruction::ANDNML(1, 5));
assert_first_instruction("ANDNL $1,5", MMixInstruction::ANDNL(1, 5));
}
#[test]
fn test_prefix_robust_bitwise_or_orn() {
assert_first_instruction("OR $1,$2,$3", MMixInstruction::OR(1, 2, 3));
assert_first_instruction("ORN $1,$2,$3", MMixInstruction::ORN(1, 2, 3));
assert_first_instruction("OR $1,$2,7", MMixInstruction::ORI(1, 2, 7));
assert_first_instruction("ORN $1,$2,7", MMixInstruction::ORNI(1, 2, 7));
assert_first_instruction("ORI $1,$2,7", MMixInstruction::ORI(1, 2, 7));
assert_first_instruction("ORNI $1,$2,7", MMixInstruction::ORNI(1, 2, 7));
assert_first_instruction("ORH $1,5", MMixInstruction::ORH(1, 5));
assert_first_instruction("ORMH $1,5", MMixInstruction::ORMH(1, 5));
assert_first_instruction("ORML $1,5", MMixInstruction::ORML(1, 5));
assert_first_instruction("ORL $1,5", MMixInstruction::ORL(1, 5));
}
#[test]
fn test_prefix_robust_set_family() {
assert_first_instruction("SET $1,$2", MMixInstruction::SETRR(1, 2));
assert_first_instruction("SETI $1,5", MMixInstruction::SET(1, 5));
assert_first_instruction("SETL $1,5", MMixInstruction::SETL(1, 5));
assert_first_instruction("SETH $1,5", MMixInstruction::SETH(1, 5));
assert_first_instruction("SETMH $1,5", MMixInstruction::SETMH(1, 5));
assert_first_instruction("SETML $1,5", MMixInstruction::SETML(1, 5));
}
#[test]
fn test_prefix_robust_neg() {
assert_first_instruction("NEG $1,5,$3", MMixInstruction::NEG(1, 5, 3));
assert_first_instruction("NEGU $1,5,$3", MMixInstruction::NEGU(1, 5, 3));
assert_first_instruction("NEGI $1,5,7", MMixInstruction::NEGI(1, 5, 7));
assert_first_instruction("NEGUI $1,5,7", MMixInstruction::NEGUI(1, 5, 7));
}
#[test]
fn test_prefix_robust_float_fix_flot() {
assert_first_instruction("FIX $1,0,$3", MMixInstruction::FIX(1, 0, 3));
assert_first_instruction("FIXU $1,0,$3", MMixInstruction::FIXU(1, 0, 3));
assert_first_instruction("FLOT $1,0,$3", MMixInstruction::FLOT(1, 0, 3));
assert_first_instruction("FLOTU $1,0,$3", MMixInstruction::FLOTU(1, 0, 3));
assert_first_instruction("FLOTI $1,0,5", MMixInstruction::FLOTI(1, 0, 5));
assert_first_instruction("FLOTUI $1,0,5", MMixInstruction::FLOTUI(1, 0, 5));
assert_first_instruction("SFLOT $1,0,$3", MMixInstruction::SFLOT(1, 0, 3));
assert_first_instruction("SFLOTU $1,0,$3", MMixInstruction::SFLOTU(1, 0, 3));
assert_first_instruction("SFLOTI $1,0,5", MMixInstruction::SFLOTI(1, 0, 5));
assert_first_instruction("SFLOTUI $1,0,5", MMixInstruction::SFLOTUI(1, 0, 5));
}
#[test]
fn test_prefix_robust_load_store_families() {
let cases: &[(&str, MMixInstruction)] = &[
("LDB $1,$2,$3", MMixInstruction::LDB(1, 2, 3)),
("LDBU $1,$2,$3", MMixInstruction::LDBU(1, 2, 3)),
("LDBI $1,$2,5", MMixInstruction::LDBI(1, 2, 5)),
("LDBUI $1,$2,5", MMixInstruction::LDBUI(1, 2, 5)),
("LDW $1,$2,$3", MMixInstruction::LDW(1, 2, 3)),
("LDWU $1,$2,$3", MMixInstruction::LDWU(1, 2, 3)),
("LDWI $1,$2,5", MMixInstruction::LDWI(1, 2, 5)),
("LDWUI $1,$2,5", MMixInstruction::LDWUI(1, 2, 5)),
("LDT $1,$2,$3", MMixInstruction::LDT(1, 2, 3)),
("LDTU $1,$2,$3", MMixInstruction::LDTU(1, 2, 3)),
("LDTI $1,$2,5", MMixInstruction::LDTI(1, 2, 5)),
("LDTUI $1,$2,5", MMixInstruction::LDTUI(1, 2, 5)),
("LDO $1,$2,$3", MMixInstruction::LDO(1, 2, 3)),
("LDOU $1,$2,$3", MMixInstruction::LDOU(1, 2, 3)),
("LDOI $1,$2,5", MMixInstruction::LDOI(1, 2, 5)),
("LDOUI $1,$2,5", MMixInstruction::LDOUI(1, 2, 5)),
("STB $1,$2,$3", MMixInstruction::STB(1, 2, 3)),
("STBU $1,$2,$3", MMixInstruction::STBU(1, 2, 3)),
("STBI $1,$2,5", MMixInstruction::STBI(1, 2, 5)),
("STBUI $1,$2,5", MMixInstruction::STBUI(1, 2, 5)),
("STW $1,$2,$3", MMixInstruction::STW(1, 2, 3)),
("STWU $1,$2,$3", MMixInstruction::STWU(1, 2, 3)),
("STWI $1,$2,5", MMixInstruction::STWI(1, 2, 5)),
("STWUI $1,$2,5", MMixInstruction::STWUI(1, 2, 5)),
("STT $1,$2,$3", MMixInstruction::STT(1, 2, 3)),
("STTU $1,$2,$3", MMixInstruction::STTU(1, 2, 3)),
("STTI $1,$2,5", MMixInstruction::STTI(1, 2, 5)),
("STTUI $1,$2,5", MMixInstruction::STTUI(1, 2, 5)),
("STO $1,$2,$3", MMixInstruction::STO(1, 2, 3)),
("STOU $1,$2,$3", MMixInstruction::STOU(1, 2, 3)),
("STOI $1,$2,5", MMixInstruction::STOI(1, 2, 5)),
("STOUI $1,$2,5", MMixInstruction::STOUI(1, 2, 5)),
];
for (src, expected) in cases {
assert_first_instruction(src, expected.clone());
}
}
#[test]
fn test_prefix_robust_lda() {
assert_first_instruction("LDA $1,$2,$3", MMixInstruction::LDA(1, 2, 3));
assert_first_instruction("LDA $1,$2,5", MMixInstruction::LDAI(1, 2, 5));
assert_first_instruction("LDAI $1,$2,5", MMixInstruction::LDAI(1, 2, 5));
}
#[test]
fn test_prefix_robust_get_geta_getab() {
assert_first_instruction("GET $1,0", MMixInstruction::GET(1, 0));
assert_first_instruction_matches("GETA $0,4", |i| {
matches!(i, MMixInstruction::GETA(0, _, _))
});
let source = "LOC #100\nBACK: HALT\nGETAB $0,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {source:?}: {e}"));
assert!(matches!(
asm.instructions[1].1,
MMixInstruction::GETAB(0, _, _)
));
}
#[test]
fn test_prefix_robust_go_pushgo() {
assert_first_instruction("GO $1,$2,$3", MMixInstruction::GO(1, 2, 3));
assert_first_instruction("GOI $1,$2,5", MMixInstruction::GOI(1, 2, 5));
assert_first_instruction("PUSHGO $1,$2,$3", MMixInstruction::PUSHGO(1, 2, 3));
assert_first_instruction("PUSHGOI $1,$2,5", MMixInstruction::PUSHGOI(1, 2, 5));
}
#[test]
fn test_prefix_robust_pushj() {
assert_first_instruction_matches("PUSHJ $1,4", |i| {
matches!(i, MMixInstruction::PUSHJ(1, _, _))
});
let source = "LOC #100\nBACK: HALT\nPUSHJB $1,BACK";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse()
.unwrap_or_else(|e| panic!("failed to parse {source:?}: {e}"));
assert!(matches!(
asm.instructions[1].1,
MMixInstruction::PUSHJB(1, _, _)
));
}
#[test]
fn test_prefix_robust_put() {
assert_first_instruction("PUT 5,$1", MMixInstruction::PUT(5, 1));
assert_first_instruction("PUTI 5,7", MMixInstruction::PUTI(5, 7));
}
#[test]
fn test_prefix_robust_prefetch_and_sync() {
assert_first_instruction("PREGO $1,$2,$3", MMixInstruction::PREGO(1, 2, 3));
assert_first_instruction("PREGOI $1,$2,5", MMixInstruction::PREGOI(1, 2, 5));
assert_first_instruction("PRELD $1,$2,$3", MMixInstruction::PRELD(1, 2, 3));
assert_first_instruction("PRELDI $1,$2,5", MMixInstruction::PRELDI(1, 2, 5));
assert_first_instruction("PREST $1,$2,$3", MMixInstruction::PREST(1, 2, 3));
assert_first_instruction("PRESTI $1,$2,5", MMixInstruction::PRESTI(1, 2, 5));
assert_first_instruction("SYNCD $1,$2,$3", MMixInstruction::SYNCD(1, 2, 3));
assert_first_instruction("SYNCDI $1,$2,5", MMixInstruction::SYNCDI(1, 2, 5));
assert_first_instruction("SYNCID $1,$2,$3", MMixInstruction::SYNCID(1, 2, 3));
assert_first_instruction("SYNCIDI $1,$2,5", MMixInstruction::SYNCIDI(1, 2, 5));
assert_first_instruction("SYNC 5", MMixInstruction::SYNC(5));
}
#[test]
fn test_prefix_robust_extra_load_store() {
assert_first_instruction("LDUNC $1,$2,$3", MMixInstruction::LDUNC(1, 2, 3));
assert_first_instruction("LDUNCI $1,$2,5", MMixInstruction::LDUNCI(1, 2, 5));
assert_first_instruction("STUNC $1,$2,$3", MMixInstruction::STUNC(1, 2, 3));
assert_first_instruction("STUNCI $1,$2,5", MMixInstruction::STUNCI(1, 2, 5));
assert_first_instruction("LDHT $1,$2,$3", MMixInstruction::LDHT(1, 2, 3));
assert_first_instruction("LDHTI $1,$2,5", MMixInstruction::LDHTI(1, 2, 5));
assert_first_instruction("STHT $1,$2,$3", MMixInstruction::STHT(1, 2, 3));
assert_first_instruction("STHTI $1,$2,5", MMixInstruction::STHTI(1, 2, 5));
assert_first_instruction("LDSF $1,$2,$3", MMixInstruction::LDSF(1, 2, 3));
assert_first_instruction("LDSFI $1,$2,5", MMixInstruction::LDSFI(1, 2, 5));
assert_first_instruction("STSF $1,$2,$3", MMixInstruction::STSF(1, 2, 3));
assert_first_instruction("STSFI $1,$2,5", MMixInstruction::STSFI(1, 2, 5));
assert_first_instruction("LDVTS $1,$2,$3", MMixInstruction::LDVTS(1, 2, 3));
assert_first_instruction("LDVTSI $1,$2,5", MMixInstruction::LDVTSI(1, 2, 5));
assert_first_instruction("CSWAP $1,$2,$3", MMixInstruction::CSWAP(1, 2, 3));
assert_first_instruction("CSWAPI $1,$2,5", MMixInstruction::CSWAPI(1, 2, 5));
assert_first_instruction("STCO 5,$1,$2", MMixInstruction::STCO(5, 1, 2));
assert_first_instruction("STCOI 5,$1,7", MMixInstruction::STCOI(5, 1, 7));
}
type InstructionPredicate = fn(&MMixInstruction) -> bool;
#[test]
fn test_prefix_robust_branch_family() {
let cases: &[(&str, InstructionPredicate)] = &[
("BN $1,4", |i| matches!(i, MMixInstruction::BN(1, _))),
("BACK: HALT\nBNB $1,BACK", |i| {
matches!(i, MMixInstruction::BNB(1, _))
}),
("BNN $1,4", |i| matches!(i, MMixInstruction::BNN(1, _))),
("BACK: HALT\nBNNB $1,BACK", |i| {
matches!(i, MMixInstruction::BNNB(1, _))
}),
("BNP $1,4", |i| matches!(i, MMixInstruction::BNP(1, _))),
("BACK: HALT\nBNPB $1,BACK", |i| {
matches!(i, MMixInstruction::BNPB(1, _))
}),
("BNZ $1,4", |i| matches!(i, MMixInstruction::BNZ(1, _))),
("BACK: HALT\nBNZB $1,BACK", |i| {
matches!(i, MMixInstruction::BNZB(1, _))
}),
("BEV $1,4", |i| matches!(i, MMixInstruction::BEV(1, _))),
("BACK: HALT\nBEVB $1,BACK", |i| {
matches!(i, MMixInstruction::BEVB(1, _))
}),
("BOD $1,4", |i| matches!(i, MMixInstruction::BOD(1, _))),
("BACK: HALT\nBODB $1,BACK", |i| {
matches!(i, MMixInstruction::BODB(1, _))
}),
("BP $1,4", |i| matches!(i, MMixInstruction::BP(1, _))),
("BACK: HALT\nBPB $1,BACK", |i| {
matches!(i, MMixInstruction::BPB(1, _))
}),
("BZ $1,4", |i| matches!(i, MMixInstruction::BZ(1, _))),
("BACK: HALT\nBZB $1,BACK", |i| {
matches!(i, MMixInstruction::BZB(1, _))
}),
];
for (src, pred) in cases {
assert_last_instruction_matches(src, pred);
}
}
#[test]
fn test_prefix_robust_pbranch_family() {
let cases: &[(&str, InstructionPredicate)] = &[
("PBN $1,4", |i| matches!(i, MMixInstruction::PBN(1, _, _))),
("BACK: HALT\nPBNB $1,BACK", |i| {
matches!(i, MMixInstruction::PBNB(1, _, _))
}),
("PBNN $1,4", |i| matches!(i, MMixInstruction::PBNN(1, _, _))),
("BACK: HALT\nPBNNB $1,BACK", |i| {
matches!(i, MMixInstruction::PBNNB(1, _, _))
}),
("PBNP $1,4", |i| matches!(i, MMixInstruction::PBNP(1, _, _))),
("BACK: HALT\nPBNPB $1,BACK", |i| {
matches!(i, MMixInstruction::PBNPB(1, _, _))
}),
("PBNZ $1,4", |i| matches!(i, MMixInstruction::PBNZ(1, _, _))),
("BACK: HALT\nPBNZB $1,BACK", |i| {
matches!(i, MMixInstruction::PBNZB(1, _, _))
}),
("PBEV $1,4", |i| matches!(i, MMixInstruction::PBEV(1, _, _))),
("BACK: HALT\nPBEVB $1,BACK", |i| {
matches!(i, MMixInstruction::PBEVB(1, _, _))
}),
("PBOD $1,4", |i| matches!(i, MMixInstruction::PBOD(1, _, _))),
("BACK: HALT\nPBODB $1,BACK", |i| {
matches!(i, MMixInstruction::PBODB(1, _, _))
}),
("PBP $1,4", |i| matches!(i, MMixInstruction::PBP(1, _, _))),
("BACK: HALT\nPBPB $1,BACK", |i| {
matches!(i, MMixInstruction::PBPB(1, _, _))
}),
("PBZ $1,4", |i| matches!(i, MMixInstruction::PBZ(1, _, _))),
("BACK: HALT\nPBZB $1,BACK", |i| {
matches!(i, MMixInstruction::PBZB(1, _, _))
}),
];
for (src, pred) in cases {
assert_last_instruction_matches(src, pred);
}
}
#[test]
fn test_prefix_robust_shift_signed_vs_unsigned() {
assert_first_instruction("SL $1,$2,$3", MMixInstruction::SL(1, 2, 3));
assert_first_instruction("SLU $1,$2,$3", MMixInstruction::SLU(1, 2, 3));
assert_first_instruction("SR $1,$2,$3", MMixInstruction::SR(1, 2, 3));
assert_first_instruction("SRU $1,$2,$3", MMixInstruction::SRU(1, 2, 3));
assert_first_instruction("SL $1,$2,7", MMixInstruction::SLI(1, 2, 7));
assert_first_instruction("SLU $1,$2,7", MMixInstruction::SLUI(1, 2, 7));
assert_first_instruction("SR $1,$2,7", MMixInstruction::SRI(1, 2, 7));
assert_first_instruction("SRU $1,$2,7", MMixInstruction::SRUI(1, 2, 7));
assert_first_instruction("SLI $1,$2,7", MMixInstruction::SLI(1, 2, 7));
assert_first_instruction("SRUI $1,$2,7", MMixInstruction::SRUI(1, 2, 7));
}
#[test]
fn test_prefix_robust_conditional_csn_csnn() {
assert_first_instruction("CSN $1,$2,$3", MMixInstruction::CSN(1, 2, 3));
assert_first_instruction("CSNN $1,$2,$3", MMixInstruction::CSNN(1, 2, 3));
assert_first_instruction("CSNZ $1,$2,$3", MMixInstruction::CSNZ(1, 2, 3));
assert_first_instruction("CSNP $1,$2,$3", MMixInstruction::CSNP(1, 2, 3));
assert_first_instruction("CSN $1,$2,7", MMixInstruction::CSNI(1, 2, 7));
assert_first_instruction("CSNN $1,$2,7", MMixInstruction::CSNNI(1, 2, 7));
assert_first_instruction("CSNZ $1,$2,7", MMixInstruction::CSNZI(1, 2, 7));
assert_first_instruction("CSNP $1,$2,7", MMixInstruction::CSNPI(1, 2, 7));
assert_first_instruction("CSNI $1,$2,7", MMixInstruction::CSNI(1, 2, 7));
assert_first_instruction("CSNNI $1,$2,7", MMixInstruction::CSNNI(1, 2, 7));
}
#[test]
fn test_prefix_robust_zero_or_set_zsn_zsnn() {
assert_first_instruction("ZSN $1,$2,$3", MMixInstruction::ZSN(1, 2, 3));
assert_first_instruction("ZSNN $1,$2,$3", MMixInstruction::ZSNN(1, 2, 3));
assert_first_instruction("ZSNZ $1,$2,$3", MMixInstruction::ZSNZ(1, 2, 3));
assert_first_instruction("ZSNP $1,$2,$3", MMixInstruction::ZSNP(1, 2, 3));
assert_first_instruction("ZSN $1,$2,7", MMixInstruction::ZSNI(1, 2, 7));
assert_first_instruction("ZSNN $1,$2,7", MMixInstruction::ZSNNI(1, 2, 7));
assert_first_instruction("ZSNZ $1,$2,7", MMixInstruction::ZSNZI(1, 2, 7));
assert_first_instruction("ZSNP $1,$2,7", MMixInstruction::ZSNPI(1, 2, 7));
}
#[test]
fn test_immediate_boundary_zero() {
assert_first_instruction("ADD $1,$2,0", MMixInstruction::ADDI(1, 2, 0));
assert_first_instruction("AND $1,$2,0", MMixInstruction::ANDI(1, 2, 0));
assert_first_instruction("SR $1,$2,0", MMixInstruction::SRI(1, 2, 0));
}
#[test]
fn test_immediate_boundary_max_decimal_255() {
assert_first_instruction("ADD $1,$2,255", MMixInstruction::ADDI(1, 2, 255));
assert_first_instruction("OR $1,$2,255", MMixInstruction::ORI(1, 2, 255));
assert_first_instruction("ZSP $1,$2,255", MMixInstruction::ZSPI(1, 2, 255));
}
#[test]
fn test_immediate_boundary_max_hex_ff() {
assert_first_instruction("ADD $1,$2,#FF", MMixInstruction::ADDI(1, 2, 0xFF));
assert_first_instruction("XOR $1,$2,#FF", MMixInstruction::XORI(1, 2, 0xFF));
assert_first_instruction("CSN $1,$2,#FF", MMixInstruction::CSNI(1, 2, 0xFF));
}
#[test]
fn test_immediate_boundary_overflow_decimal_256() {
assert_parse_error_contains("ADD $1,$2,256", "out of range 0..255");
assert_parse_error_contains("AND $1,$2,256", "out of range 0..255");
assert_parse_error_contains("SRU $1,$2,256", "out of range 0..255");
}
#[test]
fn test_immediate_boundary_overflow_hex_100() {
assert_parse_error_contains("ADD $1,$2,#100", "out of range 0..255");
}
#[test]
fn test_immediate_boundary_overflow_large_value() {
assert_parse_error_contains("ADD $1,$2,#10000", "out of range 0..255");
assert_parse_error_contains("ADD $1,$2,1000000", "out of range 0..255");
}
#[test]
fn test_immediate_boundary_negative_rejected_in_auto() {
assert_parse_error_contains("ADD $1,$2,-1", "out of range 0..255");
assert_parse_error_contains("AND $1,$2,-128", "out of range 0..255");
}
#[test]
fn test_immediate_register_max_255() {
assert_first_instruction("ADD $1,$2,$255", MMixInstruction::ADD(1, 2, 255));
assert_first_instruction("AND $1,$2,$255", MMixInstruction::AND(1, 2, 255));
}
#[test]
fn test_immediate_char_literal() {
assert_first_instruction("ADD $1,$2,'A'", MMixInstruction::ADDI(1, 2, 65));
assert_first_instruction("AND $1,$2,'\\n'", MMixInstruction::ANDI(1, 2, 10));
assert_first_instruction("OR $1,$2,'\\0'", MMixInstruction::ORI(1, 2, 0));
}
#[test]
fn test_symbol_z_constant_in_range() {
assert_first_instruction("K IS 0\nADD $1,$2,K", MMixInstruction::ADDI(1, 2, 0));
assert_first_instruction("K IS 255\nAND $1,$2,K", MMixInstruction::ANDI(1, 2, 255));
assert_first_instruction("K IS 42\nCSZ $1,$2,K", MMixInstruction::CSZI(1, 2, 42));
}
#[test]
fn test_symbol_z_constant_out_of_range() {
assert_parse_error_contains("K IS 256\nADD $1,$2,K", "out of range 0..255");
assert_parse_error_contains("K IS 1000\nAND $1,$2,K", "out of range 0..255");
}
#[test]
fn test_neg_immediate_spelling_range_checks_its_z() {
assert_parse_error_contains("NEGI $1,0,#300", "out of range 0..255");
assert_parse_error_contains("NEGUI $1,0,#300", "out of range 0..255");
assert_parse_error_contains("BigC IS #300\nNEGI $1,0,BigC", "out of range 0..255");
assert_parse_error_contains("BigC IS #300\nNEGUI $1,0,BigC", "out of range 0..255");
assert_parse_error_contains("NEGI $1,0,-1", "out of range 0..255");
assert_parse_error_contains("NEGUI $1,0,-1", "out of range 0..255");
assert_first_instruction("NEGI $1,0,5", MMixInstruction::NEGI(1, 0, 5));
assert_first_instruction("NEGUI $1,0,5", MMixInstruction::NEGUI(1, 0, 5));
assert_first_instruction(
"SmallC IS 5\nNEGI $1,0,SmallC",
MMixInstruction::NEGI(1, 0, 5),
);
}
#[test]
fn test_symbol_z_register_alias_zero() {
assert_first_instruction("Z IS $0\nADD $1,$2,Z", MMixInstruction::ADD(1, 2, 0));
}
#[test]
fn test_symbol_z_register_alias_max() {
assert_first_instruction("M IS $255\nAND $1,$2,M", MMixInstruction::AND(1, 2, 255));
}
#[test]
fn test_symbol_z_label_address_out_of_range() {
let src = "\
LOC #200
Foo OCTA 0
Main ADD $1,$2,Foo
";
assert_parse_error_contains(src, "out of range 0..255");
}
#[test]
fn test_symbol_z_undefined_errors() {
assert_parse_error_contains("ADD $1,$2,Nope", "Undefined symbol");
assert_parse_error_contains("AND $1,$2,Nope", "Undefined symbol");
}
#[test]
fn test_cross_family_routing_in_one_program() {
let src = "\
ADD $1,$2,5
AND $3,$4,7
SR $5,$6,3
BDIF $7,$8,9
CSZ $1,$2,1
ZSP $3,$4,2
";
let mut asm = MMixAssembler::new(src, "<test>");
asm.parse().unwrap();
assert_eq!(
asm.instructions
.iter()
.map(|(_, i)| i.clone())
.collect::<Vec<_>>(),
vec![
MMixInstruction::ADDI(1, 2, 5),
MMixInstruction::ANDI(3, 4, 7),
MMixInstruction::SRI(5, 6, 3),
MMixInstruction::BDIFI(7, 8, 9),
MMixInstruction::CSZI(1, 2, 1),
MMixInstruction::ZSPI(3, 4, 2),
]
);
}
#[test]
fn test_perturbation_and_xor_or() {
assert_first_instruction("AND $1,$2,#FF", MMixInstruction::ANDI(1, 2, 0xFF));
assert_first_instruction("XOR $5,$6,#0F", MMixInstruction::XORI(5, 6, 0x0F));
assert_first_instruction("OR $10,$20,#80", MMixInstruction::ORI(10, 20, 0x80));
}
#[test]
fn test_perturbation_bitfiddle_family() {
assert_first_instruction("BDIF $1,$2,#10", MMixInstruction::BDIFI(1, 2, 0x10));
assert_first_instruction("WDIF $1,$2,100", MMixInstruction::WDIFI(1, 2, 100));
assert_first_instruction("TDIF $1,$2,50", MMixInstruction::TDIFI(1, 2, 50));
assert_first_instruction("ODIF $1,$2,255", MMixInstruction::ODIFI(1, 2, 255));
assert_first_instruction("SADD $1,$2,0", MMixInstruction::SADDI(1, 2, 0));
assert_first_instruction("MOR $1,$2,128", MMixInstruction::MORI(1, 2, 128));
assert_first_instruction("MXOR $1,$2,64", MMixInstruction::MXORI(1, 2, 64));
}
#[test]
fn test_perturbation_shift_family() {
assert_first_instruction("SL $3,$1,8", MMixInstruction::SLI(3, 1, 8));
assert_first_instruction("SLU $1,$2,16", MMixInstruction::SLUI(1, 2, 16));
assert_first_instruction("SR $1,$2,4", MMixInstruction::SRI(1, 2, 4));
assert_first_instruction("SRU $1,$2,32", MMixInstruction::SRUI(1, 2, 32));
}
#[test]
fn test_byte_identical_regression_all_families() {
let pairs: &[(&str, &str)] = &[
("ADD $1,$2,5", "ADDI $1,$2,5"),
("ADDU $1,$2,5", "ADDUI $1,$2,5"),
("2ADDU $1,$2,5", "2ADDUI $1,$2,5"),
("4ADDU $1,$2,5", "4ADDUI $1,$2,5"),
("8ADDU $1,$2,5", "8ADDUI $1,$2,5"),
("16ADDU $1,$2,5", "16ADDUI $1,$2,5"),
("SUB $1,$2,5", "SUBI $1,$2,5"),
("SUBU $1,$2,5", "SUBUI $1,$2,5"),
("MUL $1,$2,5", "MULI $1,$2,5"),
("MULU $1,$2,5", "MULUI $1,$2,5"),
("DIV $1,$2,5", "DIVI $1,$2,5"),
("DIVU $1,$2,5", "DIVUI $1,$2,5"),
("CMP $1,$2,5", "CMPI $1,$2,5"),
("CMPU $1,$2,5", "CMPUI $1,$2,5"),
("AND $1,$2,5", "ANDI $1,$2,5"),
("OR $1,$2,5", "ORI $1,$2,5"),
("XOR $1,$2,5", "XORI $1,$2,5"),
("ANDN $1,$2,5", "ANDNI $1,$2,5"),
("ORN $1,$2,5", "ORNI $1,$2,5"),
("NAND $1,$2,5", "NANDI $1,$2,5"),
("NOR $1,$2,5", "NORI $1,$2,5"),
("NXOR $1,$2,5", "NXORI $1,$2,5"),
("MUX $1,$2,5", "MUXI $1,$2,5"),
("BDIF $1,$2,5", "BDIFI $1,$2,5"),
("WDIF $1,$2,5", "WDIFI $1,$2,5"),
("TDIF $1,$2,5", "TDIFI $1,$2,5"),
("ODIF $1,$2,5", "ODIFI $1,$2,5"),
("SADD $1,$2,5", "SADDI $1,$2,5"),
("MOR $1,$2,5", "MORI $1,$2,5"),
("MXOR $1,$2,5", "MXORI $1,$2,5"),
("SL $1,$2,5", "SLI $1,$2,5"),
("SLU $1,$2,5", "SLUI $1,$2,5"),
("SR $1,$2,5", "SRI $1,$2,5"),
("SRU $1,$2,5", "SRUI $1,$2,5"),
("CSN $1,$2,5", "CSNI $1,$2,5"),
("CSZ $1,$2,5", "CSZI $1,$2,5"),
("CSP $1,$2,5", "CSPI $1,$2,5"),
("CSOD $1,$2,5", "CSODI $1,$2,5"),
("CSNN $1,$2,5", "CSNNI $1,$2,5"),
("CSNZ $1,$2,5", "CSNZI $1,$2,5"),
("CSNP $1,$2,5", "CSNPI $1,$2,5"),
("CSEV $1,$2,5", "CSEVI $1,$2,5"),
("ZSN $1,$2,5", "ZSNI $1,$2,5"),
("ZSZ $1,$2,5", "ZSZI $1,$2,5"),
("ZSP $1,$2,5", "ZSPI $1,$2,5"),
("ZSOD $1,$2,5", "ZSODI $1,$2,5"),
("ZSNN $1,$2,5", "ZSNNI $1,$2,5"),
("ZSNZ $1,$2,5", "ZSNZI $1,$2,5"),
("ZSNP $1,$2,5", "ZSNPI $1,$2,5"),
("ZSEV $1,$2,5", "ZSEVI $1,$2,5"),
];
for (auto_src, explicit_src) in pairs {
let mut auto_asm = MMixAssembler::new(auto_src, "<auto>");
auto_asm
.parse()
.unwrap_or_else(|e| panic!("auto src {auto_src:?} failed: {e}"));
let mut explicit_asm = MMixAssembler::new(explicit_src, "<explicit>");
explicit_asm
.parse()
.unwrap_or_else(|e| panic!("explicit src {explicit_src:?} failed: {e}"));
let auto_bytes = auto_asm.encode_instruction_bytes(&auto_asm.instructions[0].1);
let explicit_bytes =
explicit_asm.encode_instruction_bytes(&explicit_asm.instructions[0].1);
assert_eq!(
auto_bytes,
explicit_bytes,
"byte mismatch: auto {auto_src:?} -> {:?} {:?} vs explicit {explicit_src:?} -> {:?} {:?}",
auto_asm.instructions[0].1,
auto_bytes,
explicit_asm.instructions[0].1,
explicit_bytes
);
}
}
#[test]
fn test_explicit_i_negative_still_wraps() {
assert_first_instruction("ADDI $1,$2,-1", MMixInstruction::ADDI(1, 2, 0xFF));
assert_first_instruction("ANDI $1,$2,-1", MMixInstruction::ANDI(1, 2, 0xFF));
assert_first_instruction("SLUI $1,$2,-1", MMixInstruction::SLUI(1, 2, 0xFF));
assert_parse_error_contains("ADD $1,$2,-1", "out of range 0..255");
}
#[test]
fn test_debug_directive_preserves_line_fidelity() {
let lines = [
"\tLOC\tData_Segment",
"\tGREG\t@",
"Text\tBYTE\t\"Hello world!\",'\\n',0",
"",
"\tLOC\t#100",
"",
"Main\tdebug \"Version 0.1: Hello World Example\"",
"Start\tLDA\t$255,Text",
"\tTRAP\t0,Fputs,StdOut",
"\tTRAP\t0,Halt,0",
];
let lda_line = lines.iter().position(|l| l.contains("LDA")).unwrap() + 1;
let source = lines.join("\n");
let mut asm = MMixAssembler::new(&source, "hello_world.mms");
asm.parse().unwrap();
let lda_addr = *asm.labels.get("Start").expect("Start label defined");
let loc = asm
.source_loc(lda_addr)
.expect("LDA's address should have a source location");
assert_eq!(loc.file, "hello_world.mms");
assert_eq!(loc.line, lda_line, "LDA must report its ORIGINAL line");
}
#[test]
fn test_addr_for_line_round_trips_with_source_loc() {
let source = "Main\tdebug \"hi\"\nStart\tLDA\t$255,Start\n\tTRAP\t0,Halt,0\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let lda_addr = *asm.labels.get("Start").unwrap();
let loc = asm.source_loc(lda_addr).unwrap();
assert_eq!(
asm.addr_for_line(&loc.file, loc.line),
Some(lda_addr),
"addr_for_line must invert source_loc"
);
}
#[test]
fn test_source_text_returns_original_not_preprocessed() {
let source = "Main\tdebug \"hi\"\nStart\tLDA\t$255,Start\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let text = asm.source_text("<test>", 1).expect("line 1 exists");
assert!(
text.contains("debug"),
"expected original text, got {text:?}"
);
assert!(
!text.contains("PUSHJ"),
"source_text must not leak preprocessed text, got {text:?}"
);
let lda_text = asm.source_text("<test>", 2).expect("line 2 exists");
assert!(lda_text.contains("LDA"));
}
#[test]
fn test_generated_debug_subroutine_has_no_source_loc() {
let source = "Main\tdebug \"hi\"\nStart\tLDA\t$255,Start\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let debug_sub_addr = *asm
.labels
.get("DbgStr_0001")
.expect("generated debug subroutine label exists");
assert_eq!(
asm.source_loc(debug_sub_addr),
None,
"generated code must not map to a user source line"
);
}
#[test]
fn test_syntax_error_after_a_debug_line_reports_the_original_line() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
\tSET\t$1,7
\tFLOT\t$1,$2,$3
";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm
.parse()
.expect_err("FLOT $1,$2,$3 must still fail to parse");
assert_eq!(
err, "<test>:4:12: syntax error: expected EOI",
"must report original line 4, not the preprocessed line the \
debug expansion's landing pad shifts it to"
);
}
#[test]
fn test_redefinition_after_two_debug_lines_reports_original_lines() {
let source = "\
\tLOC\t#100
Main\tdebug\t\"hi\"
\tdebug\t\"ho\"
Foo\tIS\t1
\tSET\t$1,7
Foo\tIS\t2
";
let mut asm = MMixAssembler::new(source, "<test>");
let err = asm
.parse()
.expect_err("redefining Foo must still be rejected");
assert_eq!(
err, "<test>:6: symbol 'Foo' redefined (first defined at <test>:4)",
"both sites must report their ORIGINAL lines, not the \
preprocessed lines two debug expansions shift them to"
);
}
#[test]
fn test_multi_word_data_directive_maps_to_one_line() {
let source = "Data\tBYTE\t1,2,3\nWide\tOCTA\t1,2,3\n";
let mut asm = MMixAssembler::new(source, "<test>");
asm.parse().unwrap();
let base_addr = *asm.labels.get("Data").unwrap();
for offset in 0..3 {
let loc = asm
.source_loc(base_addr + offset)
.unwrap_or_else(|| panic!("no source_loc at offset {offset}"));
assert_eq!(loc.line, 1);
}
assert_eq!(asm.addr_for_line("<test>", 1), Some(base_addr));
let wide_addr = *asm.labels.get("Wide").unwrap();
for offset in 0..3 {
let addr = wide_addr + offset * 8;
let loc = asm
.source_loc(addr)
.unwrap_or_else(|| panic!("no source_loc at unit {offset}"));
assert_eq!(loc.line, 2);
}
assert_eq!(asm.addr_for_line("<test>", 2), Some(wide_addr));
}
const TWO_REGION_PROGRAM: &str = "\
LOC Data_Segment
Cells OCTA 0
OCTA 0
OCTA 0
Sp GREG Cells
LOC #100
Main SETI $1,7
STOI $1,Sp,0
ADDUI Sp,Sp,8
SETI $1,35
STOI $1,Sp,0
LDOI $2,Sp,0
SUBUI Sp,Sp,8
LDOI $3,Sp,0
ADDU $255,$2,$3
TRAP 0,Halt,0
";
#[test]
fn test_source_loc_covers_an_expansion_and_stops_at_its_end() {
let mut asm = MMixAssembler::new(TWO_REGION_PROGRAM, "stack.mms");
asm.parse().unwrap();
let main = *asm.labels.get("Main").unwrap();
assert_eq!(main, 0x100);
for addr in [0x100, 0x104, 0x108, 0x10c] {
let loc = asm
.source_loc(addr)
.unwrap_or_else(|| panic!("no source_loc at 0x{addr:x}"));
assert_eq!(loc.line, 8, "0x{addr:x} is inside line 8's SETI");
}
assert_eq!(asm.source_loc(0x110).map(|loc| loc.line), Some(9));
assert_eq!(asm.source_loc(0x140), None);
}
#[test]
fn test_source_loc_maps_the_data_region_independently() {
let mut asm = MMixAssembler::new(TWO_REGION_PROGRAM, "stack.mms");
asm.parse().unwrap();
let cells = *asm.labels.get("Cells").unwrap();
assert!(cells >= 0x2000_0000_0000_0000);
assert_eq!(asm.source_loc(cells).map(|loc| loc.line), Some(2));
assert_eq!(asm.source_loc(cells + 7).map(|loc| loc.line), Some(2));
assert_eq!(asm.source_loc(cells + 8).map(|loc| loc.line), Some(3));
assert_eq!(asm.source_loc(cells + 24), None);
}
#[test]
fn test_source_text_resolves_duplicate_filename_to_first_unit() {
let mut asm = MMixAssembler::new("First\tHALT\n", "dup.mms");
asm.add_source("Second\tHALT\n", "dup.mms");
asm.parse().unwrap();
let text = asm.source_text("dup.mms", 1).unwrap();
assert!(text.contains("First"));
}
fn fixture_reader(
files: Vec<(&str, &str)>,
) -> impl Fn(&std::path::Path) -> std::io::Result<String> {
let map: HashMap<std::path::PathBuf, String> = files
.into_iter()
.map(|(p, s)| (std::path::PathBuf::from(p), s.to_string()))
.collect();
move |p: &std::path::Path| {
map.get(p).cloned().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "no such fixture file")
})
}
}
#[test]
fn resolve_includes_single_include_inserts_a_unit() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"INCLUDE lib.mms\nOCTA 2\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(units.len(), 2);
assert_eq!(units[0].0, "lib.mms");
assert!(units[0].1.contains("OCTA 1"));
assert_eq!(units[1].0, "root.mms");
assert!(units[1].1.contains("OCTA 2"));
}
#[test]
fn resolve_includes_filename_fidelity() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"INCLUDE lib.mms\nOCTA 2\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(units[0].0, "lib.mms");
assert_ne!(units[0].0, "root.mms");
}
#[test]
fn resolve_includes_pads_line_numbers_after_an_include() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"% a\nINCLUDE lib.mms\nBYTE 0\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
let host_segment = &units.last().unwrap().1;
assert_eq!(host_segment.matches('\n').count() - 1, 2);
assert!(host_segment.starts_with("\n\nBYTE 0"));
let mut asm = MMixAssembler::new(&units[0].1, &units[0].0);
for (name, src) in units.iter().skip(1) {
asm.add_source(src, name);
}
asm.parse().unwrap();
assert_eq!(asm.addr_for_line("root.mms", 3), Some(8));
}
#[test]
fn resolve_includes_nested_relative_resolution() {
let reader = fixture_reader(vec![
("a/sub/b.mms", "INCLUDE c.mms\nOCTA 2\n"),
("a/sub/c.mms", "OCTA 3\n"),
]);
let units = MMixAssembler::resolve_includes(
"INCLUDE sub/b.mms\nOCTA 1\n",
"a/root.mms",
std::path::Path::new("a"),
&reader,
)
.unwrap();
let c_unit = units
.iter()
.find(|(name, _)| name.contains("c.mms"))
.expect("c.mms unit present");
assert_eq!(c_unit.0, "a/sub/c.mms");
assert!(c_unit.1.contains("OCTA 3"));
}
#[test]
fn resolve_includes_cycle_is_an_error() {
let reader = fixture_reader(vec![
("a.mms", "INCLUDE b.mms\n"),
("b.mms", "INCLUDE a.mms\n"),
]);
let err = MMixAssembler::resolve_includes(
"INCLUDE a.mms\n",
"driver.mms",
std::path::Path::new(""),
&reader,
)
.unwrap_err();
assert!(err.contains("cycle"));
assert!(err.contains("a.mms"));
assert!(err.contains("b.mms"));
}
#[test]
fn resolve_includes_missing_file_is_an_error_not_a_panic() {
let reader = fixture_reader(vec![]);
let err = MMixAssembler::resolve_includes(
"INCLUDE missing.mms\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap_err();
assert!(err.contains("missing.mms"));
}
#[test]
fn resolve_includes_passthrough_preserves_content_with_no_include() {
let reader = fixture_reader(vec![]);
let source = "OCTA 1\nOCTA 2";
let units =
MMixAssembler::resolve_includes(source, "root.mms", std::path::Path::new(""), &reader)
.unwrap();
assert_eq!(units.len(), 1);
assert_eq!(units[0].0, "root.mms");
assert_eq!(units[0].1, source);
}
#[test]
fn resolve_includes_recognizes_comment_case_and_leading_dot() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let lower_with_comment = MMixAssembler::resolve_includes(
".include lib.mms % pull it in\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
let quoted = MMixAssembler::resolve_includes(
"INCLUDE \"lib.mms\"\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(lower_with_comment.len(), 1);
assert_eq!(quoted.len(), 1);
assert_eq!(lower_with_comment[0].1, quoted[0].1);
assert!(quoted[0].1.contains("OCTA 1"));
}
}