use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::error::BusError;
use crate::core::space::{AddressSpace, MemAttrs, MemResult, RamStore, Region, UnassignedPolicy};
use crate::core::value::Width;
use crate::ir::{Align, Fault, InsnStart, Interp, IrHost, MemOp, Outcome, RegSlot, verify};
use super::lift::{self, Origin, PC, Shape, x_slot};
use super::{Config, Hart};
#[cfg(feature = "jit")]
use crate::ir::AccessKind;
#[cfg(feature = "jit")]
use crate::jit::{
BlockCache, Context as TlbContext, DirtyPages, Dispatcher, Epoch, Frontend, Stop, StoreLog,
Tlb, Translation,
};
pub const BASE: u64 = 0x2000_0000;
pub const RAM_SIZE: u64 = 4 * 4096;
pub const DATA: u64 = 4096;
#[derive(Debug, Clone)]
pub struct Case {
pub cfg: Config,
pub program: Vec<u32>,
pub regs: [u64; 32],
pub shape: Shape,
}
impl Case {
#[must_use]
pub fn new(program: Vec<u32>) -> Case {
Case {
cfg: Config::rv64i(),
program,
regs: [0; 32],
shape: Shape::default(),
}
}
#[must_use]
pub fn with_shape(mut self, shape: Shape) -> Case {
self.shape = shape;
self
}
#[must_use]
pub fn with_reg(mut self, n: usize, value: u64) -> Case {
if n < 32 {
self.regs[n] = value;
}
self
}
#[must_use]
pub fn seeded(program: Vec<u32>) -> Case {
Case::new(program)
.with_reg(1, BASE + DATA + 0x101)
.with_reg(2, BASE + DATA + 0x400)
.with_reg(3, BASE + DATA + 0x800)
.with_reg(4, BASE + DATA + 0xc00)
}
#[must_use]
pub fn with_config(mut self, cfg: Config) -> Case {
self.cfg = cfg;
self
}
}
pub const SYNTH_REGS: u32 = 16;
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn synthesize(form: u32, fields: u32) -> u32 {
let rd = fields % SYNTH_REGS;
let rs1 = (fields >> 4) % SYNTH_REGS;
let rs2 = (fields >> 8) % SYNTH_REGS;
let imm12 = ((fields >> 12) & 0xfff) as i32;
let imm12 = (imm12 << 20) >> 20;
let base = 1 + (rs1 & 3);
let off = (((fields >> 12) & 0x7f) as i32) - 64;
let disp = ((((fields >> 16) & 0x3f) as i32) - 32) * 2;
const OP_IMM: u32 = 0x13;
const OP: u32 = 0x33;
const OP_IMM_32: u32 = 0x1b;
const OP_32: u32 = 0x3b;
const LOAD: u32 = 0x03;
match form % 49 {
0 => i_type(OP_IMM, 0, rd, rs1, imm12),
1 => i_type(OP_IMM, 4, rd, rs1, imm12),
2 => i_type(OP_IMM, 6, rd, rs1, imm12),
3 => i_type(OP_IMM, 7, rd, rs1, imm12),
4 => i_type(OP_IMM, 2, rd, rs1, imm12),
5 => i_type(OP_IMM, 3, rd, rs1, imm12),
6 => i_type(OP_IMM, 1, rd, rs1, imm12 & 63),
7 => i_type(OP_IMM, 5, rd, rs1, imm12 & 63),
8 => i_type(OP_IMM, 5, rd, rs1, 0x400 | (imm12 & 63)),
9 => r_type(OP, 0, 0x00, rd, rs1, rs2),
10 => r_type(OP, 0, 0x20, rd, rs1, rs2),
11 => r_type(OP, 4, 0x00, rd, rs1, rs2),
12 => r_type(OP, 6, 0x00, rd, rs1, rs2),
13 => r_type(OP, 7, 0x00, rd, rs1, rs2),
14 => r_type(OP, 2, 0x00, rd, rs1, rs2),
15 => r_type(OP, 3, 0x00, rd, rs1, rs2),
16 => r_type(OP, 1, 0x00, rd, rs1, rs2),
17 => r_type(OP, 5, 0x00, rd, rs1, rs2),
18 => r_type(OP, 5, 0x20, rd, rs1, rs2),
19 => 0x37 | (rd << 7) | ((fields << 12) & 0xffff_f000),
20 => 0x17 | (rd << 7) | ((fields << 12) & 0xffff_f000),
21 => i_type(OP_IMM_32, 0, rd, rs1, imm12),
22 => i_type(OP_IMM_32, 1, rd, rs1, imm12 & 31),
23 => i_type(OP_IMM_32, 5, rd, rs1, imm12 & 31),
24 => i_type(OP_IMM_32, 5, rd, rs1, 0x400 | (imm12 & 31)),
25 => r_type(OP_32, 0, 0x00, rd, rs1, rs2),
26 => r_type(OP_32, 0, 0x20, rd, rs1, rs2),
27 => r_type(OP_32, 1, 0x00, rd, rs1, rs2),
28 => r_type(OP_32, 5, 0x00, rd, rs1, rs2),
29 => r_type(OP_32, 5, 0x20, rd, rs1, rs2),
30 => i_type(LOAD, 0, rd, base, off),
31 => i_type(LOAD, 1, rd, base, off),
32 => i_type(LOAD, 2, rd, base, off),
33 => i_type(LOAD, 3, rd, base, off),
34 => i_type(LOAD, 4, rd, base, off),
35 => i_type(LOAD, 5, rd, base, off),
36 => i_type(LOAD, 6, rd, base, off),
37 => s_type(0, base, rs2, off),
38 => s_type(1, base, rs2, off),
39 => s_type(2, base, rs2, off),
40 => s_type(3, base, rs2, off),
41 => b_type(0, rs1, rs2, disp),
42 => b_type(1, rs1, rs2, disp),
43 => b_type(4, rs1, rs2, disp),
44 => b_type(5, rs1, rs2, disp),
45 => b_type(6, rs1, rs2, disp),
46 => b_type(7, rs1, rs2, disp),
47 => j_type(rd, disp),
_ => i_type(0x67, 0, rd, rs1, imm12),
}
}
const fn i_type(opcode: u32, funct3: u32, rd: u32, rs1: u32, imm: i32) -> u32 {
opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | (((imm as u32) & 0xfff) << 20)
}
const fn r_type(opcode: u32, funct3: u32, funct7: u32, rd: u32, rs1: u32, rs2: u32) -> u32 {
opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) | (funct7 << 25)
}
const fn s_type(funct3: u32, rs1: u32, rs2: u32, imm: i32) -> u32 {
let imm = imm as u32;
0x23 | ((imm & 0x1f) << 7)
| (funct3 << 12)
| (rs1 << 15)
| (rs2 << 20)
| (((imm >> 5) & 0x7f) << 25)
}
const fn b_type(funct3: u32, rs1: u32, rs2: u32, imm: i32) -> u32 {
let imm = imm as u32;
0x63 | (((imm >> 11) & 1) << 7)
| (((imm >> 1) & 0xf) << 8)
| (funct3 << 12)
| (rs1 << 15)
| (rs2 << 20)
| (((imm >> 5) & 0x3f) << 25)
| (((imm >> 12) & 1) << 31)
}
const fn j_type(rd: u32, imm: i32) -> u32 {
let imm = imm as u32;
0x6f | (rd << 7)
| (((imm >> 12) & 0xff) << 12)
| (((imm >> 11) & 1) << 20)
| (((imm >> 1) & 0x3ff) << 21)
| (((imm >> 20) & 1) << 31)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Nothing,
Trapped {
insns: usize,
},
Agreed {
insns: usize,
ticks: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Divergence {
pub what: String,
pub program: String,
}
impl core::fmt::Display for Divergence {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}\n{}", self.what, self.program)
}
}
#[allow(clippy::missing_panics_doc)]
pub fn compare(case: &Case) -> Result<Verdict, Divergence> {
assert!(
case.cfg.pmp_count == 0,
"the harness compares ticks, and a PMP refusal is not one the block can know about"
);
assert!(
(case.program.len() as u64) * 4 <= DATA,
"a case's program lives in the first page"
);
let (oracle_space, oracle_ram) = machine(case);
let (subject_space, subject_ram) = machine(case);
let mut src = Words(&case.program);
let lifted = lift::lift(
&case.cfg,
Origin::Bare,
BASE,
&mut src,
lift::MAX_INSNS,
case.shape,
)
.expect("the harness builds RV64 cases only");
if lifted.insns == 0 {
return Ok(Verdict::Nothing);
}
if let Err(e) = verify(&lifted.block) {
return Err(diverged(
case,
format!(
"the frontend produced a block the verifier rejects: {e}\n{}",
lifted.block
),
));
}
let mut host = Host::new(case, subject_space);
let mut interp = Interp::new();
let outcome = interp
.run(&lifted.block, &mut host)
.map_err(|e| diverged(case, format!("the backend refused the block: {e}")))?;
let retired = interp.boundaries().saturating_sub(1) as usize;
let subject_faulted = matches!(outcome, Outcome::Fault(_));
let hart = Hart::new(case.cfg.with_reset_vector(BASE));
hart.attach_space(oracle_space);
for (n, value) in case.regs.iter().enumerate().skip(1) {
hart.set_x(n as u32, *value);
}
let want = retired + usize::from(subject_faulted);
let mut stepped = 0usize;
while stepped < want && hart.csrs().mcause == 0 {
hart.step();
stepped += 1;
}
let oracle_trapped = hart.csrs().mcause != 0;
if oracle_trapped != subject_faulted {
return Err(diverged(
case,
format!(
"the interpreter {} and the lifted block {} (outcome {outcome:?}, \
mcause {:#x}, mtval {:#x})",
if oracle_trapped {
"trapped"
} else {
"did not trap"
},
if subject_faulted {
"faulted"
} else {
"did not fault"
},
hart.csrs().mcause,
hart.csrs().mtval,
),
));
}
if let Outcome::Fault(fault) = &outcome {
precise_state(case, &hart, fault, &host.slots, host.ticks, "the block")?;
memory(case, &oracle_ram, &subject_ram)?;
return Ok(Verdict::Trapped { insns: retired });
}
if !matches!(outcome, Outcome::Exit) {
return Err(diverged(
case,
format!("a lifted block must end in exit_tb, but it reported {outcome:?}"),
));
}
for n in 1..32u32 {
let want = hart.x(n);
let got = host.slot(x_slot(n));
if want != got {
return Err(diverged(
case,
format!(
"x{n}: the interpreter says {want:#018x}, the lifted block says {got:#018x}"
),
));
}
}
let want_pc = hart.pc();
let got_pc = host.slot(PC);
if want_pc != got_pc {
return Err(diverged(
case,
format!(
"pc: the interpreter says {want_pc:#018x}, the lifted block says {got_pc:#018x}"
),
));
}
let want_ticks = hart.cycles();
if want_ticks != host.ticks {
return Err(diverged(
case,
format!(
"ticks: the interpreter charged {want_ticks}, the lifted block charged {}",
host.ticks
),
));
}
let column = interp
.mark()
.and_then(|m| lifted.block.marks().get(m as usize))
.expect("a block that ran reached a boundary")
.ticks;
if column + host.access_ticks != want_ticks {
return Err(diverged(
case,
format!(
"the exit boundary's tick column says {column} and the accesses spent {}, but {want_ticks} ticks were charged",
host.access_ticks
),
));
}
memory(case, &oracle_ram, &subject_ram)?;
Ok(Verdict::Agreed {
insns: retired,
ticks: want_ticks,
})
}
fn memory(case: &Case, oracle: &RamStore, subject: &RamStore) -> Result<(), Divergence> {
for off in 0..RAM_SIZE {
let want = oracle.read_u8(off).unwrap_or(0);
let got = subject.read_u8(off).unwrap_or(0);
if want != got {
return Err(diverged(
case,
format!(
"memory at {:#x}: the interpreter left {want:#04x}, the lifted block left \
{got:#04x}",
BASE + off
),
));
}
}
Ok(())
}
fn precise_state(
case: &Case,
hart: &Hart,
fault: &Fault,
slots: &[u64; lift::SLOT_COUNT as usize],
ticks: u64,
what: &str,
) -> Result<(), Divergence> {
for n in 1..32u32 {
let want = hart.x(n);
let got = slots[x_slot(n).0 as usize];
if want != got {
return Err(diverged(
case,
format!(
"x{n} at the fault: the interpreter says {want:#018x}, {what} says \
{got:#018x} ({fault:?}, mcause {:#x})",
hart.csrs().mcause,
),
));
}
}
let want_pc = hart.csrs().mepc;
if want_pc != fault.pc {
return Err(diverged(
case,
format!(
"the faulting instruction's pc: the interpreter took the trap at \
{want_pc:#018x} (mepc), {what} reported {:#018x}",
fault.pc
),
));
}
let want_ticks = hart.cycles();
if want_ticks != ticks {
return Err(diverged(
case,
format!(
"ticks at the fault: the interpreter charged {want_ticks}, {what} charged \
{ticks}. A fault mid-block must leave the cycle counter where the \
interpreter leaves it, or the state hash differs (ROADMAP.md §0)"
),
));
}
Ok(())
}
fn diverged(case: &Case, what: String) -> Divergence {
let mut program = String::new();
for (n, word) in case.program.iter().enumerate() {
let pc = BASE + n as u64 * 4;
let text = super::disasm::format_word(*word, pc, case.cfg.xlen);
program.push_str(&format!(" {pc:#010x} {word:08x} {text}\n"));
}
for (n, value) in case.regs.iter().enumerate().skip(1) {
if *value != 0 {
program.push_str(&format!(" x{n} = {value:#018x}\n"));
}
}
Divergence { what, program }
}
fn machine(case: &Case) -> (Arc<AddressSpace>, Arc<RamStore>) {
let ram = Arc::new(RamStore::new(RAM_SIZE));
for (n, word) in case.program.iter().enumerate() {
for (k, byte) in word.to_le_bytes().iter().enumerate() {
ram.write_u8(n as u64 * 4 + k as u64, *byte)
.expect("the program fits");
}
}
let space = AddressSpace::new("mem", 64).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), BASE)
.expect("one region maps");
(Arc::new(space), ram)
}
struct Words<'a>(&'a [u32]);
impl lift::InsnSource for Words<'_> {
fn halfword(&mut self, addr: u64) -> Option<u16> {
let off = addr.checked_sub(BASE)?;
let word = *self.0.get((off / 4) as usize)?;
Some(if off % 4 == 0 {
word as u16
} else {
(word >> 16) as u16
})
}
}
struct Host {
slots: [u64; lift::SLOT_COUNT as usize],
space: Arc<AddressSpace>,
attrs: MemAttrs,
misaligned: bool,
ticks: u64,
access_ticks: u64,
}
impl Host {
fn new(case: &Case, space: Arc<AddressSpace>) -> Host {
let mut slots = [0u64; lift::SLOT_COUNT as usize];
for (n, value) in case.regs.iter().enumerate().skip(1) {
slots[n] = *value;
}
Host {
slots,
space,
attrs: MemAttrs::DEFAULT.with_requester(case.cfg.requester),
misaligned: case.cfg.misaligned,
ticks: 0,
access_ticks: 0,
}
}
fn slot(&self, slot: RegSlot) -> u64 {
self.slots[slot.0 as usize]
}
fn once(&mut self, addr: u64, width: Width, value: Option<u64>) -> MemResult<u64> {
self.ticks += 1;
self.access_ticks += 1;
match value {
None => self.space.read(addr, width, self.attrs),
Some(v) => self.space.write(addr, width, v, self.attrs).map(|()| 0),
}
}
fn access(&mut self, mem: &MemOp, addr: u64, value: Option<u64>) -> MemResult<u64> {
let bytes = mem.size.bytes();
if addr.is_multiple_of(bytes) {
return self.once(addr, mem.size, value);
}
if mem.align == Align::Fault || !self.misaligned {
return Err(BusError::BadAccess);
}
match value {
None => {
let mut got = 0u64;
for i in 0..bytes {
let byte = self.once(addr.wrapping_add(i), Width::U8, None)?;
got |= (byte & 0xff) << (8 * i);
}
Ok(got)
}
Some(v) => {
for i in 0..bytes {
self.once(addr.wrapping_add(i), Width::U8, Some(v >> (8 * i)))?;
}
Ok(0)
}
}
}
}
impl IrHost for Host {
fn read_slot(&mut self, slot: RegSlot) -> u128 {
u128::from(self.slot(slot))
}
fn write_slot(&mut self, slot: RegSlot, value: u128) {
self.slots[slot.0 as usize] = value as u64;
}
fn load(&mut self, mem: &MemOp, addr: u64) -> MemResult<u64> {
self.access(mem, addr, None)
}
fn store(&mut self, mem: &MemOp, addr: u64, value: u64) -> MemResult {
self.access(mem, addr, Some(value)).map(|_| ())
}
fn charge(&mut self, ticks: u64) {
self.ticks += ticks;
}
fn insn_start(&mut self, _mark: &InsnStart) {}
}
#[cfg(feature = "jit")]
#[allow(clippy::missing_panics_doc)]
pub fn compare_cached(case: &Case, blocks: usize) -> Result<Verdict, Divergence> {
assert!(
case.cfg.pmp_count == 0,
"the harness compares ticks, and a PMP refusal is not one the block can know about"
);
assert!(
(case.program.len() as u64) * 4 <= DATA,
"a case's program lives in the first page"
);
let (oracle_space, oracle_ram) = machine(case);
let (subject_space, subject_ram) = machine(case);
let mut front = Lifter::new(case, Arc::clone(&subject_space));
let mut host = CachedHost::new(case, subject_space);
let mut disp = Dispatcher::with_cache(BlockCache::with_capacity(256));
let run = disp
.run(&mut front, &mut host, BASE, blocks)
.map_err(|e| diverged(case, format!("the dispatcher refused a block: {e}")))?;
if let Some(e) = front.rejected.take() {
return Err(diverged(case, e));
}
if let Err(e) = disp.cache().check() {
return Err(diverged(
case,
format!("the block cache is inconsistent: {e}"),
));
}
if run.insns == 0 {
return Ok(Verdict::Nothing);
}
let hart = Hart::new(case.cfg.with_reset_vector(BASE));
hart.attach_space(oracle_space);
for (n, value) in case.regs.iter().enumerate().skip(1) {
hart.set_x(n as u32, *value);
}
let subject_faulted = matches!(run.stop, Stop::Fault(_));
let want = run.insns + usize::from(subject_faulted);
let mut stepped = 0usize;
while stepped < want && hart.csrs().mcause == 0 {
hart.step();
stepped += 1;
}
let oracle_trapped = hart.csrs().mcause != 0;
if oracle_trapped != subject_faulted {
return Err(diverged(
case,
format!(
"the interpreter {} and the cached path {} (stop {:?}, mcause {:#x}, mtval {:#x})",
if oracle_trapped {
"trapped"
} else {
"did not trap"
},
if subject_faulted {
"faulted"
} else {
"did not fault"
},
run.stop,
hart.csrs().mcause,
hart.csrs().mtval,
),
));
}
if let Stop::Fault(fault) = &run.stop {
precise_state(
case,
&hart,
fault,
&host.slots,
host.ticks,
"the cached path",
)?;
memory(case, &oracle_ram, &subject_ram)?;
return Ok(Verdict::Trapped { insns: run.insns });
}
for n in 1..32u32 {
let want = hart.x(n);
let got = host.slot(x_slot(n));
if want != got {
return Err(diverged(
case,
format!(
"x{n} after {} blocks: the interpreter says {want:#018x}, the cached path \
says {got:#018x}",
run.blocks
),
));
}
}
let want_pc = hart.pc();
if want_pc != run.pc {
return Err(diverged(
case,
format!(
"pc after {} blocks: the interpreter says {want_pc:#018x}, the cached path says \
{:#018x}",
run.blocks, run.pc
),
));
}
let want_ticks = hart.cycles();
if want_ticks != host.ticks {
return Err(diverged(
case,
format!(
"ticks after {} blocks: the interpreter charged {want_ticks}, the cached path \
charged {}. A cache hit and a cache miss must be indistinguishable to the \
guest, including in cycle accounting (ROADMAP.md §0)",
run.blocks, host.ticks
),
));
}
memory(case, &oracle_ram, &subject_ram)?;
Ok(Verdict::Agreed {
insns: run.insns,
ticks: want_ticks,
})
}
#[cfg(feature = "jit")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedRun {
pub verdict: Verdict,
pub blocks: usize,
pub insns_retired: usize,
pub translated: u64,
pub chained: u64,
pub smc: u64,
pub tlb_hits: u64,
}
#[cfg(feature = "jit")]
#[allow(clippy::missing_panics_doc)]
pub fn measure_cached(case: &Case, blocks: usize) -> Result<CachedRun, Divergence> {
let verdict = compare_cached(case, blocks)?;
let (space, _ram) = machine(case);
let mut front = Lifter::new(case, Arc::clone(&space));
let mut host = CachedHost::new(case, space);
let mut disp = Dispatcher::with_cache(BlockCache::with_capacity(256));
let run = disp
.run(&mut front, &mut host, BASE, blocks)
.map_err(|e| diverged(case, format!("the dispatcher refused a block: {e}")))?;
Ok(CachedRun {
verdict,
blocks: run.blocks,
insns_retired: run.insns,
translated: disp.stats().translated,
chained: disp.stats().chained,
smc: disp.stats().smc,
tlb_hits: host.tlb.stats().hits,
})
}
#[cfg(feature = "jit")]
struct Lifter {
cfg: Config,
shape: Shape,
space: Arc<AddressSpace>,
attrs: MemAttrs,
rejected: Option<String>,
}
#[cfg(feature = "jit")]
impl Lifter {
fn new(case: &Case, space: Arc<AddressSpace>) -> Lifter {
Lifter {
cfg: case.cfg,
shape: case.shape,
space,
attrs: MemAttrs::DEFAULT.with_requester(case.cfg.requester),
rejected: None,
}
}
}
#[cfg(feature = "jit")]
impl Frontend for Lifter {
fn epoch(&mut self) -> Epoch {
Epoch {
topology: self.space.generation(),
translation: 0,
}
}
fn key(&mut self) -> u64 {
lift::key(&self.cfg, Origin::Bare, self.shape)
}
fn pc_slot(&self) -> RegSlot {
PC
}
fn translate(&mut self, pc: u64) -> crate::core::error::Result<Translation> {
let space = Arc::clone(&self.space);
let attrs = self.attrs;
let mut src = |addr: u64| space.read(addr, Width::U16, attrs).ok().map(|v| v as u16);
let lifted = lift::lift(
&self.cfg,
Origin::Bare,
pc,
&mut src,
lift::MAX_INSNS,
self.shape,
)?;
if self.rejected.is_none()
&& let Err(e) = verify(&lifted.block)
{
self.rejected = Some(format!(
"the frontend produced a block the verifier rejects: {e}\n{}",
lifted.block
));
}
Ok(Translation {
page: pc & !crate::jit::PAGE_MASK,
insns: lifted.insns,
block: lifted.block,
})
}
}
#[cfg(feature = "jit")]
struct CachedHost {
slots: [u64; lift::SLOT_COUNT as usize],
tlb: Tlb,
attrs: MemAttrs,
misaligned: bool,
ticks: u64,
dirty: DirtyPages,
}
#[cfg(feature = "jit")]
const MACHINE: TlbContext = TlbContext {
level: 3,
translating: false,
};
#[cfg(feature = "jit")]
impl CachedHost {
fn new(case: &Case, space: Arc<AddressSpace>) -> CachedHost {
let mut slots = [0u64; lift::SLOT_COUNT as usize];
for (n, value) in case.regs.iter().enumerate().skip(1) {
slots[n] = *value;
}
CachedHost {
slots,
tlb: Tlb::new(space),
attrs: MemAttrs::DEFAULT.with_requester(case.cfg.requester),
misaligned: case.cfg.misaligned,
ticks: 0,
dirty: DirtyPages::new(),
}
}
fn slot(&self, slot: RegSlot) -> u64 {
self.slots[slot.0 as usize]
}
fn once(&mut self, addr: u64, width: Width, value: Option<u64>) -> MemResult<u64> {
self.ticks += 1;
match value {
None => self
.tlb
.read(AccessKind::Load, addr, addr, width, MACHINE, self.attrs),
Some(v) => {
let done = self
.tlb
.write(addr, addr, width, v, MACHINE, self.attrs)
.map(|()| 0);
if done.is_ok() {
self.dirty.note(addr, width.bytes());
}
done
}
}
}
fn access(&mut self, mem: &MemOp, addr: u64, value: Option<u64>) -> MemResult<u64> {
let bytes = mem.size.bytes();
if addr.is_multiple_of(bytes) {
return self.once(addr, mem.size, value);
}
if mem.align == Align::Fault || !self.misaligned {
return Err(BusError::BadAccess);
}
match value {
None => {
let mut got = 0u64;
for i in 0..bytes {
let byte = self.once(addr.wrapping_add(i), Width::U8, None)?;
got |= (byte & 0xff) << (8 * i);
}
Ok(got)
}
Some(v) => {
for i in 0..bytes {
self.once(addr.wrapping_add(i), Width::U8, Some(v >> (8 * i)))?;
}
Ok(0)
}
}
}
}
#[cfg(feature = "jit")]
impl IrHost for CachedHost {
fn read_slot(&mut self, slot: RegSlot) -> u128 {
u128::from(self.slot(slot))
}
fn write_slot(&mut self, slot: RegSlot, value: u128) {
self.slots[slot.0 as usize] = value as u64;
}
fn load(&mut self, mem: &MemOp, addr: u64) -> MemResult<u64> {
self.access(mem, addr, None)
}
fn store(&mut self, mem: &MemOp, addr: u64, value: u64) -> MemResult {
self.access(mem, addr, Some(value)).map(|_| ())
}
fn charge(&mut self, ticks: u64) {
self.ticks += ticks;
}
fn insn_start(&mut self, _mark: &InsnStart) {}
}
#[cfg(feature = "jit")]
impl StoreLog for CachedHost {
fn drain_dirty(&mut self, sink: &mut dyn FnMut(u64)) {
self.dirty.drain_dirty(sink);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cpu::riscv::csr::Extensions;
use alloc::vec;
const fn addi(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x13, 0, rd, rs1, imm)
}
const fn ld(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x03, 3, rd, rs1, imm)
}
const fn sd(rs1: u32, rs2: u32, imm: i32) -> u32 {
s_type(3, rs1, rs2, imm)
}
const fn beq(rs1: u32, rs2: u32, imm: i32) -> u32 {
b_type(0, rs1, rs2, imm)
}
const fn jal(rd: u32, imm: i32) -> u32 {
j_type(rd, imm)
}
const ECALL: u32 = 0x0000_0073;
fn agreed(case: &Case) -> Verdict {
match compare(case) {
Ok(v) => v,
Err(e) => panic!("{e}"),
}
}
#[test]
fn a_straight_line_program_agrees_on_every_column() {
let case = Case::new(vec![addi(5, 0, 7), addi(6, 5, 3), addi(7, 6, -1)]);
assert_eq!(agreed(&case), Verdict::Agreed { insns: 3, ticks: 6 });
}
#[test]
fn a_store_agrees_on_memory_and_a_load_reads_it_back() {
let addr = BASE + DATA;
let store = Case::new(vec![sd(1, 2, 0)])
.with_reg(1, addr)
.with_reg(2, 0x0123_4567_89ab_cdef);
assert_eq!(agreed(&store), Verdict::Agreed { insns: 1, ticks: 3 });
let load = Case::new(vec![ld(3, 1, 0)]).with_reg(1, addr);
assert_eq!(agreed(&load), Verdict::Agreed { insns: 1, ticks: 3 });
}
#[test]
fn a_misaligned_access_agrees_on_the_tick_it_costs() {
let case = Case::new(vec![ld(3, 1, 1)]).with_reg(1, BASE + DATA);
assert_eq!(
agreed(&case),
Verdict::Agreed {
insns: 1,
ticks: 10
}
);
}
#[test]
fn a_core_that_traps_misaligned_accesses_traps_in_both_engines() {
let mut strict = Config::rv64i();
strict.misaligned = false;
let case = Case::new(vec![ld(3, 1, 1)])
.with_config(strict)
.with_reg(1, BASE + DATA);
assert_eq!(compare(&case), Ok(Verdict::Trapped { insns: 0 }));
}
#[test]
fn an_access_off_the_end_of_ram_faults_in_both_engines() {
let case = Case::new(vec![ld(3, 1, 0)]).with_reg(1, BASE + RAM_SIZE + 0x1000);
assert_eq!(compare(&case), Ok(Verdict::Trapped { insns: 0 }));
}
#[test]
fn a_fault_in_the_middle_of_a_trace_reports_the_interpreters_exact_state() {
let case = Case::new(vec![
addi(2, 0, 0x11), addi(3, 2, 0x22), addi(4, 3, 0x33), jal(0, 8), addi(31, 0, -1), addi(5, 4, 0x44), addi(6, 5, 0x55), addi(7, 6, 0x66), addi(8, 7, 0x77), ld(9, 1, 0), addi(10, 0, -1), ])
.with_reg(1, BASE + RAM_SIZE + 0x4000);
assert_eq!(compare(&case), Ok(Verdict::Trapped { insns: 8 }));
#[cfg(feature = "jit")]
assert_eq!(compare_cached(&case, 4), Ok(Verdict::Trapped { insns: 8 }));
}
#[test]
fn a_side_exit_that_is_taken_leaves_with_the_registers_the_interpreter_has() {
let case = Case::new(vec![
addi(5, 0, 7), beq(0, 0, 12), addi(6, 0, 1), addi(7, 0, 2), addi(8, 0, 3), ]);
assert_eq!(compare(&case), Ok(Verdict::Agreed { insns: 2, ticks: 4 }));
}
#[test]
fn a_long_trace_charges_every_instruction_it_merged_in() {
let case = Case::new(vec![addi(10, 10, 1), jal(0, -4)]);
assert_eq!(
compare(&case),
Ok(Verdict::Agreed {
insns: 64,
ticks: 128
})
);
}
#[test]
fn a_fault_after_a_side_exit_was_not_taken_still_reports_exact_state() {
let case = Case::new(vec![
addi(2, 0, 5), beq(2, 0, 12), addi(3, 2, 7), ld(4, 1, 0), addi(5, 0, -1), ])
.with_reg(1, BASE + RAM_SIZE + 0x4000);
assert_eq!(compare(&case), Ok(Verdict::Trapped { insns: 3 }));
}
#[test]
fn every_shape_agrees_with_the_interpreter_about_the_same_program() {
let program = vec![
addi(5, 0, 3),
sd(1, 5, 0),
ld(6, 1, 0),
beq(6, 5, 8),
addi(7, 0, -1),
addi(8, 6, 1),
ECALL,
];
for shape in [Shape::BasicBlock, Shape::Extended, Shape::Trace] {
let case = Case::seeded(program.clone()).with_shape(shape);
match compare(&case) {
Ok(Verdict::Agreed { .. } | Verdict::Trapped { .. }) => {}
Ok(other) => panic!("{shape:?} produced {other:?}"),
Err(e) => panic!("{shape:?} diverged:\n{e}"),
}
}
}
#[test]
fn an_unsupported_first_instruction_is_nothing_to_compare() {
assert_eq!(compare(&Case::new(vec![0x0000_0073])), Ok(Verdict::Nothing));
}
#[test]
fn a_compressed_core_agrees_too() {
let mut cfg = Config::rv64i();
cfg.ext = Extensions {
c: true,
..Extensions::I
};
let c_addi = 0x0285u32;
let case = Case::new(vec![c_addi | (c_addi << 16)]).with_config(cfg);
assert_eq!(agreed(&case), Verdict::Agreed { insns: 2, ticks: 2 });
}
#[cfg(feature = "jit")]
mod cached {
use super::*;
use crate::jit::{BlockCache, Epoch};
fn agreed(case: &Case, blocks: usize) -> CachedRun {
match measure_cached(case, blocks) {
Ok(run) => {
assert!(
matches!(run.verdict, Verdict::Agreed { .. }),
"expected agreement, got {:?}",
run.verdict
);
run
}
Err(e) => panic!("diverged:\n{e}"),
}
}
#[test]
fn a_block_served_from_the_cache_agrees_with_the_interpreter() {
let case = Case::new(vec![addi(10, 10, 1), jal(0, -4)]);
let run = agreed(&case, 20);
assert_eq!(run.blocks, 20);
assert_eq!(run.translated, 1, "one translation served twenty times");
}
#[test]
fn a_chained_pair_agrees_with_the_interpreter() {
let case = Case::new(vec![
addi(10, 10, 1),
jal(0, 8), addi(11, 11, 2), jal(0, -4), ]);
let run = agreed(&case, 30);
assert!(
run.chained >= 25,
"chained {} of {} blocks",
run.chained,
run.blocks
);
}
#[test]
fn every_access_on_the_cached_path_goes_through_the_software_tlb() {
let case = Case::seeded(vec![sd(1, 5, 0), ld(6, 1, 0), addi(7, 6, 1)]);
let run = agreed(&case, 8);
assert!(run.tlb_hits > 0, "no access was served from an entry");
}
const fn jalr(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x67, 0, rd, rs1, imm)
}
fn indirect_loop() -> Config {
let mut cfg = Config::rv64i();
cfg.ext = Extensions {
c: true,
..Extensions::I
};
cfg
}
#[test]
fn a_store_into_the_code_page_invalidates_the_translation_of_it() {
let replacement = u64::from(addi(10, 10, 7)) | (u64::from(addi(0, 0, 0)) << 32);
let case = Case::new(vec![addi(10, 10, 1), sd(1, 11, 0), jalr(0, 12, 0)])
.with_config(indirect_loop())
.with_reg(1, BASE)
.with_reg(11, replacement)
.with_reg(12, BASE);
let run = agreed(&case, 12);
assert!(run.smc > 0, "no translation was invalidated by the store");
assert!(run.translated > 1, "the loop was never lifted again");
}
#[test]
fn a_store_in_the_middle_of_a_trace_invalidates_the_trace() {
let replacement = u64::from(addi(10, 10, 7)) | (u64::from(addi(13, 13, 1)) << 32);
let case = Case::new(vec![
addi(10, 10, 1),
addi(13, 13, 1),
sd(1, 11, 0),
addi(14, 14, 1),
jalr(0, 12, 0),
])
.with_config(indirect_loop())
.with_reg(1, BASE)
.with_reg(11, replacement)
.with_reg(12, BASE);
let run = agreed(&case, 10);
assert!(
run.smc > 0,
"the trace was not invalidated by its own store"
);
assert!(run.translated > 1, "the trace was never lifted again");
}
#[test]
fn a_store_that_misses_every_translated_page_invalidates_nothing() {
let case = Case::seeded(vec![addi(10, 10, 1), sd(2, 10, 0), jal(0, -8)]);
let run = agreed(&case, 12);
assert_eq!(run.smc, 0);
assert!(
run.translated <= 3,
"the loop was translated {} times, so the cache is being thrown away",
run.translated
);
}
#[test]
fn a_trace_merges_a_loop_into_one_block_and_still_agrees() {
let program = vec![addi(10, 10, 1), jal(0, -4)];
let basic = agreed(
&Case::new(program.clone()).with_shape(Shape::BasicBlock),
24,
);
let trace = agreed(&Case::new(program).with_shape(Shape::Trace), 24);
assert_eq!(basic.blocks, 24);
assert_eq!(trace.blocks, 24);
assert!(
trace.insns_retired > basic.insns_retired * 20,
"a trace retired {} instructions in the same block budget where basic blocks \
retired {}",
trace.insns_retired,
basic.insns_retired
);
}
#[test]
fn changing_the_page_tables_makes_the_same_virtual_address_a_different_block() {
let cfg = Config::rv64i();
let before = lift::key(&cfg, Origin::Paged { generation: 1 }, Shape::default());
let after = lift::key(&cfg, Origin::Paged { generation: 2 }, Shape::default());
assert_ne!(before, after, "the generation is in the key");
let mut cache = BlockCache::with_capacity(16);
let mut src = Words(&[addi(10, 10, 1)]);
let lifted = lift::lift(
&cfg,
Origin::Paged { generation: 1 },
BASE,
&mut src,
4,
Shape::default(),
)
.expect("rv64");
let id = cache.insert(BASE, before, BASE, lifted.insns, lifted.block);
assert_eq!(cache.lookup(BASE, before), Some(id));
assert_eq!(
cache.lookup(BASE, after),
None,
"the mapping changed, so the block at this VA must be lifted again"
);
}
#[test]
fn a_bare_block_and_a_paged_block_at_the_same_address_are_different_blocks() {
let cfg = Config::rv64i();
assert_ne!(
lift::key(&cfg, Origin::Bare, Shape::default()),
lift::key(&cfg, Origin::Paged { generation: 0 }, Shape::default()),
"a physical lift and a virtual lift of the same number must not collide"
);
}
#[test]
fn a_topology_change_invalidates_a_bare_block_that_the_key_would_not() {
let cfg = Config::rv64i();
let key = lift::key(&cfg, Origin::Bare, Shape::default());
let mut cache = BlockCache::with_capacity(16);
let mut src = Words(&[addi(10, 10, 1)]);
let lifted =
lift::lift(&cfg, Origin::Bare, BASE, &mut src, 4, Shape::default()).expect("rv64");
cache.insert(BASE, key, BASE, lifted.insns, lifted.block);
assert!(cache.lookup(BASE, key).is_some());
assert!(cache.sync(Epoch {
topology: 1,
translation: 0
}));
assert_eq!(cache.lookup(BASE, key), None);
}
#[test]
fn the_cached_path_charges_exactly_the_ticks_the_interpreter_charges() {
let case = Case::seeded(vec![sd(1, 5, 1), ld(6, 1, 1), jal(0, -8)]);
let run = agreed(&case, 10);
let Verdict::Agreed { ticks, .. } = run.verdict else {
unreachable!("agreed() asserted it")
};
assert!(ticks > 0);
}
#[test]
fn an_unsupported_instruction_hands_the_pc_back_rather_than_spinning() {
const ECALL: u32 = 0x0000_0073;
let case = Case::new(vec![addi(10, 10, 1), ECALL, addi(11, 11, 1)]);
let run = agreed(&case, 100);
assert!(run.blocks < 100, "it stopped at the ecall");
}
}
}