use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use crate::core::space::{AddressSpace, RamStore, Region, UnassignedPolicy};
use super::cp0::{Segment, cause_bits};
use super::{Arch, Config, Cpu};
#[derive(Debug, Clone, PartialEq, Eq)]
struct VectorState {
regs: [u32; 32],
hi: u32,
lo: u32,
epc: u32,
_tar: u32,
cause: u32,
pc: u32,
branch_slot: bool,
branch_taken: bool,
branch_target: u32,
load_reg: Option<u32>,
load_value: u32,
}
#[derive(Debug, Clone, Copy)]
struct Cycle {
actions: u32,
size: u32,
addr: u32,
value: u32,
}
impl Cycle {
const fn is_write(self) -> bool {
self.actions & 2 != 0
}
}
#[derive(Debug, Clone)]
struct Vector {
name: String,
opcode: u32,
opcode_addr: u32,
initial: VectorState,
expected: VectorState,
cycles: Vec<Cycle>,
}
struct Reader<'a> {
bytes: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
const fn new(bytes: &'a [u8]) -> Reader<'a> {
Reader { bytes, at: 0 }
}
fn u32(&mut self) -> Option<u32> {
let slice = self.bytes.get(self.at..self.at + 4)?;
self.at += 4;
Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
}
fn u64(&mut self) -> Option<u64> {
let lo = u64::from(self.u32()?);
let hi = u64::from(self.u32()?);
Some(lo | (hi << 32))
}
fn pascal(&mut self, field: usize) -> Option<String> {
let slice = self.bytes.get(self.at..self.at + field)?;
self.at += field;
let len = usize::from(slice[0]).min(field - 1);
Some(String::from_utf8_lossy(&slice[1..1 + len]).into_owned())
}
fn state(&mut self) -> Option<VectorState> {
let mut regs = [0u32; 32];
for slot in &mut regs {
*slot = self.u32()?;
}
let hi = self.u32()?;
let lo = self.u32()?;
let epc = self.u32()?;
let tar = self.u32()?;
let cause = self.u32()?;
let pc = self.u32()?;
let branch_target = self.u32()?;
let branch_slot = self.u32()? != 0;
let branch_taken = self.u32()? != 0;
let load_target = self.u32()? as i32;
let load_value = self.u32()?;
Some(VectorState {
regs,
hi,
lo,
epc,
_tar: tar,
cause,
pc,
branch_slot,
branch_taken,
branch_target,
load_reg: (0..32).contains(&load_target).then_some(load_target as u32),
load_value,
})
}
fn vector(&mut self) -> Option<Vector> {
let name = self.pascal(51)?;
let opcode = self.u32()?;
let opcode_addr = self.u32()?;
let initial = self.state()?;
let expected = self.state()?;
let count = self.u32()? as usize;
let mut cycles = Vec::with_capacity(count.min(64));
for _ in 0..count {
let value = self.u64()? as u32;
let actions = self.u32()?;
let addr = self.u64()? as u32;
let size = self.u32()?;
cycles.push(Cycle {
actions,
size,
addr,
value,
});
}
Some(Vector {
name,
opcode,
opcode_addr,
initial,
expected,
cycles,
})
}
}
fn parse(bytes: &[u8]) -> Result<Vec<Vector>, String> {
let mut r = Reader::new(bytes);
let count = r.u32().ok_or("truncated header")? as usize;
let mut out = Vec::with_capacity(count.min(4096));
for i in 0..count {
out.push(
r.vector()
.ok_or_else(|| format!("truncated at vector {i}"))?,
);
}
Ok(out)
}
fn phys(vaddr: u32) -> u32 {
let segment = Segment::of(vaddr);
if segment.mapped() {
vaddr
} else {
Segment::unmapped_phys(vaddr)
}
}
const PAGE: u32 = 0x1000;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Skip {
Aliased,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Outcome {
Pass,
Fail(Vec<String>),
Skipped(Skip),
}
fn run_one(v: &Vector) -> Outcome {
let mut pages: BTreeMap<u32, Arc<RamStore>> = BTreeMap::new();
let mut seed: BTreeMap<u32, u8> = BTreeMap::new();
let mut expected: BTreeMap<u32, u8> = BTreeMap::new();
let mut words: BTreeSet<u32> = BTreeSet::new();
let mut seen: BTreeMap<u32, u32> = BTreeMap::new();
for c in &v.cycles {
let word = c.addr & !3;
let p = phys(word);
if let Some(prior) = seen.insert(p, word)
&& prior != word
{
return Outcome::Skipped(Skip::Aliased);
}
words.insert(p);
pages
.entry(p & !(PAGE - 1))
.or_insert_with(|| Arc::new(RamStore::new(u64::from(PAGE))));
for i in 0..c.size.min(4) {
let at = phys(c.addr).wrapping_add(i);
let byte = ((c.value >> (8 * i)) & 0xff) as u8;
seed.entry(at).or_insert(byte);
if c.is_write() {
expected.insert(at, byte);
}
}
}
let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
{
let mut topology = space.topology();
for (base, store) in &pages {
topology
.map(Region::ram("ram", Arc::clone(store)), u64::from(*base))
.expect("a page fits");
}
}
let byte_at = |map: &BTreeMap<u32, u8>, at: u32| map.get(&at).copied().unwrap_or(0);
for (at, byte) in &seed {
let store = &pages[&(at & !(PAGE - 1))];
store
.write_u8(u64::from(at & (PAGE - 1)), *byte)
.expect("in range");
}
let cpu = Cpu::new(Config::new(Arch::LR33300).with_reset_vector(v.initial.pc));
cpu.attach_space(Arc::new(space));
let s = &v.initial;
for (i, value) in s.regs.iter().enumerate() {
cpu.set_reg(i as u32, *value);
}
cpu.set_hi_lo(s.hi, s.lo);
let next_pc = if s.branch_slot && s.branch_taken {
s.branch_target
} else {
s.pc.wrapping_add(4)
};
cpu.set_control(s.pc, next_pc, s.branch_slot);
let mut cp0 = cpu.cp0();
cp0.epc = s.epc;
cp0.status = 0;
cp0.cause = s.cause & !cause_bits::HW;
cpu.set_cp0(cp0);
for pin in 0..6 {
let bit = 1 << (cause_bits::HW_SHIFT + pin);
cpu.set_interrupt(pin, s.cause & bit != 0);
}
cpu.set_pending_load(s.load_reg.map(|reg| (reg, s.load_value)));
cpu.step();
let want = &v.expected;
let mut bad: Vec<String> = Vec::new();
for i in 0..32u32 {
let got = cpu.reg(i);
if got != want.regs[i as usize] {
bad.push(format!(
"r{i} = {got:#010x}, expected {:#010x}",
want.regs[i as usize]
));
}
}
if cpu.hi() != want.hi {
bad.push(format!(
"hi = {:#010x}, expected {:#010x}",
cpu.hi(),
want.hi
));
}
if cpu.lo() != want.lo {
bad.push(format!(
"lo = {:#010x}, expected {:#010x}",
cpu.lo(),
want.lo
));
}
if cpu.pc() != want.pc {
bad.push(format!(
"pc = {:#010x}, expected {:#010x}",
cpu.pc(),
want.pc
));
}
if cpu.in_delay_slot() != want.branch_slot {
bad.push(format!(
"delay slot = {}, expected {}",
cpu.in_delay_slot(),
want.branch_slot
));
} else if want.branch_slot {
let want_next = if want.branch_taken {
want.branch_target
} else {
want.pc.wrapping_add(4)
};
if cpu.next_pc() != want_next {
bad.push(format!(
"next pc = {:#010x}, expected {want_next:#010x}",
cpu.next_pc()
));
}
}
let got_load = cpu.pending_load();
let want_load = want.load_reg.map(|r| (r, want.load_value));
if got_load != want_load {
bad.push(format!(
"pending load = {got_load:x?}, expected {want_load:x?}"
));
}
let cp0 = cpu.cp0();
if cp0.epc != want.epc {
bad.push(format!(
"epc = {:#010x}, expected {:#010x}",
cp0.epc, want.epc
));
}
let mask = cause_bits::EXC_CODE | cause_bits::BD;
if cp0.cause & mask != want.cause & mask {
bad.push(format!(
"cause = {:#010x}, expected {:#010x} (masked to ExcCode and BD)",
cp0.cause & mask,
want.cause & mask
));
}
for word in &words {
let store = &pages[&(word & !(PAGE - 1))];
for i in 0..4u32 {
let at = word.wrapping_add(i);
let got = store.read_u8(u64::from(at & (PAGE - 1))).expect("in range");
let want = expected
.get(&at)
.copied()
.unwrap_or_else(|| byte_at(&seed, at));
if got != want {
bad.push(format!(
"memory at {at:#010x} = {got:#04x}, expected {want:#04x}"
));
}
}
}
if bad.is_empty() {
Outcome::Pass
} else {
Outcome::Fail(bad)
}
}
fn corpus_files(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut out: Vec<PathBuf> = entries
.filter_map(std::result::Result::ok)
.map(|e| e.path())
.filter(|p| p.to_string_lossy().ends_with(".json.bin"))
.collect();
out.sort();
out
}
#[test]
fn single_step_tests_r3000() {
let Ok(dir) = std::env::var("RSEMU_MIPS_TESTS") else {
println!(
"conformance: RSEMU_MIPS_TESTS is not set, so nothing ran.\n\
`scripts/fetch-testdata.sh mips-r3000` downloads the corpus \
(SingleStepTests/r3000, MIT); the corpus is never committed."
);
return;
};
let only: Vec<String> = std::env::var("RSEMU_MIPS_TESTS_ONLY")
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let files = corpus_files(Path::new(&dir));
assert!(!files.is_empty(), "no .json.bin files under {dir}");
let mut total = 0usize;
let mut passed = 0usize;
let mut skipped = 0usize;
let mut failing_files: Vec<String> = Vec::new();
for path in files {
let name = path
.file_name()
.map(|n| n.to_string_lossy().replace(".json.bin", ""))
.unwrap_or_default();
if !only.is_empty() && !only.iter().any(|s| name.contains(s.as_str())) {
continue;
}
let bytes = std::fs::read(&path).expect("the corpus is readable");
let vectors = match parse(&bytes) {
Ok(v) => v,
Err(e) => panic!("{name}: {e}"),
};
let mut file_passed = 0usize;
let mut file_skipped = 0usize;
let mut first: Option<(String, Vec<String>)> = None;
for v in &vectors {
match run_one(v) {
Outcome::Pass => file_passed += 1,
Outcome::Skipped(_) => file_skipped += 1,
Outcome::Fail(why) => {
if first.is_none() {
first = Some((
format!(
"{} (opcode {:#010x} at {:#010x})",
v.name, v.opcode, v.opcode_addr
),
why,
));
}
}
}
}
let ran = vectors.len() - file_skipped;
total += ran;
passed += file_passed;
skipped += file_skipped;
let failed = ran - file_passed;
if failed == 0 {
println!("{name:10} {file_passed:5}/{ran:<5} ok");
} else {
failing_files.push(name.clone());
println!("{name:10} {file_passed:5}/{ran:<5} FAILED {failed}");
if let Some((what, why)) = first {
println!(" first: {what}");
for line in why.iter().take(6) {
println!(" {line}");
}
}
}
}
println!("conformance: {passed}/{total} vectors, {skipped} skipped");
assert!(
failing_files.is_empty(),
"{} file(s) failed: {}",
failing_files.len(),
failing_files.join(", ")
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_container_parser_rejects_a_truncated_file() {
assert!(parse(&[]).is_err());
assert!(parse(&[1, 0, 0, 0]).is_err());
assert_eq!(parse(&[0, 0, 0, 0]).map(|v| v.len()), Ok(0));
}
#[test]
fn the_segment_map_the_runner_mirrors_is_the_cores() {
assert_eq!(phys(0x8123_4567), 0x0123_4567);
assert_eq!(phys(0xa123_4567), 0x0123_4567);
assert_eq!(phys(0x0123_4567), 0x0123_4567);
assert_eq!(phys(0xc123_4567), 0xc123_4567);
}
}