use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use crate::core::device::{Device, ResetKind};
use crate::core::space::{AddressSpace, MemAttrs, RamStore, Region, UnassignedPolicy};
use crate::core::value::Width;
use crate::cpu::arm::aprofile::{self, Arm, Mode, psr, thumb};
use super::isa::{self, Insn};
use super::{ArmV7m, Config, Regs, xpsr};
const FAILURE_CAP: usize = 40;
const RAM_SIZE: u64 = 0x2000;
const CODE: u32 = 0x0100;
const DATA: u32 = 0x0400;
const STACK: u32 = 0x0c00;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Why {
NewInV7m,
Exception,
Interworking,
BaseInList,
EmptyList,
Wide,
}
impl Why {
const fn name(self) -> &'static str {
match self {
Why::NewInV7m => "new in ARMv7-M",
Why::Exception => "a different exception model",
Why::Interworking => "interworking to a non-Thumb target",
Why::BaseInList => "STM with the base in the list",
Why::EmptyList => "an empty register list",
Why::Wide => "a thirty-two-bit encoding in ARMv7-M",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verdict {
Same,
Diverge(Why),
}
fn classify(raw: u16) -> Verdict {
if isa::is_32bit(raw) {
return Verdict::Diverge(Why::Wide);
}
let v5 = thumb::decode(raw);
let v7 = isa::decode_16(raw);
if matches!(
v7,
Insn::Undefined | Insn::Udf { .. } | Insn::Bkpt { .. } | Insn::Svc { .. }
) {
return Verdict::Diverge(Why::Exception);
}
if matches!(
v5,
thumb::Thumb::Undefined | thumb::Thumb::Swi { .. } | thumb::Thumb::Bkpt { .. }
) {
return Verdict::Diverge(Why::NewInV7m);
}
match v7 {
Insn::Bx { rm } | Insn::Blx { rm } if rm >= 13 => Verdict::Diverge(Why::Interworking),
Insn::LoadStoreMultiple { list: 0, .. } => Verdict::Diverge(Why::EmptyList),
Insn::LoadStoreMultiple {
load: false,
rn,
list,
..
} if list & (1 << rn) != 0 && list.trailing_zeros() != u32::from(rn) => {
Verdict::Diverge(Why::BaseInList)
}
_ => Verdict::Same,
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn next_u32(&mut self) -> u32 {
self.next() as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Pointers,
Arithmetic,
}
#[derive(Debug, Clone, Copy)]
struct Setup {
r: [u32; 16],
flags: u32,
}
impl Setup {
fn build(rng: &mut Rng, shape: Shape) -> Setup {
let mut r = [0u32; 16];
for slot in r.iter_mut().take(13) {
*slot = match shape {
Shape::Pointers => DATA + ((rng.next_u32() & 0x1ff) * 4),
Shape::Arithmetic => rng.next_u32(),
};
}
r[13] = STACK + ((rng.next_u32() & 0x3f) * 4);
r[14] = (rng.next_u32() & 0x1fff) | 1;
r[15] = CODE;
Setup {
r,
flags: rng.next_u32() & (xpsr::N | xpsr::Z | xpsr::C | xpsr::V | xpsr::Q),
}
}
}
struct Harness {
v5: Arm,
v5_ram: Arc<RamStore>,
v7: ArmV7m,
v7_ram: Arc<RamStore>,
image: Vec<u8>,
}
impl Harness {
fn new(seed: u64) -> Harness {
let mut rng = Rng(seed);
let mut image = vec![0u8; RAM_SIZE as usize];
for chunk in image.as_chunks_mut::<4>().0 {
let word = rng.next_u32() | 1;
chunk.copy_from_slice(&word.to_le_bytes());
}
image[0..4].copy_from_slice(&STACK.to_le_bytes());
image[4..8].copy_from_slice(&(CODE | 1).to_le_bytes());
for slot in image[8..0x100].iter_mut() {
*slot = 0;
}
let build = || {
let ram = Arc::new(RamStore::new(RAM_SIZE));
ram.write_at(0, &image).expect("in range");
let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), 0)
.expect("RAM fits");
(ram, Arc::new(space))
};
let (v5_ram, v5_space) = build();
let (v7_ram, v7_space) = build();
let v5 = Arm::new(aprofile::Config::ARM926EJS);
v5.attach_space(v5_space);
let v7 = ArmV7m::new(Config::CORTEX_M4);
v7.attach_space(v7_space);
Harness {
v5,
v5_ram,
v7,
v7_ram,
image,
}
}
fn arm_case(&self, setup: &Setup, raw: u16) {
Device::reset(&self.v5, ResetKind::Cold);
Device::reset(&self.v7, ResetKind::Cold);
for ram in [&self.v5_ram, &self.v7_ram] {
ram.write_at(0, &self.image).expect("in range");
ram.write_at(u64::from(CODE), &raw.to_le_bytes())
.expect("in range");
ram.write_at(u64::from(CODE) + 2, &0xbf00u16.to_le_bytes())
.expect("in range");
}
self.v5.step();
self.v7.step();
let mut v5regs = aprofile::Regs::new();
v5regs.r = setup.r;
v5regs.cpsr = u32::from(Mode::SYSTEM.0) | psr::T | setup.flags;
self.v5.set_regs(v5regs);
let mut v7regs = Regs::new();
v7regs.r = setup.r;
v7regs.msp = setup.r[13];
v7regs.xpsr = xpsr::T | setup.flags;
self.v7.set_regs(v7regs);
}
fn state(&self) -> ([u32; 16], u32) {
let regs = self.v7.regs();
(regs.r, regs.xpsr & xpsr::FLAGS)
}
fn v5_state(&self) -> ([u32; 16], u32) {
let regs = self.v5.regs();
(regs.r, regs.cpsr & xpsr::FLAGS)
}
fn ram_differs(&self) -> Option<u64> {
let mut a = [0u8; RAM_SIZE as usize];
let mut b = [0u8; RAM_SIZE as usize];
self.v5_ram.read_at(0, &mut a).expect("in range");
self.v7_ram.read_at(0, &mut b).expect("in range");
(0..RAM_SIZE).find(|&i| a[i as usize] != b[i as usize])
}
}
fn make_target_thumb(setup: &mut Setup, raw: u16) {
if let Insn::Bx { rm } | Insn::Blx { rm } = isa::decode_16(raw)
&& rm < 13
{
setup.r[rm as usize] |= 1;
}
}
fn run_case(h: &Harness, raw: u16, setup: &Setup, verdict: Verdict) -> Option<String> {
h.arm_case(setup, raw);
h.v5.step();
h.v7.step();
let (r5, f5) = h.v5_state();
let (r7, f7) = h.state();
let same_regs = r5 == r7 && f5 == f7;
let same_ram = h.ram_differs().is_none();
match verdict {
Verdict::Same => {
if same_regs && same_ram {
return None;
}
let mut detail = String::new();
for i in 0..16 {
if r5[i] != r7[i] {
detail.push_str(&format!(" r{i}: v5={:08x} v7m={:08x}", r5[i], r7[i]));
}
}
if f5 != f7 {
detail.push_str(&format!(" flags: v5={f5:08x} v7m={f7:08x}"));
}
if let Some(at) = h.ram_differs() {
detail.push_str(&format!(" ram differs first at {at:#06x}"));
}
Some(format!(
"{raw:04x} {} / {}:{detail}",
thumb::decode(raw),
isa::decode_16(raw)
))
}
Verdict::Diverge(why) => check_divergence(h, raw, why, same_regs && same_ram),
}
}
fn check_divergence(h: &Harness, raw: u16, why: Why, identical: bool) -> Option<String> {
let v7 = h.v7.regs();
let v5 = h.v5.regs();
let fail = |what: &str| Some(format!("{raw:04x} [{}]: {what}", why.name()));
match why {
Why::NewInV7m => {
if v5.r[15] != 0x04 {
return fail("ARMv5 did not take the undefined-instruction vector");
}
if v7.in_handler() {
return fail("ARMv7-M faulted on an encoding it defines");
}
None
}
Why::Exception => {
if !v7.in_handler() {
return fail("ARMv7-M did not take an exception");
}
if v7.msp > STACK + 0x100 {
return fail("ARMv7-M did not push an exception frame");
}
None
}
Why::Interworking | Why::BaseInList | Why::EmptyList | Why::Wide => {
if identical {
return None;
}
None
}
}
}
#[test]
fn thumb1_matches_the_aprofile_core() {
let stride: u32 = option_env!("RSEMU_V7M_DIFF_STRIDE")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let h = Harness::new(0x5eed_1234_abcd_0001);
let mut rng = Rng(0xc0ff_ee00_1234_5678);
let mut compared = 0usize;
let mut diverged = [0usize; 6];
let mut failures: Vec<String> = Vec::new();
for shape in [Shape::Pointers, Shape::Arithmetic] {
let mut raw = 0u32;
while raw < 0x1_0000 {
let encoding = raw as u16;
raw += stride;
let verdict = classify(encoding);
if shape == Shape::Arithmetic
&& (isa::is_32bit(encoding) || touches_memory(isa::decode_16(encoding)))
{
continue;
}
let mut setup = Setup::build(&mut rng, shape);
make_target_thumb(&mut setup, encoding);
match verdict {
Verdict::Same => compared += 1,
Verdict::Diverge(why) => diverged[why as usize] += 1,
}
if let Some(failure) = run_case(&h, encoding, &setup, verdict)
&& failures.len() < FAILURE_CAP
{
failures.push(failure);
}
}
}
for failure in &failures {
std::println!("FAIL {failure}");
}
std::println!(
"differential vs cpu::arm::aprofile: {compared} encodings compared, \
{} classified as divergent ({} new in v7-M, {} exception model, \
{} interworking, {} STM base-in-list, {} empty list, \
{} thirty-two-bit in v7-M)",
diverged.iter().sum::<usize>(),
diverged[Why::NewInV7m as usize],
diverged[Why::Exception as usize],
diverged[Why::Interworking as usize],
diverged[Why::BaseInList as usize],
diverged[Why::EmptyList as usize],
diverged[Why::Wide as usize],
);
assert!(
failures.is_empty(),
"{} differential failures",
failures.len()
);
}
fn touches_memory(insn: Insn) -> bool {
matches!(
insn,
Insn::LoadStore { .. }
| Insn::LoadLiteral { .. }
| Insn::LoadStoreDual { .. }
| Insn::LoadStoreExclusive { .. }
| Insn::LoadStoreMultiple { .. }
| Insn::TableBranch { .. }
)
}
#[test]
fn the_harness_actually_drives_both_cores() {
let h = Harness::new(1);
let setup = Setup {
r: {
let mut r = [0u32; 16];
r[13] = STACK;
r[15] = CODE;
r[1] = 0x1234_5678;
r
},
flags: 0,
};
h.arm_case(&setup, 0x2042);
h.v5.step();
h.v7.step();
assert_eq!(h.v5.reg(0), 0x42);
assert_eq!(h.v7.reg(0), 0x42);
assert_eq!(h.v5.pc(), CODE + 2);
assert_eq!(h.v7.pc(), CODE + 2);
assert!(h.ram_differs().is_none());
}
#[test]
fn memory_outside_ram_faults() {
let h = Harness::new(2);
let space = h.v7.space().expect("attached");
assert!(space.read(RAM_SIZE, Width::U32, MemAttrs::DEFAULT).is_err());
}