use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::Path;
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, UnassignedPolicy,
};
use crate::core::sync::{self, LockRank};
use crate::core::value::Endian;
use super::{ADDRESS_MASK, Config, M68k, Regs, flags};
pub(super) static LEDGER: &[(&str, &str)] = &[(
"STOP",
"settles the prefetch queue before stopping, so it costs two bus cycles \
and eight clocks where hardware makes those cycles on the way out and \
bills four. Deliberate: the alternative leaves the resume address two \
bytes inside the instruction. Not in the corpus.",
)];
pub(super) static ANOMALIES: &[(&str, &str)] = &[
(
"e502 [ASL.b Q, D2] 1583",
"expects all 32 bits of D2 to change for a byte shift",
),
(
"e502 [ASL.b Q, D2] 1761",
"expects all 32 bits of D2 to change for a byte shift",
),
];
#[derive(Debug, Clone, PartialEq)]
enum Json {
Num(i64),
Str(String),
Arr(Vec<Json>),
Obj(Vec<(String, Json)>),
}
impl Json {
fn get(&self, key: &str) -> Option<&Json> {
match self {
Json::Obj(fields) => fields.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
fn num(&self) -> i64 {
match self {
Json::Num(n) => *n,
other => panic!("expected a number, got {other:?}"),
}
}
fn u32_at(&self, key: &str) -> u32 {
self.get(key)
.unwrap_or_else(|| panic!("missing field {key}"))
.num() as u32
}
fn arr(&self) -> &[Json] {
match self {
Json::Arr(items) => items,
other => panic!("expected an array, got {other:?}"),
}
}
fn str(&self) -> &str {
match self {
Json::Str(s) => s,
other => panic!("expected a string, got {other:?}"),
}
}
}
struct Parser<'a> {
bytes: &'a [u8],
at: usize,
}
impl<'a> Parser<'a> {
fn new(bytes: &'a [u8]) -> Parser<'a> {
Parser { bytes, at: 0 }
}
fn skip_space(&mut self) {
while matches!(self.bytes.get(self.at), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.at += 1;
}
}
fn eat(&mut self, byte: u8) {
self.skip_space();
assert_eq!(
self.bytes.get(self.at),
Some(&byte),
"at offset {}",
self.at
);
self.at += 1;
}
fn peek(&mut self) -> u8 {
self.skip_space();
self.bytes.get(self.at).copied().unwrap_or(b'\0')
}
fn value(&mut self) -> Json {
match self.peek() {
b'{' => {
self.at += 1;
let mut fields = Vec::new();
if self.peek() == b'}' {
self.at += 1;
return Json::Obj(fields);
}
loop {
let key = self.string();
self.eat(b':');
fields.push((key, self.value()));
match self.peek() {
b',' => self.at += 1,
_ => break,
}
}
self.eat(b'}');
Json::Obj(fields)
}
b'[' => {
self.at += 1;
let mut items = Vec::new();
if self.peek() == b']' {
self.at += 1;
return Json::Arr(items);
}
loop {
items.push(self.value());
match self.peek() {
b',' => self.at += 1,
_ => break,
}
}
self.eat(b']');
Json::Arr(items)
}
b'"' => Json::Str(self.string()),
b'n' => {
self.at += 4;
Json::Num(0)
}
b't' => {
self.at += 4;
Json::Num(1)
}
b'f' => {
self.at += 5;
Json::Num(0)
}
_ => {
let start = self.at;
if self.bytes.get(self.at) == Some(&b'-') {
self.at += 1;
}
while matches!(self.bytes.get(self.at), Some(b'0'..=b'9')) {
self.at += 1;
}
let text = core::str::from_utf8(&self.bytes[start..self.at]).expect("ascii");
Json::Num(text.parse().expect("integer"))
}
}
}
fn string(&mut self) -> String {
self.eat(b'"');
let start = self.at;
while self.bytes.get(self.at) != Some(&b'"') {
self.at += 1;
}
let text = String::from_utf8_lossy(&self.bytes[start..self.at]).into_owned();
self.at += 1;
text
}
}
#[derive(Debug, Clone, Copy, Eq)]
struct Access {
addr: u32,
value: u16,
word: bool,
write: bool,
high_bit_unknown: bool,
}
impl PartialEq for Access {
fn eq(&self, other: &Access) -> bool {
let mask = if self.high_bit_unknown || other.high_bit_unknown {
0x7f
} else {
0xffff
};
self.addr == other.addr
&& self.word == other.word
&& self.write == other.write
&& self.value & mask == other.value & mask
}
}
impl core::fmt::Display for Access {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"[{}{} {:06x} {}]",
if self.write { 'w' } else { 'r' },
if self.word { ".w" } else { ".b" },
self.addr,
if self.word {
format!("{:04x}", self.value)
} else {
format!("{:02x}", self.value)
}
)
}
}
#[derive(Debug)]
struct Bus(sync::Mutex<BusState>);
#[derive(Debug)]
struct BusState {
memory: Vec<u8>,
dirty: BTreeMap<u32, u8>,
log: Vec<Access>,
}
impl Bus {
fn new() -> Bus {
Bus(sync::Mutex::with_rank(
LockRank::DEVICE,
BusState {
memory: alloc::vec![0u8; (ADDRESS_MASK as usize) + 1],
dirty: BTreeMap::new(),
log: Vec::new(),
},
))
}
fn poke(&self, addr: u32, value: u8) {
let mut state = self.0.lock();
let addr = addr & ADDRESS_MASK;
let previous = state.memory[addr as usize];
state.dirty.entry(addr).or_insert(previous);
state.memory[addr as usize] = value;
}
fn peek(&self, addr: u32) -> u8 {
self.0.lock().memory[(addr & ADDRESS_MASK) as usize]
}
fn rewind(&self) {
let mut state = self.0.lock();
let dirty = core::mem::take(&mut state.dirty);
for (addr, previous) in dirty {
state.memory[addr as usize] = previous;
}
state.log.clear();
}
fn take_log(&self) -> Vec<Access> {
core::mem::take(&mut self.0.lock().log)
}
}
impl MemOps for Bus {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let mut state = self.0.lock();
let base = offset as u32 & ADDRESS_MASK;
for (i, slot) in dst.iter_mut().enumerate() {
*slot = state.memory[(base.wrapping_add(i as u32) & ADDRESS_MASK) as usize];
}
if !attrs.debug {
let value = if dst.len() == 2 {
(u16::from(dst[0]) << 8) | u16::from(dst[1])
} else {
u16::from(dst[0])
};
state.log.push(Access {
addr: base,
value,
word: dst.len() == 2,
write: false,
high_bit_unknown: false,
});
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let mut state = self.0.lock();
let base = offset as u32 & ADDRESS_MASK;
for (i, byte) in src.iter().enumerate() {
let addr = base.wrapping_add(i as u32) & ADDRESS_MASK;
let previous = state.memory[addr as usize];
state.dirty.entry(addr).or_insert(previous);
state.memory[addr as usize] = *byte;
}
if !attrs.debug {
let value = if src.len() == 2 {
(u16::from(src[0]) << 8) | u16::from(src[1])
} else {
u16::from(src[0])
};
state.log.push(Access {
addr: base,
value,
word: src.len() == 2,
write: true,
high_bit_unknown: false,
});
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::ANY.with_endian(Endian::Big)
}
}
fn regs_of(node: &Json) -> Regs {
let mut regs = Regs {
usp: node.u32_at("usp"),
ssp: node.u32_at("ssp"),
pc: node.u32_at("pc"),
sr: node.u32_at("sr") as u16,
..Regs::default()
};
for i in 0..8 {
regs.d[i] = node.u32_at(&format!("d{i}"));
}
for i in 0..7 {
regs.a[i] = node.u32_at(&format!("a{i}"));
}
regs.a[7] = if regs.supervisor() {
regs.ssp
} else {
regs.usp
};
let prefetch = node.get("prefetch").expect("prefetch").arr();
regs.prefetch = [prefetch[0].num() as u16, prefetch[1].num() as u16];
regs
}
fn trace_of(node: &Json) -> Vec<Access> {
let mut out = Vec::new();
for t in node.get("transactions").expect("transactions").arr() {
let t = t.arr();
let kind = t[0].str();
if kind != "r" && kind != "w" && kind != "t" {
continue;
}
let addr = t[3].num() as u32 & ADDRESS_MASK;
let word = t[4].str() == ".w";
let value = t[5].num() as u16;
out.push(Access {
addr,
word,
value,
write: kind == "w",
high_bit_unknown: kind == "t",
});
if kind == "t" {
out.push(Access {
addr,
word,
value: value | 0x80,
write: true,
high_bit_unknown: false,
});
}
}
out
}
#[derive(Debug, Default)]
struct Tally {
vectors: usize,
skipped: usize,
state_failures: usize,
cycle_failures: usize,
trace_failures: usize,
first_state: Option<String>,
first_cycles: Option<String>,
first_trace: Option<String>,
}
impl Tally {
fn merge(&mut self, other: &Tally) {
self.vectors += other.vectors;
self.skipped += other.skipped;
self.state_failures += other.state_failures;
self.cycle_failures += other.cycle_failures;
self.trace_failures += other.trace_failures;
}
}
fn run_file(path: &Path, limit: usize) -> Tally {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let vectors = Parser::new(&bytes).value();
let bus = alloc::sync::Arc::new(Bus::new());
let space = AddressSpace::new("cpu", 24)
.with_endian(Endian::Big)
.with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(
Region::io("ram", u64::from(ADDRESS_MASK) + 1, bus.clone()),
0,
)
.expect("16 MiB fits in 24 bits");
let space = alloc::sync::Arc::new(space);
let cpu = M68k::new(Config::default());
cpu.attach_space(space);
let mut tally = Tally::default();
for vector in vectors.arr().iter().take(limit) {
let name = vector.get("name").expect("name").str().to_string();
if ANOMALIES.iter().any(|(anomaly, _)| *anomaly == name) {
tally.skipped += 1;
continue;
}
let initial = vector.get("initial").expect("initial");
let expected = vector.get("final").expect("final");
bus.rewind();
for cell in initial.get("ram").expect("ram").arr() {
let cell = cell.arr();
bus.poke(cell[0].num() as u32, cell[1].num() as u8);
}
cpu.set_reset_pending(false);
cpu.resume();
cpu.set_regs(regs_of(initial));
bus.take_log();
let used = cpu.step();
tally.vectors += 1;
let want = regs_of(expected);
let got = cpu.regs();
let mut detail = String::new();
if got != want {
detail.push_str(&diff_regs(&want, &got));
}
for cell in expected.get("ram").expect("ram").arr() {
let cell = cell.arr();
let addr = cell[0].num() as u32;
let value = cell[1].num() as u8;
let actual = bus.peek(addr);
if actual != value {
detail.push_str(&format!(
" ram[{addr:06x}] want {value:02x} got {actual:02x}\n"
));
}
}
if !detail.is_empty() {
tally.state_failures += 1;
if tally.first_state.is_none() {
tally.first_state = Some(format!("{name}\n{detail}"));
}
}
let want_cycles = vector.get("length").expect("length").num() as u64;
if used != want_cycles {
tally.cycle_failures += 1;
if tally.first_cycles.is_none() {
tally.first_cycles = Some(format!("{name}: want {want_cycles} cycles, got {used}"));
}
}
let want_trace = trace_of(vector);
let got_trace = bus.take_log();
if got_trace != want_trace {
tally.trace_failures += 1;
if tally.first_trace.is_none() {
let show = |cs: &[Access]| {
cs.iter()
.map(alloc::string::ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
};
tally.first_trace = Some(format!(
"{name}\n want {}\n got {}",
show(&want_trace),
show(&got_trace)
));
}
}
}
tally
}
fn diff_regs(want: &Regs, got: &Regs) -> String {
let mut out = String::new();
for i in 0..8 {
if want.d[i] != got.d[i] {
out.push_str(&format!(
" d{i} want {:08x} got {:08x}\n",
want.d[i], got.d[i]
));
}
}
for i in 0..7 {
if want.a[i] != got.a[i] {
out.push_str(&format!(
" a{i} want {:08x} got {:08x}\n",
want.a[i], got.a[i]
));
}
}
for (name, w, g) in [
("usp", want.usp, got.usp),
("ssp", want.ssp, got.ssp),
("pc", want.pc, got.pc),
] {
if w != g {
out.push_str(&format!(" {name} want {w:08x} got {g:08x}\n"));
}
}
if want.sr != got.sr {
out.push_str(&format!(
" sr want {:04x} ({}) got {:04x} ({})\n",
want.sr,
ccr_text(want.sr),
got.sr,
ccr_text(got.sr)
));
}
if want.prefetch != got.prefetch {
out.push_str(&format!(
" prefetch want {:04x},{:04x} got {:04x},{:04x}\n",
want.prefetch[0], want.prefetch[1], got.prefetch[0], got.prefetch[1]
));
}
out
}
fn ccr_text(sr: u16) -> String {
let mut out = String::new();
for (mask, name) in [
(flags::X, 'X'),
(flags::N, 'N'),
(flags::Z, 'Z'),
(flags::V, 'V'),
(flags::C, 'C'),
] {
out.push(if sr & mask != 0 { name } else { '-' });
}
out
}
#[test]
fn single_step_tests() {
let Ok(dir) = std::env::var("RSEMU_680X0_DIR") else {
println!(
"conformance: set RSEMU_680X0_DIR to a decompressed \
SingleStepTests/680x0 68000/v1 directory to run it. The corpus has \
no licence file, so it is fetched and run, never vendored."
);
return;
};
let dir = Path::new(&dir);
let only: Option<Vec<String>> = std::env::var("RSEMU_680X0_TESTS")
.ok()
.map(|s| s.split(',').map(|p| p.trim().to_string()).collect());
let limit: usize = std::env::var("RSEMU_680X0_LIMIT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let strict = std::env::var("RSEMU_680X0_STRICT").is_ok();
let mut names: Vec<String> = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("{}: {e}", dir.display()))
.filter_map(|entry| {
let entry = entry.ok()?;
let name = entry.file_name().into_string().ok()?;
let stem = name.strip_suffix(".json")?;
Some(stem.to_string())
})
.collect();
names.sort();
assert!(
!names.is_empty(),
"no NAME.json files under {} — the corpus ships gzipped; \
run `gunzip {}/*.json.gz` first",
dir.display(),
dir.display()
);
let mut total = Tally::default();
let mut state_failures: Vec<(String, usize)> = Vec::new();
let mut cycle_failures: Vec<(String, usize)> = Vec::new();
let mut trace_failures: Vec<(String, usize)> = Vec::new();
for name in &names {
if let Some(only) = &only
&& !only.iter().any(|o| o == name)
{
continue;
}
let tally = run_file(&dir.join(format!("{name}.json")), limit);
if tally.state_failures != 0 {
state_failures.push((name.clone(), tally.state_failures));
println!(
"{name}: {}/{} vectors with wrong state; first is\n{}",
tally.state_failures,
tally.vectors,
tally.first_state.as_deref().unwrap_or("")
);
}
if tally.cycle_failures != 0 {
cycle_failures.push((name.clone(), tally.cycle_failures));
println!(
"{name}: {}/{} vectors with wrong cycle count; first is {}",
tally.cycle_failures,
tally.vectors,
tally.first_cycles.as_deref().unwrap_or("")
);
}
if tally.trace_failures != 0 {
trace_failures.push((name.clone(), tally.trace_failures));
println!(
"{name}: {}/{} vectors with a different bus trace; first is\n{}",
tally.trace_failures,
tally.vectors,
tally.first_trace.as_deref().unwrap_or("")
);
}
total.merge(&tally);
}
println!(
"conformance: {} vectors — state {} ok, cycles {} ok, trace {} ok\
{}",
total.vectors,
total.vectors - total.state_failures,
total.vectors - total.cycle_failures,
total.vectors - total.trace_failures,
if total.skipped == 0 {
String::new()
} else {
format!(" ({} skipped as corpus anomalies)", total.skipped)
}
);
assert!(
state_failures.is_empty(),
"instruction families with wrong state: {state_failures:?}"
);
if strict {
assert!(
cycle_failures.is_empty(),
"families with wrong cycle counts: {cycle_failures:?}"
);
assert!(
trace_failures.is_empty(),
"families with a different bus trace: {trace_failures:?}"
);
}
}
#[test]
fn the_ledger_only_records_timing() {
for (name, why) in LEDGER {
assert!(
why.contains("time") || why.contains("cycle"),
"{name}: the ledger is for timing, not semantics"
);
}
}
#[test]
fn the_anomaly_list_stays_short() {
assert!(
ANOMALIES.len() <= 4,
"too many vectors dismissed as corpus bugs"
);
for (name, why) in ANOMALIES {
assert!(!why.is_empty(), "{name} needs a reason");
}
}