use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, UnassignedPolicy,
};
use crate::core::sync::{self, LockRank};
use super::{Arm, Config, Mode, Regs, psr};
pub(super) const SPSR_ORDER: [usize; 5] = [
0, 2, 3, 1, 4, ];
#[derive(Debug, Clone, PartialEq)]
enum Json {
Null,
Bool(bool),
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) -> Option<i64> {
match self {
Json::Num(n) => Some(*n),
Json::Bool(b) => Some(i64::from(*b)),
_ => None,
}
}
fn arr(&self) -> Option<&[Json]> {
match self {
Json::Arr(items) => Some(items),
_ => None,
}
}
}
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 peek(&mut self) -> u8 {
self.skip_space();
self.bytes.get(self.at).copied().unwrap_or(b'\0')
}
fn eat(&mut self, byte: u8) -> Result<(), String> {
if self.peek() != byte {
return Err(format!("expected `{}` at offset {}", byte as char, self.at));
}
self.at += 1;
Ok(())
}
fn value(&mut self) -> Result<Json, String> {
match self.peek() {
b'{' => {
self.at += 1;
let mut fields = Vec::new();
if self.peek() == b'}' {
self.at += 1;
return Ok(Json::Obj(fields));
}
loop {
let key = self.string()?;
self.eat(b':')?;
fields.push((key, self.value()?));
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b'}')?;
Ok(Json::Obj(fields))
}
b'[' => {
self.at += 1;
let mut items = Vec::new();
if self.peek() == b']' {
self.at += 1;
return Ok(Json::Arr(items));
}
loop {
items.push(self.value()?);
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b']')?;
Ok(Json::Arr(items))
}
b'"' => Ok(Json::Str(self.string()?)),
b't' => {
self.at += 4;
Ok(Json::Bool(true))
}
b'f' => {
self.at += 5;
Ok(Json::Bool(false))
}
b'n' => {
self.at += 4;
Ok(Json::Null)
}
_ => {
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;
}
if start == self.at {
return Err(format!("not a value at offset {start}"));
}
let text = core::str::from_utf8(&self.bytes[start..self.at])
.map_err(|_| "non-ascii number".to_string())?;
text.parse()
.map(Json::Num)
.map_err(|_| format!("integer out of range at offset {start}"))
}
}
}
fn string(&mut self) -> Result<String, String> {
self.eat(b'"')?;
let start = self.at;
while self.bytes.get(self.at).is_some_and(|b| *b != b'"') {
self.at += 1;
}
let text = String::from_utf8_lossy(&self.bytes[start..self.at]).into_owned();
self.at += 1;
Ok(text)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CpuState {
r: [u32; 16],
r_fiq: [u32; 7],
r_svc: [u32; 2],
r_abt: [u32; 2],
r_irq: [u32; 2],
r_und: [u32; 2],
cpsr: u32,
spsr: [u32; 5],
}
fn words<const N: usize>(json: &Json, key: &str) -> Result<[u32; N], String> {
let items = json
.get(key)
.ok_or_else(|| format!("missing key `{key}`"))?
.arr()
.ok_or_else(|| format!("key `{key}` is not an array"))?;
if items.len() != N {
return Err(format!(
"key `{key}` has {} entries, expected {N}",
items.len()
));
}
let mut out = [0u32; N];
for (slot, item) in out.iter_mut().zip(items) {
*slot = item
.num()
.ok_or_else(|| format!("key `{key}` holds a non-number"))? as u32;
}
Ok(out)
}
fn word(json: &Json, key: &str) -> Result<u32, String> {
Ok(json
.get(key)
.ok_or_else(|| format!("missing key `{key}`"))?
.num()
.ok_or_else(|| format!("key `{key}` is not a number"))? as u32)
}
impl CpuState {
fn from_json(json: &Json) -> Result<CpuState, String> {
Ok(CpuState {
r: words::<16>(json, "R")?,
r_fiq: words::<7>(json, "R_fiq")?,
r_svc: words::<2>(json, "R_svc")?,
r_abt: words::<2>(json, "R_abt")?,
r_irq: words::<2>(json, "R_irq")?,
r_und: words::<2>(json, "R_und")?,
cpsr: word(json, "CPSR")?,
spsr: words::<5>(json, "SPSR")?,
})
}
fn to_regs(self) -> Regs {
let mut regs = Regs::new();
regs.cpsr = u32::from(Mode::SYSTEM.0);
regs.r = self.r;
regs.r[15] = pc_of(&self);
regs.banked_r8_r12[1].copy_from_slice(&self.r_fiq[..5]);
regs.banked_sp_lr[1] = [self.r_fiq[5], self.r_fiq[6]];
regs.banked_sp_lr[2] = self.r_irq;
regs.banked_sp_lr[3] = self.r_svc;
regs.banked_sp_lr[4] = self.r_abt;
regs.banked_sp_lr[5] = self.r_und;
for (corpus, ours) in SPSR_ORDER.iter().enumerate() {
regs.spsr[*ours] = self.spsr[corpus];
}
regs.write_cpsr(self.cpsr);
regs
}
fn from_regs(regs: &Regs) -> CpuState {
let mut spsr = [0u32; 5];
for (corpus, ours) in SPSR_ORDER.iter().enumerate() {
spsr[corpus] = regs.spsr[*ours];
}
let cpsr = regs.cpsr;
let pipelined = regs.r[15].wrapping_add(if cpsr & psr::T != 0 { 4 } else { 8 });
let mut flat = *regs;
flat.set_mode(Mode::SYSTEM);
let mut r = flat.r;
r[15] = pipelined;
let mut r_fiq = [0u32; 7];
r_fiq[..5].copy_from_slice(&flat.banked_r8_r12[1]);
r_fiq[5] = flat.banked_sp_lr[1][0];
r_fiq[6] = flat.banked_sp_lr[1][1];
CpuState {
r,
r_fiq,
r_svc: flat.banked_sp_lr[3],
r_abt: flat.banked_sp_lr[4],
r_irq: flat.banked_sp_lr[2],
r_und: flat.banked_sp_lr[5],
cpsr,
spsr,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Transaction {
write: bool,
fetch: bool,
size: u32,
addr: u32,
data: u32,
}
impl Transaction {
const FETCH: u32 = 0;
const WRITE: u32 = 2;
fn from_json(json: &Json) -> Result<Transaction, String> {
let kind = word(json, "kind")?;
Ok(Transaction {
write: kind == Transaction::WRITE,
fetch: kind == Transaction::FETCH,
size: word(json, "size")?,
addr: word(json, "addr")?,
data: word(json, "data")?,
})
}
}
#[derive(Debug, Default)]
struct VectorMemory {
bytes: BTreeMap<u32, u8>,
writes: Vec<(u32, u32, u32)>,
trace: Option<Vec<(bool, u32, u32, u32)>>,
inject: Option<(u32, u32)>,
}
#[derive(Debug)]
struct VectorBus(sync::Mutex<VectorMemory>);
impl MemOps for VectorBus {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let mut m = self.0.lock();
let mut value = 0u32;
if let Some((at, opcode)) = m.inject
&& at == offset as u32
{
m.inject = None;
for (i, slot) in dst.iter_mut().enumerate() {
*slot = (opcode >> (8 * i)) as u8;
value |= u32::from(*slot) << (8 * i);
}
if let Some(trace) = m.trace.as_mut() {
trace.push((false, offset as u32, dst.len() as u32, value));
}
return Ok(());
}
for (i, slot) in dst.iter_mut().enumerate() {
let addr = (offset as u32).wrapping_add(i as u32);
*slot = m.bytes.get(&addr).copied().unwrap_or(0);
if i < 4 {
value |= u32::from(*slot) << (8 * i);
}
}
if let Some(trace) = m.trace.as_mut() {
trace.push((false, offset as u32, dst.len() as u32, value));
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
let mut m = self.0.lock();
let mut value = 0u32;
for (i, byte) in src.iter().enumerate() {
let addr = (offset as u32).wrapping_add(i as u32);
m.bytes.insert(addr, *byte);
if i < 4 {
value |= u32::from(*byte) << (8 * i);
}
}
let addr = offset as u32;
let size = src.len() as u32;
m.writes.push((addr, size, value));
if let Some(trace) = m.trace.as_mut() {
trace.push((true, addr, size, value));
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::ANY
}
}
#[derive(Debug)]
struct Failure {
index: usize,
divergence: Option<&'static str>,
signature: String,
detail: String,
}
fn tracing(index: usize) -> bool {
std::env::var("RSEMU_ARM7TDMI_TRACE")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.is_some_and(|want| want == index)
}
fn run_vector(vector: &Json, index: usize) -> Result<Option<Failure>, String> {
let initial = CpuState::from_json(
vector
.get("initial")
.ok_or_else(|| "missing key `initial`".to_string())?,
)?;
let expected = CpuState::from_json(
vector
.get("final")
.ok_or_else(|| "missing key `final`".to_string())?,
)?;
let mut memory = VectorMemory::default();
if let Some(opcode) = vector.get("opcode").and_then(Json::num) {
let width = if initial.cpsr & psr::T != 0 { 2 } else { 4 };
let pc = pc_of(&initial) & !(width - 1);
memory.inject = Some((pc, opcode as u32));
}
let mut expected_writes = Vec::new();
if let Some(items) = vector.get("transactions").and_then(Json::arr) {
for item in items {
let t = Transaction::from_json(item)?;
if t.write {
expected_writes.push((align(t.addr, t.size), t.size, t.data & size_mask(t.size)));
} else {
if t.fetch {
continue;
}
let base = align(t.addr, t.size);
for i in 0..t.size {
let byte = (t.data >> (8 * i)) as u8;
memory.bytes.insert(base.wrapping_add(i), byte);
}
}
}
}
let bus = alloc::sync::Arc::new(VectorBus(sync::Mutex::with_rank(LockRank::DEVICE, memory)));
let space = AddressSpace::new("cpu", 32).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::io("ram", 1 << 32, bus.clone()), 0)
.map_err(|e| e.to_string())?;
let cpu = Arm::new(Config::ARM7TDMI);
cpu.attach_space(alloc::sync::Arc::new(space));
cpu.set_regs(initial.to_regs());
{
let mut session = cpu.session.lock();
session.state.reset_pending = false;
}
let trace = tracing(index);
{
let mut m = bus.0.lock();
m.writes.clear();
if trace {
m.trace = Some(Vec::new());
}
}
if trace {
println!("vector {index}: initial {}", cpu.regs());
}
cpu.step();
if trace {
for (write, addr, size, value) in bus.0.lock().trace.take().unwrap_or_default() {
println!(
" {} {addr:08x} size {size} data {value:08x}",
if write { "write" } else { "read " }
);
}
println!("vector {index}: final {}", cpu.regs());
}
let mut detail = String::new();
let mut signature = Vec::new();
let got = CpuState::from_regs(&cpu.regs());
if got.r != expected.r {
for (i, (a, b)) in got.r.iter().zip(expected.r.iter()).enumerate() {
if a != b {
detail.push_str(&format!(" r{i}: want {b:08x} got {a:08x}\n"));
signature.push(format!("r{i}"));
}
}
}
if got.cpsr != expected.cpsr {
detail.push_str(&format!(
" cpsr: want {:08x} got {:08x}\n",
expected.cpsr, got.cpsr
));
signature.push("cpsr".to_string());
}
if got.spsr != expected.spsr {
detail.push_str(&format!(
" spsr: want {:08x?} got {:08x?}\n",
expected.spsr, got.spsr
));
signature.push("spsr".to_string());
}
for (name, a, b) in [
("fiq", &got.r_fiq[..], &expected.r_fiq[..]),
("svc", &got.r_svc[..], &expected.r_svc[..]),
("abt", &got.r_abt[..], &expected.r_abt[..]),
("irq", &got.r_irq[..], &expected.r_irq[..]),
("und", &got.r_und[..], &expected.r_und[..]),
] {
if a != b {
detail.push_str(&format!(" {name} bank: want {b:08x?} got {a:08x?}\n"));
signature.push(format!("{name} bank"));
}
}
let writes = core::mem::take(&mut bus.0.lock().writes);
for want in &expected_writes {
if !writes
.iter()
.any(|(addr, size, data)| (*addr, *size, *data & size_mask(*size)) == *want)
{
detail.push_str(&format!(
" missing write: {:08x} size {} data {:08x}\n",
want.0, want.1, want.2
));
signature.push("write".to_string());
}
}
let opcode = vector.get("opcode").and_then(Json::num).unwrap_or(0) as u32;
let signature = signature.join("+");
let divergence =
architectural_divergence(opcode, initial.cpsr & psr::T != 0, initial.cpsr, &signature);
Ok((!detail.is_empty()).then_some(Failure {
index,
divergence,
signature,
detail,
}))
}
fn align(addr: u32, size: u32) -> u32 {
addr & !(size.saturating_sub(1))
}
fn size_mask(size: u32) -> u32 {
match size {
1 => 0xff,
2 => 0xffff,
_ => u32::MAX,
}
}
fn pc_of(state: &CpuState) -> u32 {
let ahead = if state.cpsr & psr::T != 0 { 4 } else { 8 };
state.r[15].wrapping_sub(ahead)
}
pub(super) const REJECTED_FILES: &[(&str, &str)] = &[(
"thumb_undefined_bcc",
"every vector in the file is wrong. ARM ARM A6.1 and A7.1.14: a Thumb \
conditional branch with cond == 0b1110 is UNDEFINED (0b1111 is SWI), and \
the architecture reserves it precisely so the encoding can be trapped. \
The corpus treats cond 0b1110 as an unconditional branch. Reported \
upstream as SingleStepTests/ARM7TDMI#2, and against the emulator the \
vectors were generated from as nba-emu/NanoBoyAdvance#395. This core \
raises the Undefined Instruction exception; see \
`tests::an_undefined_thumb_encoding_is_still_undefined`.",
)];
fn rejection_reason(name: &str) -> &'static str {
REJECTED_FILES
.iter()
.find(|(f, _)| *f == name)
.map_or("", |(_, why)| *why)
}
fn architectural_divergence(
opcode: u32,
thumb: bool,
cpsr: u32,
signature: &str,
) -> Option<&'static str> {
if thumb {
return match super::thumb::decode(opcode as u16) {
super::thumb::Thumb::Alu {
op: super::thumb::AluOp::Mul,
..
} if signature == "cpsr" => Some("ARMv4 corrupts C on a Thumb MUL; ARMv5 preserves it"),
super::thumb::Thumb::BranchExchange { link: true, .. }
if link_register_only(signature) =>
{
Some("Thumb BLX Rm is an ARMv5T addition; ARMv4T has no link here")
}
super::thumb::Thumb::PushPop {
load: true,
extra,
list,
} if (extra || list == 0) && interworking_shaped(signature) => {
Some("ARMv5 POP/LDM into R15 interworks on bit 0; ARMv4T does not")
}
super::thumb::Thumb::BlockTransfer {
load: true,
list: 0,
..
} if interworking_shaped(signature) => {
Some("ARMv5 POP/LDM into R15 interworks on bit 0; ARMv4T does not")
}
_ => None,
};
}
let decoded = super::isa::decode(opcode);
if !decoded.passes(cpsr) {
return None;
}
let has_spsr = Mode((cpsr & psr::MODE) as u8).spsr_index().is_some();
match decoded.insn {
super::isa::Insn::DataProc { op, s, rd, .. }
if s && rd & 0xf == 15 && !op.writes_result() && has_spsr && signature == "cpsr" =>
{
Some(
"ARMv4 TSTP/TEQP/CMPP/CMNP: the corpus restores CPSR from SPSR; \
ARMv5 removed the form and this core sets the flags",
)
}
super::isa::Insn::LoadStore { load: true, rd, .. }
if rd & 0xf == 15 && interworking_shaped(signature) =>
{
Some("ARMv5 LDR into R15 interworks on bit 0; ARMv4T does not")
}
super::isa::Insn::BlockTransfer {
load: true, list, ..
} if list & 0x8000 != 0 && interworking_shaped(signature) => {
Some("ARMv5 LDM with R15 in the list interworks on bit 0; ARMv4T does not")
}
super::isa::Insn::LoadStore { rn, index, .. }
| super::isa::Insn::LoadStoreExtra { rn, index, .. }
if rn & 0xf == 15 && index.writes_base() && signature == "r15" =>
{
Some(
"LDR/STR with Rn == R15 and writeback is UNPREDICTABLE; the reference's R15 bookkeeping for it disagrees with its own handling of every other write to R15",
)
}
super::isa::Insn::Msr {
spsr: false, mask, ..
} if mask & 1 != 0 && signature == "r15" => Some(
"MSR setting the T bit: the reference leaves R15 ARM-pipelined while CPSR says Thumb, unlike its own BX",
),
super::isa::Insn::Mul { s: true, .. } | super::isa::Insn::MulLong { s: true, .. }
if signature == "cpsr" =>
{
Some("ARMv4 corrupts C on a flag-setting multiply; ARMv5 preserves it")
}
_ => None,
}
}
fn link_register_only(signature: &str) -> bool {
signature
.split('+')
.all(|part| part == "r14" || part.ends_with(" bank"))
}
fn interworking_shaped(signature: &str) -> bool {
signature
.split('+')
.all(|part| part == "cpsr" || part == "r15")
}
fn json_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
out.extend(json_files(&path));
} else if path.extension().is_some_and(|e| e == "json") {
out.push(path);
}
}
out.sort();
out
}
#[test]
fn single_step_tests() {
let Ok(dir) = std::env::var("RSEMU_ARM7TDMI_DIR") else {
println!(
"conformance: set RSEMU_ARM7TDMI_DIR to an unpacked \
SingleStepTests/ARM7TDMI directory to run it. Note that the corpus \
is ARMv4T and validates none of this core's ARMv5 additions — see \
this module's documentation."
);
return;
};
let dir = Path::new(&dir);
let files = json_files(dir);
assert!(!files.is_empty(), "no .json files under {}", dir.display());
let only = std::env::var("RSEMU_ARM7TDMI_FILES").ok();
let mut ran_files = 0usize;
let mut ran_vectors = 0usize;
let mut total_failures = 0usize;
let mut failing_files: Vec<(String, usize)> = Vec::new();
let mut passed_files: Vec<(String, usize, usize, usize)> = Vec::new();
for path in &files {
let name = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
if let Some(only) = &only
&& !only.split(',').any(|f| f.trim() == name)
{
continue;
}
if REJECTED_FILES.iter().any(|(f, _)| *f == name) {
println!("{name}: skipped — {}", rejection_reason(&name));
continue;
}
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let parsed = Parser::new(&bytes)
.value()
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let vectors = parsed
.arr()
.unwrap_or_else(|| panic!("{}: the file is not an array of vectors", path.display()));
ran_files += 1;
let mut failures = Vec::new();
let mut excused: BTreeMap<&'static str, usize> = BTreeMap::new();
for (index, vector) in vectors.iter().enumerate() {
ran_vectors += 1;
match run_vector(vector, index) {
Ok(None) => {}
Ok(Some(failure)) => match failure.divergence {
Some(why) => *excused.entry(why).or_default() += 1,
None => failures.push(failure),
},
Err(e) => panic!(
"{}: vector {index} does not match the expected schema: {e}\n\
See this module's documentation for the shape it reads.",
path.display()
),
}
}
let excused_total: usize = excused.values().sum();
passed_files.push((
name.clone(),
vectors.len() - failures.len() - excused_total,
excused_total,
vectors.len(),
));
for (why, count) in &excused {
println!("{name}: {count} vectors excused — {why}");
}
if failures.is_empty() {
println!(
"{name}: {} of {} passed{}",
vectors.len() - excused_total,
vectors.len() - excused_total,
if excused_total == 0 {
String::new()
} else {
format!(" ({excused_total} excused)")
}
);
} else {
total_failures += failures.len();
failing_files.push((name.clone(), failures.len()));
println!(
"{name}: {} of {} passed ({excused_total} excused); {} failed",
vectors.len() - failures.len() - excused_total,
vectors.len() - excused_total,
failures.len(),
);
let mut classes: BTreeMap<&str, (usize, &Failure)> = BTreeMap::new();
for failure in &failures {
let entry = classes
.entry(failure.signature.as_str())
.or_insert((0, failure));
entry.0 += 1;
}
for (signature, (count, example)) in classes {
println!(" [{signature}] x{count}, e.g. #{}:", example.index);
print!("{}", example.detail);
}
}
}
println!("conformance: {ran_files} files, {ran_vectors} vectors, {total_failures} failures");
for (name, passed, excused, total) in &passed_files {
println!(
" {name}: {passed}/{} passed, {excused} excused, {total} in file",
total - excused
);
}
assert!(failing_files.is_empty(), "failing files: {failing_files:?}");
}
#[test]
fn schema_round_trips_through_the_runner() {
let json = br#"[{
"initial": {
"R": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4104],
"R_fiq": [0,0,0,0,0,0,0], "R_svc": [0,0], "R_abt": [0,0],
"R_irq": [0,0], "R_und": [0,0],
"CPSR": 31, "SPSR": [0,0,0,0,0]
},
"final": {
"R": [66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4108],
"R_fiq": [0,0,0,0,0,0,0], "R_svc": [0,0], "R_abt": [0,0],
"R_irq": [0,0], "R_und": [0,0],
"CPSR": 31, "SPSR": [0,0,0,0,0]
},
"opcode": 3818913858,
"transactions": []
}]"#;
let parsed = Parser::new(json).value().expect("the schema parses");
let vectors = parsed.arr().expect("an array of vectors");
let outcome = run_vector(&vectors[0], 0).expect("the schema is understood");
assert!(outcome.is_none(), "{outcome:?}");
}
#[test]
fn a_missing_key_is_reported_rather_than_ignored() {
let json = br#"[{"initial": {"R": [0]}, "final": {}}]"#;
let parsed = Parser::new(json).value().unwrap();
let vectors = parsed.arr().unwrap();
let err = run_vector(&vectors[0], 0).unwrap_err();
assert!(err.contains("R"), "{err}");
}