use std::collections::HashMap;
use crate::cpu::decode::decode;
use crate::cpu::disasm::disassemble;
const RECORD: usize = 66;
const PC: u32 = 0x10000;
struct Record {
name: String,
t1: Vec<u8>,
t2: Vec<u8>,
field_pos: usize,
param_pos: usize,
param_len: u8,
bias: i32,
relative: bool,
alt_code: u8,
}
fn hex(s: &[u8]) -> Option<Vec<u8>> {
s.iter()
.take_while(|&&b| b != 0)
.map(|&b| char::from(b).to_digit(16).map(|d| d as u8))
.collect()
}
fn cstr(s: &[u8]) -> String {
s.iter()
.take_while(|&&b| b != 0)
.map(|&b| char::from(b))
.collect()
}
fn norm(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
const FIELDS: [&str; 8] = ["P", "WP", "XS", "X", "S", "M", "B", "W"];
struct Inst {
rec: usize,
nibbles: Vec<u8>,
expected: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Skip {
PseudoOp,
Macro,
TestHalf,
CountOnlyLoad,
AsciiLoad,
DataNibble,
}
fn classify(r: &Record, raw_t1: &[u8]) -> Option<Skip> {
let n = r.name.as_str();
if raw_t1.first().is_none_or(|&b| b == 0) {
return Some(Skip::PseudoOp);
}
if hex(raw_t1).is_none() {
return Some(Skip::Macro);
}
if n == "RTNYES" || n == "GOYES" {
return Some(Skip::TestHalf);
}
if n.ends_with("(N)") {
return Some(Skip::CountOnlyLoad);
}
if n.ends_with("ASC") {
return Some(Skip::AsciiLoad);
}
if n == "NIBFS" {
return Some(Skip::DataNibble);
}
None
}
fn put(v: &mut Vec<u8>, pos: usize, n: u8) {
if v.len() <= pos {
v.resize(pos + 1, 0);
}
v[pos] = n;
}
fn le(n: &[u8]) -> u64 {
n.iter()
.rev()
.fold(0u64, |acc, &d| (acc << 4) | u64::from(d))
}
fn instantiate(idx: usize, r: &Record) -> Vec<Inst> {
let name = r.name.as_str();
let is_test = name.starts_with('?');
let fsd = r.field_pos != 0 && r.field_pos == r.param_pos;
let mut forms: Vec<(Vec<u8>, Option<String>)> = Vec::new();
if r.field_pos == 0 {
forms.push((r.t1.clone(), None));
} else {
for (code, f) in FIELDS.iter().enumerate() {
let mut t = r.t1.clone();
let base = t.get(r.field_pos).copied().unwrap_or(0);
put(&mut t, r.field_pos, base + code as u8);
forms.push((t, Some((*f).to_string())));
}
if !r.t2.is_empty() && r.alt_code == 0xF {
forms.push((r.t2.clone(), Some("A".to_string())));
} else {
let mut t = r.t1.clone();
put(&mut t, r.field_pos, 0xF);
forms.push((t, Some("A".to_string())));
}
if !r.t2.is_empty() && r.alt_code == 7 {
forms.push((r.t2.clone(), Some("W".to_string())));
}
if fsd {
for d in 1..=16u8 {
let mut t = r.t1.clone();
let op = t[r.field_pos - 1] + 8;
put(&mut t, r.field_pos - 1, op);
put(&mut t, r.field_pos, d - 1);
forms.push((t, Some(d.to_string())));
}
}
}
let mut out = Vec::new();
for (t, field) in forms {
let mut params: Vec<(Vec<u8>, Option<String>)> = Vec::new();
if r.param_len == 0 || fsd {
params.push((t, None));
} else if r.param_len == 0xFF {
let digits: usize = if name.starts_with('D') { 2 } else { 16 };
let mut v = t.clone();
if digits == 16 {
put(&mut v, r.param_pos - 1, 15);
}
let ds: Vec<u8> = (1..=digits).map(|i| (i % 16) as u8).collect();
for (i, &d) in ds.iter().enumerate() {
put(&mut v, r.param_pos + i, d);
}
params.push((v, Some(format!("{:0w$X}", le(&ds), w = digits))));
} else if r.param_len == 1 && !r.relative {
for raw in 0..16u8 {
let mut v = t.clone();
put(&mut v, r.param_pos, raw);
let value = i32::from(raw) - r.bias;
let text = if name.contains('(') {
format!("#{value:X}")
} else {
value.to_string()
};
params.push((v, Some(text)));
}
} else {
let len = usize::from(r.param_len);
let ds: Vec<u8> = (1..=len).map(|i| i as u8).collect();
let mut v = t.clone();
for (i, &d) in ds.iter().enumerate() {
put(&mut v, r.param_pos + i, d);
}
let raw = le(&ds);
let text = if r.relative {
let bits = 4 * len as u32;
let off = ((raw << (64 - bits)) as i64 >> (64 - bits)) as i32;
let base =
PC as usize + r.param_pos + if name.starts_with("GOSUB") { len } else { 0 };
format!("#{:05X}", (base as u32).wrapping_add_signed(off) & 0xF_FFFF)
} else if name.starts_with("GO") {
format!("#{raw:05X}")
} else {
format!("#{raw:0len$X}")
};
params.push((v, Some(text)));
}
for (v, ptext) in params {
let modifier = match (&field, &ptext) {
(Some(f), Some(p)) => format!("{f},{p}"),
(Some(f), None) => f.clone(),
(None, Some(p)) => p.clone(),
(None, None) => String::new(),
};
let base_text = norm(&format!("{name} {modifier}"));
if is_test {
let yy = v.len();
let mut rtn = v.clone();
put(&mut rtn, yy + 1, 0);
out.push(Inst {
rec: idx,
nibbles: rtn,
expected: format!("{base_text} RTNYES"),
});
let mut go = v.clone();
put(&mut go, yy, 4);
put(&mut go, yy + 1, 0);
out.push(Inst {
rec: idx,
nibbles: go,
expected: format!("{base_text} GOYES #{:05X}", PC as usize + yy + 4),
});
} else {
out.push(Inst {
rec: idx,
nibbles: v,
expected: base_text,
});
}
}
}
out
}
#[test]
fn sasm_opc_oracle() {
let Ok(dir) = std::env::var("SATURNUS_LITERATURE_DIR") else {
println!("SATURNUS_LITERATURE_DIR not set: SASM.OPC oracle test skipped");
return;
};
let path = std::path::Path::new(&dir).join("raw/saturn-hardware/hp48-sdk-1993/SASM.OPC");
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let Some(nl) = data.iter().position(|&b| b == b'\n') else {
panic!("SASM.OPC: no header line");
};
let header = String::from_utf8_lossy(&data[..nl]).to_string();
let Some(count) = header
.rsplit(':')
.next()
.and_then(|s| s.trim().parse::<usize>().ok())
else {
panic!("SASM.OPC: no record count in header {header:?}");
};
let body = &data[nl + 1..];
assert_eq!(body.len(), count * RECORD, "unexpected SASM.OPC size");
let mut records = Vec::new();
let mut skipped: HashMap<Skip, Vec<String>> = HashMap::new();
let mut insts = Vec::new();
for (idx, raw) in body.as_chunks::<RECORD>().0.iter().enumerate() {
let r = Record {
name: cstr(&raw[0..16]),
t1: hex(&raw[18..25]).unwrap_or_default(),
t2: hex(&raw[25..39]).unwrap_or_default(),
field_pos: usize::from(raw[39]),
param_pos: usize::from(raw[40]),
param_len: raw[41],
bias: i32::from_le_bytes([raw[42], raw[43], raw[44], raw[45]]),
relative: raw[46] == 1,
alt_code: raw[48],
};
match classify(&r, &raw[18..25]) {
Some(s) => skipped.entry(s).or_default().push(r.name.clone()),
None => insts.extend(instantiate(idx, &r)),
}
records.push(r);
}
let mut names: HashMap<Vec<u8>, Vec<String>> = HashMap::new();
for i in &insts {
names
.entry(i.nibbles.clone())
.or_default()
.push(i.expected.clone());
}
let mut per_rec: HashMap<usize, (usize, usize, usize)> = HashMap::new();
let mut failures = Vec::new();
for i in &insts {
let nibs = &i.nibbles;
let d = decode(
|a| {
let k = a.wrapping_sub(PC) & 0xF_FFFF;
nibs.get(k as usize).copied().unwrap_or(0)
},
PC,
);
let got = norm(&disassemble(&d.instr));
let e = per_rec.entry(i.rec).or_default();
let len_ok = usize::from(d.len) == nibs.len();
if len_ok && got == i.expected {
e.0 += 1;
} else if len_ok && names.get(nibs).is_some_and(|v| v.contains(&got)) {
e.1 += 1;
} else {
e.2 += 1;
let hexs: String = nibs.iter().map(|n| format!("{n:X}")).collect();
failures.push(format!(
"{} [{hexs}]: expected {:?} (len {}), got {got:?} (len {})",
records[i.rec].name,
i.expected,
nibs.len(),
d.len
));
}
}
let exact = per_rec.values().filter(|c| c.2 == 0 && c.1 == 0).count();
let synonym = per_rec.values().filter(|c| c.2 == 0 && c.1 > 0).count();
let failed = per_rec.values().filter(|c| c.2 > 0).count();
println!(
"SASM.OPC: {count} records; verified {}/{count} ({exact} exact, {synonym} via synonym), \
{failed} failed, {} instantiations",
exact + synonym,
insts.len()
);
let mut kinds: Vec<_> = skipped.iter().collect();
kinds.sort();
for (kind, names) in kinds {
println!(" not verifiable, {kind:?}: {}", names.len());
}
assert_eq!(
exact + synonym + failed + skipped.values().map(Vec::len).sum::<usize>(),
count
);
assert!(
failures.is_empty(),
"{} mismatches:\n{}",
failures.len(),
failures.join("\n")
);
}