use alloc::vec::Vec;
use crate::core::error::{Error, Result};
use crate::core::value::Width;
use crate::ir::{
AccessKind, Align, Block, BlockBuilder, Cond, Const, Endian, InsnStart, MemOp, MemSpace,
Opcode, RegSlot, Sign, Temp, Type,
};
use super::csr::{Csrs, Priv};
use super::isa::{self, Fmt, Op, Xlen};
use super::mmu;
use super::{Config, PAGE_MASK};
#[inline]
#[must_use]
pub const fn x_slot(n: u32) -> RegSlot {
RegSlot((n & 31) as u16)
}
pub const PC: RegSlot = RegSlot(32);
pub const RESERVATION: RegSlot = RegSlot(33);
pub const SLOT_COUNT: u16 = 34;
pub trait InsnSource {
fn halfword(&mut self, addr: u64) -> Option<u16>;
}
impl<F: FnMut(u64) -> Option<u16>> InsnSource for F {
#[inline]
fn halfword(&mut self, addr: u64) -> Option<u16> {
self(addr)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Stop {
Unsupported,
Access,
Transfer,
Page,
Limit,
Unreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Shape {
BasicBlock,
Extended,
#[default]
Trace,
}
impl Shape {
#[inline]
#[must_use]
pub const fn access_ends_block(self) -> bool {
matches!(self, Shape::BasicBlock)
}
#[inline]
#[must_use]
pub const fn merges(self) -> bool {
matches!(self, Shape::Trace)
}
const fn key_bits(self) -> u64 {
match self {
Shape::BasicBlock => 0,
Shape::Extended => 1 << 3,
Shape::Trace => 2 << 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Origin {
Bare,
Paged {
generation: u64,
},
}
impl Origin {
#[must_use]
pub fn of(csrs: &Csrs, mode: Priv) -> Origin {
if mmu::translation_active(csrs, mode) {
Origin::Paged {
generation: csrs.translation_gen,
}
} else {
Origin::Bare
}
}
const fn key_bits(self) -> u64 {
match self {
Origin::Bare => 0,
Origin::Paged { generation } => (1 << 5) | generation.wrapping_shl(6),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lifted {
pub block: Block,
pub stop: Stop,
pub insns: usize,
pub origin: Origin,
}
pub const MAX_INSNS: usize = 64;
pub fn lift<S: InsnSource>(
cfg: &Config,
origin: Origin,
entry_pc: u64,
src: &mut S,
max_insns: usize,
shape: Shape,
) -> Result<Lifted> {
if !matches!(cfg.xlen, Xlen::Rv64) {
return Err(Error::Unimplemented("the RISC-V IR frontend is RV64 only"));
}
let mut lf = Lifter::new(cfg, origin, entry_pc, shape);
let page = lf.page;
let mut pc = entry_pc;
let mut insns = 0usize;
let stop = loop {
if insns >= max_insns {
break Stop::Limit;
}
if pc & !PAGE_MASK != page {
break Stop::Page;
}
let Some(low) = src.halfword(pc) else {
break Stop::Unreadable;
};
let (word, len, fetch) = if isa::is_32bit(low) {
if pc.wrapping_add(2) & !PAGE_MASK != page {
break Stop::Page;
}
let Some(high) = src.halfword(pc.wrapping_add(2)) else {
break Stop::Unreadable;
};
(u32::from(low) | (u32::from(high) << 16), 4u64, 2u64)
} else if cfg.ext.c {
match isa::expand(low, cfg.xlen) {
Some(word) => (word, 2u64, 1u64),
None => break Stop::Unsupported,
}
} else {
break Stop::Unsupported;
};
let next_pc = pc.wrapping_add(len);
match lf.insn(word, pc, next_pc, fetch) {
Flow::Rejected => break Stop::Unsupported,
Flow::Continue(next) => {
insns += 1;
pc = next;
}
Flow::Access { next, store } => {
insns += 1;
pc = next;
if store || shape.access_ends_block() {
break Stop::Access;
}
}
Flow::Transfer => {
insns += 1;
pc = next_pc;
break Stop::Transfer;
}
}
};
Ok(Lifted {
block: lf.finish(pc),
stop,
insns,
origin,
})
}
#[must_use]
pub fn key(cfg: &Config, origin: Origin, shape: Shape) -> u64 {
let mut key = 0u64;
if cfg.ext.c {
key |= 1;
}
if cfg.misaligned {
key |= 2;
}
if matches!(cfg.xlen, Xlen::Rv64) {
key |= 4;
}
key | shape.key_bits() | origin.key_bits()
}
const _: () = assert!(mmu::PAGE_SIZE == 4096);
#[derive(Debug, Clone, Copy)]
enum Plan {
Alu(Alu),
Load { size: Width, sign: Sign },
Store { size: Width },
Branch { cond: Cond, target: u64 },
Jal { target: u64 },
Jalr,
}
#[derive(Debug, Clone, Copy)]
enum Alu {
Const(u64),
RegImm { op: Opcode, imm: u64 },
SetCondImm { cond: Cond, imm: u64 },
ShiftImm { op: Opcode, shamt: u32 },
RegReg { op: Opcode },
SetCond { cond: Cond },
ShiftReg { op: Opcode },
WordImm { op: Opcode, imm: u32 },
WordShiftImm { op: Opcode, shamt: u32 },
WordReg { op: Opcode },
WordShiftReg { op: Opcode },
}
const fn reads(fmt: Fmt) -> (bool, bool) {
match fmt {
Fmt::I | Fmt::Shift | Fmt::Load => (true, false),
Fmt::R | Fmt::Store | Fmt::Branch => (true, true),
_ => (false, false),
}
}
const fn target_align_mask(cfg: &Config) -> u64 {
if cfg.ext.c { 1 } else { 3 }
}
#[allow(clippy::too_many_lines)]
fn classify(cfg: &Config, op: Op, word: u32, pc: u64) -> Option<Plan> {
let imm_i = isa::imm_i(word) as u64;
let plan = match op {
Op::Lui => Plan::Alu(Alu::Const(isa::imm_u(word) as u64)),
Op::Auipc => Plan::Alu(Alu::Const(pc.wrapping_add(isa::imm_u(word) as u64))),
Op::Addi => Plan::Alu(Alu::RegImm {
op: Opcode::ADD,
imm: imm_i,
}),
Op::Xori => Plan::Alu(Alu::RegImm {
op: Opcode::XOR,
imm: imm_i,
}),
Op::Ori => Plan::Alu(Alu::RegImm {
op: Opcode::OR,
imm: imm_i,
}),
Op::Andi => Plan::Alu(Alu::RegImm {
op: Opcode::AND,
imm: imm_i,
}),
Op::Slti => Plan::Alu(Alu::SetCondImm {
cond: Cond::LtS,
imm: imm_i,
}),
Op::Sltiu => Plan::Alu(Alu::SetCondImm {
cond: Cond::LtU,
imm: imm_i,
}),
Op::Slli | Op::Srli | Op::Srai => {
let shamt = isa::shamt(word);
if shamt >= 64 {
return None;
}
Plan::Alu(Alu::ShiftImm {
op: shift_opcode(op),
shamt,
})
}
Op::Add => Plan::Alu(Alu::RegReg { op: Opcode::ADD }),
Op::Sub => Plan::Alu(Alu::RegReg { op: Opcode::SUB }),
Op::Xor => Plan::Alu(Alu::RegReg { op: Opcode::XOR }),
Op::Or => Plan::Alu(Alu::RegReg { op: Opcode::OR }),
Op::And => Plan::Alu(Alu::RegReg { op: Opcode::AND }),
Op::Slt => Plan::Alu(Alu::SetCond { cond: Cond::LtS }),
Op::Sltu => Plan::Alu(Alu::SetCond { cond: Cond::LtU }),
Op::Sll | Op::Srl | Op::Sra => Plan::Alu(Alu::ShiftReg {
op: shift_opcode(op),
}),
Op::Addiw => Plan::Alu(Alu::WordImm {
op: Opcode::ADD,
imm: imm_i as u32,
}),
Op::Slliw | Op::Srliw | Op::Sraiw => Plan::Alu(Alu::WordShiftImm {
op: shift_opcode(op),
shamt: isa::shamt(word) & 31,
}),
Op::Addw => Plan::Alu(Alu::WordReg { op: Opcode::ADD }),
Op::Subw => Plan::Alu(Alu::WordReg { op: Opcode::SUB }),
Op::Sllw | Op::Srlw | Op::Sraw => Plan::Alu(Alu::WordShiftReg {
op: shift_opcode(op),
}),
Op::Lb => Plan::Load {
size: Width::U8,
sign: Sign::Signed,
},
Op::Lbu => Plan::Load {
size: Width::U8,
sign: Sign::Unsigned,
},
Op::Lh => Plan::Load {
size: Width::U16,
sign: Sign::Signed,
},
Op::Lhu => Plan::Load {
size: Width::U16,
sign: Sign::Unsigned,
},
Op::Lw => Plan::Load {
size: Width::U32,
sign: Sign::Signed,
},
Op::Lwu => Plan::Load {
size: Width::U32,
sign: Sign::Unsigned,
},
Op::Ld => Plan::Load {
size: Width::U64,
sign: Sign::Signed,
},
Op::Sb => Plan::Store { size: Width::U8 },
Op::Sh => Plan::Store { size: Width::U16 },
Op::Sw => Plan::Store { size: Width::U32 },
Op::Sd => Plan::Store { size: Width::U64 },
Op::Beq | Op::Bne | Op::Blt | Op::Bge | Op::Bltu | Op::Bgeu => {
let target = pc.wrapping_add(isa::imm_b(word) as u64);
if target & target_align_mask(cfg) != 0 {
return None;
}
Plan::Branch {
cond: branch_cond(op),
target,
}
}
Op::Jal => {
let target = pc.wrapping_add(isa::imm_j(word) as u64);
if target & target_align_mask(cfg) != 0 {
return None;
}
Plan::Jal { target }
}
Op::Jalr if cfg.ext.c => Plan::Jalr,
_ => return None,
};
Some(plan)
}
const fn shift_opcode(op: Op) -> Opcode {
match op {
Op::Slli | Op::Sll | Op::Slliw | Op::Sllw => Opcode::SHL,
Op::Srai | Op::Sra | Op::Sraiw | Op::Sraw => Opcode::SAR,
_ => Opcode::SHR,
}
}
const fn branch_cond(op: Op) -> Cond {
match op {
Op::Beq => Cond::Eq,
Op::Bne => Cond::Ne,
Op::Blt => Cond::LtS,
Op::Bge => Cond::GeS,
Op::Bltu => Cond::LtU,
_ => Cond::GeU,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Flow {
Rejected,
Continue(u64),
Access {
next: u64,
store: bool,
},
Transfer,
}
struct Lifter<'a> {
cfg: &'a Config,
shape: Shape,
page: u64,
b: BlockBuilder,
x: [Option<Temp>; 32],
zero: Option<Temp>,
ticks: u64,
pc_out: Option<Temp>,
static_exit: Option<u64>,
}
impl<'a> Lifter<'a> {
fn new(cfg: &'a Config, origin: Origin, entry_pc: u64, shape: Shape) -> Lifter<'a> {
Lifter {
cfg,
shape,
page: entry_pc & !PAGE_MASK,
b: BlockBuilder::new(entry_pc, key(cfg, origin, shape)),
x: [None; 32],
zero: None,
ticks: 0,
pc_out: None,
static_exit: None,
}
}
fn side_exit(&mut self, when: Cond, lhs: Temp, rhs: Temp, exit_pc: u64) {
let over = self.b.emit_raw(
Opcode::BRCOND,
Type::I64,
None,
None,
&[lhs, rhs],
None,
Some(when.invert()),
0,
);
let target = self.konst(exit_pc);
let mut live = self.live_regs();
live.push((PC, target));
self.b.insn_start(InsnStart {
pc: exit_pc,
next_pc: exit_pc,
ticks: self.ticks,
live,
});
self.b.exit_tb();
let after = self.b.next_index() as u32;
self.b.patch_aux(over, after);
}
fn konst(&mut self, value: u64) -> Temp {
self.b.imm(Type::I64, Const::Int(u128::from(value)))
}
fn konst32(&mut self, value: u32) -> Temp {
self.b.imm(Type::I32, Const::Int(u128::from(value)))
}
fn zero(&mut self) -> Temp {
match self.zero {
Some(t) => t,
None => {
let t = self.konst(0);
self.zero = Some(t);
t
}
}
}
fn read_x(&mut self, n: u32) -> Temp {
if n == 0 {
return self.zero();
}
match self.x[n as usize] {
Some(t) => t,
None => {
let t = self.b.get_slot(Type::I64, RegSlot(n as u16));
self.x[n as usize] = Some(t);
t
}
}
}
fn write_x(&mut self, n: u32, t: Temp) {
if n != 0 {
self.x[n as usize] = Some(t);
}
}
fn need(&mut self, t: Option<Temp>) -> Temp {
match t {
Some(t) => t,
None => self.zero(),
}
}
fn live_regs(&self) -> Vec<(RegSlot, Temp)> {
let mut live = Vec::new();
for (n, temp) in self.x.iter().enumerate() {
if let Some(t) = temp {
live.push((x_slot(n as u32), *t));
}
}
live
}
const fn align(&self) -> Align {
if self.cfg.misaligned {
Align::None
} else {
Align::Fault
}
}
fn insn(&mut self, word: u32, pc: u64, next_pc: u64, fetch: u64) -> Flow {
let Some(row) = isa::decode(word, self.cfg.xlen) else {
return Flow::Rejected;
};
if !matches!(row.ext, isa::Ext::I) {
return Flow::Rejected;
}
let Some(plan) = classify(self.cfg, row.op, word, pc) else {
return Flow::Rejected;
};
let rd = isa::rd(word);
let rs1 = isa::rs1(word);
let rs2 = isa::rs2(word);
let folded = rd == 0 && matches!(plan, Plan::Alu(_));
let (mut a, mut b) = (None, None);
if !folded {
let (r1, r2) = reads(row.fmt);
if r1 {
a = Some(self.read_x(rs1));
}
if r2 {
b = Some(self.read_x(rs2));
}
}
let live = self.live_regs();
self.b.insn_start(InsnStart {
pc,
next_pc,
ticks: self.ticks,
live,
});
self.b.charge(fetch);
self.ticks += fetch;
if folded {
return Flow::Continue(next_pc);
}
match plan {
Plan::Alu(alu) => {
let v = self.emit_alu(alu, a, b);
self.write_x(rd, v);
Flow::Continue(next_pc)
}
Plan::Load { size, sign } => {
let base = self.need(a);
let off = self.konst(isa::imm_i(word) as u64);
let addr = self.b.binary(Opcode::ADD, Type::I64, base, off);
let mem = MemOp {
size,
sign,
space: MemSpace::MEM,
seg: None,
endian: Endian::Little,
align: self.align(),
kind: AccessKind::Load,
volatile: true,
};
let v = self.b.load(Type::I64, addr, mem);
self.write_x(rd, v);
Flow::Access {
next: next_pc,
store: false,
}
}
Plan::Store { size } => {
let base = self.need(a);
let value = self.need(b);
let off = self.konst(isa::imm_s(word) as u64);
let addr = self.b.binary(Opcode::ADD, Type::I64, base, off);
let mem = MemOp {
size,
sign: Sign::Unsigned,
space: MemSpace::MEM,
seg: None,
endian: Endian::Little,
align: self.align(),
kind: AccessKind::Store,
volatile: true,
};
self.b.store(Type::I64, addr, value, mem);
Flow::Access {
next: next_pc,
store: true,
}
}
Plan::Branch { cond, target } => {
let lhs = self.need(a);
let rhs = self.need(b);
if !self.shape.merges() {
let taken = self.b.setcond(cond, Type::I64, lhs, rhs);
let then = self.konst(target);
let other = self.konst(next_pc);
let sel = self
.b
.emit(Opcode::MOVCOND, Type::I64, &[taken, then, other]);
self.pc_out = Some(sel);
return Flow::Transfer;
}
let inline_taken = target < pc && target & !PAGE_MASK == self.page;
let (exit_pc, next, exit_when) = if inline_taken {
(next_pc, target, cond.invert())
} else {
(target, next_pc, cond)
};
self.side_exit(exit_when, lhs, rhs, exit_pc);
Flow::Continue(next)
}
Plan::Jal { target } => {
if rd != 0 {
let link = self.konst(next_pc);
self.write_x(rd, link);
}
if self.shape.merges() {
return Flow::Continue(target);
}
let t = self.konst(target);
self.pc_out = Some(t);
self.static_exit = Some(target);
Flow::Transfer
}
Plan::Jalr => {
let base = self.need(a);
let off = self.konst(isa::imm_i(word) as u64);
let sum = self.b.binary(Opcode::ADD, Type::I64, base, off);
let mask = self.konst(!1u64);
let target = self.b.binary(Opcode::AND, Type::I64, sum, mask);
if rd != 0 {
let link = self.konst(next_pc);
self.write_x(rd, link);
}
self.pc_out = Some(target);
Flow::Transfer
}
}
}
fn emit_alu(&mut self, alu: Alu, a: Option<Temp>, b: Option<Temp>) -> Temp {
match alu {
Alu::Const(v) => self.konst(v),
Alu::RegImm { op, imm } => {
let lhs = self.need(a);
let rhs = self.konst(imm);
self.b.binary(op, Type::I64, lhs, rhs)
}
Alu::SetCondImm { cond, imm } => {
let lhs = self.need(a);
let rhs = self.konst(imm);
let bit = self.b.setcond(cond, Type::I64, lhs, rhs);
self.b.unary(Opcode::EXT_Z, Type::I64, bit)
}
Alu::ShiftImm { op, shamt } => {
let lhs = self.need(a);
let rhs = self.konst(u64::from(shamt));
self.b.binary(op, Type::I64, lhs, rhs)
}
Alu::RegReg { op } => {
let lhs = self.need(a);
let rhs = self.need(b);
self.b.binary(op, Type::I64, lhs, rhs)
}
Alu::SetCond { cond } => {
let lhs = self.need(a);
let rhs = self.need(b);
let bit = self.b.setcond(cond, Type::I64, lhs, rhs);
self.b.unary(Opcode::EXT_Z, Type::I64, bit)
}
Alu::ShiftReg { op } => {
let lhs = self.need(a);
let raw = self.need(b);
let mask = self.konst(63);
let sh = self.b.binary(Opcode::AND, Type::I64, raw, mask);
self.b.binary(op, Type::I64, lhs, sh)
}
Alu::WordImm { op, imm } => {
let lhs = self.need(a);
let lhs32 = self.b.unary(Opcode::TRUNC, Type::I32, lhs);
let rhs32 = self.konst32(imm);
let r = self.b.binary(op, Type::I32, lhs32, rhs32);
self.b.unary(Opcode::EXT_S, Type::I64, r)
}
Alu::WordShiftImm { op, shamt } => {
let lhs = self.need(a);
let lhs32 = self.b.unary(Opcode::TRUNC, Type::I32, lhs);
let sh = self.konst32(shamt);
let r = self.b.binary(op, Type::I32, lhs32, sh);
self.b.unary(Opcode::EXT_S, Type::I64, r)
}
Alu::WordReg { op } => {
let lhs = self.need(a);
let rhs = self.need(b);
let lhs32 = self.b.unary(Opcode::TRUNC, Type::I32, lhs);
let rhs32 = self.b.unary(Opcode::TRUNC, Type::I32, rhs);
let r = self.b.binary(op, Type::I32, lhs32, rhs32);
self.b.unary(Opcode::EXT_S, Type::I64, r)
}
Alu::WordShiftReg { op } => {
let lhs = self.need(a);
let raw = self.need(b);
let lhs32 = self.b.unary(Opcode::TRUNC, Type::I32, lhs);
let raw32 = self.b.unary(Opcode::TRUNC, Type::I32, raw);
let mask = self.konst32(31);
let sh = self.b.binary(Opcode::AND, Type::I32, raw32, mask);
let r = self.b.binary(op, Type::I32, lhs32, sh);
self.b.unary(Opcode::EXT_S, Type::I64, r)
}
}
}
fn finish(mut self, program_order_pc: u64) -> Block {
let pc = match self.pc_out {
Some(t) => t,
None => self.konst(program_order_pc),
};
let mut live = self.live_regs();
live.push((PC, pc));
let at = self.static_exit.unwrap_or(program_order_pc);
self.b.insn_start(InsnStart {
pc: at,
next_pc: at,
ticks: self.ticks,
live,
});
self.b.exit_tb();
self.b.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::{AddressSpace, RamStore, Region};
use crate::cpu::riscv::Hart;
use crate::cpu::riscv::csr::Extensions;
use crate::ir::verify;
use alloc::sync::Arc;
use alloc::vec;
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)
}
const fn addi(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x13, 0, rd, rs1, imm)
}
const fn add(rd: u32, rs1: u32, rs2: u32) -> u32 {
r_type(0x33, 0, 0, rd, rs1, rs2)
}
const fn sub(rd: u32, rs1: u32, rs2: u32) -> u32 {
r_type(0x33, 0, 0x20, rd, rs1, rs2)
}
const fn sll(rd: u32, rs1: u32, rs2: u32) -> u32 {
r_type(0x33, 1, 0, rd, rs1, rs2)
}
const fn slt(rd: u32, rs1: u32, rs2: u32) -> u32 {
r_type(0x33, 2, 0, rd, rs1, rs2)
}
const fn addw(rd: u32, rs1: u32, rs2: u32) -> u32 {
r_type(0x3b, 0, 0, rd, rs1, rs2)
}
const fn slli(rd: u32, rs1: u32, shamt: u32) -> u32 {
i_type(0x13, 1, rd, rs1, shamt as i32)
}
const fn lui(rd: u32, imm: u32) -> u32 {
0x37 | (rd << 7) | (imm & 0xffff_f000)
}
const fn auipc(rd: u32, imm: u32) -> u32 {
0x17 | (rd << 7) | (imm & 0xffff_f000)
}
const fn lb(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x03, 0, rd, rs1, imm)
}
const fn lwu(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x03, 6, 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 jalr(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0x67, 0, rd, rs1, imm)
}
const ECALL: u32 = 0x0000_0073;
const MUL: u32 = 0x0230_02b3;
const BASE: u64 = 0x2000_0000;
struct Bytes {
base: u64,
words: Vec<u32>,
}
impl InsnSource for Bytes {
fn halfword(&mut self, addr: u64) -> Option<u16> {
let off = addr.checked_sub(self.base)?;
let word = *self.words.get((off / 4) as usize)?;
Some(if off % 4 == 0 {
word as u16
} else {
(word >> 16) as u16
})
}
}
fn lift_at(cfg: &Config, base: u64, words: &[u32]) -> Lifted {
lift_shaped(cfg, base, words, Shape::default())
}
fn lift_shaped(cfg: &Config, base: u64, words: &[u32], shape: Shape) -> Lifted {
let mut src = Bytes {
base,
words: words.to_vec(),
};
let lifted = lift(cfg, Origin::Bare, base, &mut src, MAX_INSNS, shape).expect("RV64 lifts");
verify(&lifted.block).unwrap_or_else(|e| panic!("{e}\n{}", lifted.block));
lifted
}
fn rv64i(words: &[u32]) -> Lifted {
lift_at(&Config::rv64i(), BASE, words)
}
fn shaped(words: &[u32], shape: Shape) -> Lifted {
lift_shaped(&Config::rv64i(), BASE, words, shape)
}
fn ops(block: &Block) -> Vec<&'static str> {
block.insts().iter().map(|i| i.op.name()).collect()
}
fn interpreter_ticks(cfg: Config, words: &[u32], n: usize) -> u64 {
let ram = Arc::new(RamStore::new(0x1_0000));
for (w, word) in words.iter().enumerate() {
for (k, byte) in word.to_le_bytes().iter().enumerate() {
ram.write_u8(w as u64 * 4 + k as u64, *byte).unwrap();
}
}
let space = AddressSpace::new("mem", 64);
space.topology().map(Region::ram("ram", ram), BASE).unwrap();
let hart = Hart::new(cfg.with_reset_vector(BASE));
hart.attach_space(Arc::new(space));
for _ in 0..n {
hart.step();
}
hart.cycles()
}
fn block_ticks(block: &Block) -> u64 {
block.marks().last().expect("a block has boundaries").ticks
}
#[derive(Debug, Default)]
struct Slots {
state: alloc::collections::BTreeMap<u16, u128>,
ticks: u64,
boundaries: Vec<u64>,
}
impl crate::ir::IrHost for Slots {
fn read_slot(&mut self, slot: crate::ir::RegSlot) -> u128 {
self.state.get(&slot.0).copied().unwrap_or(0)
}
fn write_slot(&mut self, slot: crate::ir::RegSlot, value: u128) {
self.state.insert(slot.0, value);
}
fn load(
&mut self,
_mem: &crate::ir::MemOp,
_addr: u64,
) -> crate::core::space::MemResult<u64> {
Err(crate::core::error::BusError::Unassigned)
}
fn store(
&mut self,
_mem: &crate::ir::MemOp,
_addr: u64,
_value: u64,
) -> crate::core::space::MemResult {
Err(crate::core::error::BusError::Unassigned)
}
fn charge(&mut self, ticks: u64) {
self.ticks += ticks;
}
fn insn_start(&mut self, mark: &InsnStart) {
self.boundaries.push(mark.pc);
}
}
#[test]
fn a_lifted_block_verifies_and_then_runs_on_the_portable_backend() {
let l = rv64i(&[addi(5, 0, 7), addi(6, 5, 3), ECALL]);
assert_eq!(l.insns, 2);
verify(&l.block).expect("a lifted block must verify");
let mut host = Slots::default();
let outcome = crate::ir::Interp::new()
.run(&l.block, &mut host)
.expect("the block executes");
assert_eq!(outcome, crate::ir::Outcome::Exit);
assert_eq!(host.state.get(&5), Some(&7));
assert_eq!(host.state.get(&6), Some(&10));
assert_eq!(host.ticks, 4);
assert_eq!(
host.ticks,
interpreter_ticks(Config::rv64i(), &[addi(5, 0, 7), addi(6, 5, 3)], 2)
);
}
#[test]
fn dead_code_elimination_preserves_a_lifted_block() {
let l = rv64i(&[addi(5, 0, 7), addi(6, 5, 3), ECALL]);
let lean = crate::ir::eliminate_dead_code(&l.block);
verify(&lean).expect("an optimised block must still verify");
let mut before = Slots::default();
let mut after = Slots::default();
let out_before = crate::ir::Interp::new().run(&l.block, &mut before).unwrap();
let out_after = crate::ir::Interp::new().run(&lean, &mut after).unwrap();
assert_eq!(out_before, out_after);
assert_eq!(before.state, after.state);
assert_eq!(
before.ticks, after.ticks,
"DCE may not change the tick count"
);
}
#[test]
fn a_register_immediate_alu_op_lifts_to_a_read_a_constant_and_an_add() {
let l = rv64i(&[addi(5, 1, 7), ECALL]);
assert_eq!(l.insns, 1);
assert_eq!(l.stop, Stop::Unsupported);
assert_eq!(
ops(&l.block),
vec![
"get_slot", "insn_start", "charge", "mov", "add", "mov", "insn_start", "exit_tb",
]
);
let exit = l.block.marks().last().unwrap();
let slots: Vec<u16> = exit.live.iter().map(|(s, _)| s.0).collect();
assert_eq!(slots, vec![1, 5, PC.0]);
}
#[test]
fn register_register_and_word_forms_lift() {
let l = rv64i(&[add(5, 1, 2), sub(6, 1, 2), addw(7, 1, 2), ECALL]);
assert_eq!(l.insns, 3);
let names = ops(&l.block);
assert!(names.contains(&"add"), "{names:?}");
assert!(names.contains(&"sub"), "{names:?}");
assert!(names.contains(&"trunc"), "{names:?}");
assert!(names.contains(&"ext_s"), "{names:?}");
let word_add = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::ADD && i.ty == Type::I32)
.expect("addw computes in i32");
assert_eq!(l.block.type_of(word_add.dst.unwrap()), Some(Type::I32));
}
#[test]
fn a_register_shift_masks_its_amount_to_the_register_width() {
let l = rv64i(&[sll(5, 1, 2), ECALL]);
let and = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::AND)
.expect("the shift amount is masked");
let mask = l.block.srcs(
l.block
.insts()
.iter()
.position(|i| core::ptr::eq(i, and))
.unwrap(),
)[1];
let def = l
.block
.insts()
.iter()
.find(|i| i.dst == Some(mask))
.expect("the mask is a constant");
assert_eq!(def.imm, Some(Const::Int(63)));
}
#[test]
fn a_shift_by_an_immediate_needs_no_guard_and_an_out_of_range_one_is_rejected() {
let l = rv64i(&[slli(5, 1, 63), ECALL]);
assert_eq!(l.insns, 1);
assert!(!ops(&l.block).contains(&"and"));
let bad = 0x13 | (5 << 7) | (1 << 12) | (1 << 15) | (64 << 20);
let l = rv64i(&[bad, ECALL]);
assert_eq!(l.insns, 0);
assert_eq!(l.stop, Stop::Unsupported);
}
#[test]
fn set_less_than_widens_the_one_bit_result() {
let l = rv64i(&[slt(5, 1, 2), ECALL]);
let names = ops(&l.block);
assert!(names.contains(&"setcond"), "{names:?}");
assert!(names.contains(&"ext_z"), "{names:?}");
let cmp = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::SETCOND)
.unwrap();
assert_eq!(cmp.cond, Some(Cond::LtS));
assert_eq!(l.block.type_of(cmp.dst.unwrap()), Some(Type::I1));
}
#[test]
fn lui_and_auipc_fold_to_constants() {
let l = rv64i(&[lui(5, 0x1234_5000), auipc(6, 0x1000), ECALL]);
assert_eq!(l.insns, 2);
assert!(!ops(&l.block).contains(&"add"));
let constants: Vec<u128> = l
.block
.insts()
.iter()
.filter(|i| i.op == Opcode::MOV)
.filter_map(|i| i.imm.map(Const::bits))
.collect();
assert!(constants.contains(&0x1234_5000), "{constants:x?}");
assert!(
constants.contains(&u128::from(BASE + 4 + 0x1000)),
"{constants:x?}"
);
}
#[test]
fn a_write_to_x0_folds_the_whole_computation_away() {
let l = rv64i(&[add(0, 1, 2), ECALL]);
assert_eq!(l.insns, 1);
assert_eq!(
ops(&l.block),
vec!["insn_start", "charge", "mov", "insn_start", "exit_tb"]
);
let exit = l.block.marks().last().unwrap();
assert_eq!(exit.live.len(), 1, "only the PC is live");
assert_eq!(exit.live[0].0, PC);
}
#[test]
fn a_read_of_x0_is_a_zero_constant_shared_across_the_block() {
let l = rv64i(&[add(5, 0, 0), add(6, 0, 0), ECALL]);
assert_eq!(l.insns, 2);
let zeros = l
.block
.insts()
.iter()
.filter(|i| i.imm == Some(Const::Int(0)))
.count();
assert_eq!(zeros, 1);
for mark in l.block.marks() {
assert!(mark.live.iter().all(|(s, _)| *s != x_slot(0)));
}
}
#[test]
fn fetch_charges_match_the_interpreter() {
let program = [addi(5, 0, 1), add(6, 5, 5), lui(7, 0x1000), ECALL];
let l = rv64i(&program);
assert_eq!(l.insns, 3);
assert_eq!(block_ticks(&l.block), 6);
assert_eq!(interpreter_ticks(Config::rv64i(), &program, 3), 6);
}
#[test]
fn a_compressed_instruction_charges_one_fetch() {
let c_addi: u16 = 0x0285;
let mut cfg = Config::rv64gc();
cfg.pmp_count = 0;
let words = [u32::from(c_addi) | (u32::from(c_addi) << 16), ECALL];
let l = lift_at(&cfg, BASE, &words);
assert_eq!(l.insns, 2);
assert_eq!(block_ticks(&l.block), 2);
assert_eq!(interpreter_ticks(cfg, &words, 2), 2);
}
#[test]
fn the_tick_column_is_cumulative_and_never_runs_backwards() {
let l = rv64i(&[addi(5, 0, 1), addi(6, 0, 2), ECALL]);
let ticks: Vec<u64> = l.block.marks().iter().map(|m| m.ticks).collect();
assert_eq!(ticks, vec![0, 2, 4]);
}
#[test]
fn a_memory_op_charges_nothing_itself_whatever_the_shape() {
let program = [ld(5, 1, 8), addi(6, 0, 1)];
for shape in [Shape::BasicBlock, Shape::Extended, Shape::Trace] {
let l = shaped(&program, shape);
let charges: Vec<u128> = l
.block
.insts()
.iter()
.filter(|i| i.op == Opcode::CHARGE)
.filter_map(|i| i.imm.map(Const::bits))
.collect();
if shape.access_ends_block() {
assert_eq!(l.insns, 1, "{shape:?}");
assert_eq!(l.stop, Stop::Access);
assert_eq!(block_ticks(&l.block), 2);
assert_eq!(charges, vec![2]);
} else {
assert_eq!(l.insns, 2, "{shape:?}");
assert_eq!(block_ticks(&l.block), 4, "{shape:?}");
assert_eq!(charges, vec![2, 2], "{shape:?}");
}
}
}
#[test]
fn a_store_ends_the_block_under_every_shape() {
for shape in [Shape::BasicBlock, Shape::Extended, Shape::Trace] {
let l = shaped(&[sd(1, 2, 0), addi(6, 0, 1), ECALL], shape);
assert_eq!(l.insns, 1, "{shape:?}");
assert_eq!(l.stop, Stop::Access, "{shape:?}");
}
let l = shaped(&[ld(5, 1, 0), addi(6, 0, 1), ECALL], Shape::Trace);
assert_eq!(l.insns, 2);
}
#[test]
fn the_static_tick_column_stays_monotonic_across_an_access() {
let l = rv64i(&[addi(5, 0, 1), ld(6, 1, 0), addi(7, 0, 2), ECALL]);
let ticks: Vec<u64> = l.block.marks().iter().map(|m| m.ticks).collect();
assert_eq!(ticks, vec![0, 2, 4, 6]);
assert_eq!(l.insns, 3);
}
#[test]
fn loads_carry_their_width_sign_and_misalignment_policy() {
for (word, size, sign) in [
(lb(5, 1, 4), Width::U8, Sign::Signed),
(lwu(5, 1, 4), Width::U32, Sign::Unsigned),
(ld(5, 1, 4), Width::U64, Sign::Signed),
] {
let l = rv64i(&[word]);
let mem = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::LD)
.and_then(|i| i.mem)
.expect("a load is in the block");
assert_eq!(mem.size, size);
assert_eq!(mem.sign, sign);
assert_eq!(mem.kind, AccessKind::Load);
assert_eq!(mem.endian, Endian::Little);
assert_eq!(mem.align, Align::None);
assert!(mem.volatile);
}
let mut strict = Config::rv64i();
strict.misaligned = false;
let l = lift_at(&strict, BASE, &[ld(5, 1, 4)]);
let mem = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::LD)
.and_then(|i| i.mem)
.unwrap();
assert_eq!(mem.align, Align::Fault);
}
#[test]
fn a_store_reads_both_registers_and_writes_none() {
let l = rv64i(&[sd(1, 2, 16)]);
assert_eq!(l.stop, Stop::Access);
let st = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::ST)
.expect("a store is in the block");
assert!(st.dst.is_none());
assert_eq!(st.mem.unwrap().size, Width::U64);
let exit = l.block.marks().last().unwrap();
let slots: Vec<u16> = exit.live.iter().map(|(s, _)| s.0).collect();
assert_eq!(slots, vec![1, 2, PC.0]);
}
#[test]
fn a_load_into_x0_still_makes_its_access() {
let l = rv64i(&[ld(0, 1, 0)]);
assert!(ops(&l.block).contains(&"ld"));
let exit = l.block.marks().last().unwrap();
assert!(exit.live.iter().all(|(s, _)| *s != x_slot(0)));
}
#[test]
fn without_merging_a_conditional_branch_selects_between_two_constant_pcs() {
for shape in [Shape::BasicBlock, Shape::Extended] {
let l = shaped(&[beq(1, 2, 8), addi(5, 0, 1)], shape);
assert_eq!(l.insns, 1);
assert_eq!(l.stop, Stop::Transfer);
let names = ops(&l.block);
assert!(names.contains(&"setcond"), "{names:?}");
assert!(names.contains(&"movcond"), "{names:?}");
let sel = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::MOVCOND)
.unwrap();
let exit = l.block.marks().last().unwrap();
assert_eq!(exit.live.last().copied(), Some((PC, sel.dst.unwrap())));
let constants: Vec<u128> = l
.block
.insts()
.iter()
.filter_map(|i| i.imm.map(Const::bits))
.collect();
assert!(constants.contains(&u128::from(BASE + 8)), "{constants:x?}");
assert!(constants.contains(&u128::from(BASE + 4)), "{constants:x?}");
}
}
#[test]
fn a_forward_branch_becomes_a_side_exit_and_the_trace_falls_through() {
let l = shaped(
&[beq(1, 2, 8), addi(5, 0, 1), addi(6, 0, 2), ECALL],
Shape::Trace,
);
assert_eq!(l.insns, 3, "the branch and both instructions after it");
let names = ops(&l.block);
assert!(names.contains(&"brcond"), "{names:?}");
assert!(
!names.contains(&"movcond"),
"no pc select is needed: {names:?}"
);
let exits: Vec<u64> = l
.block
.marks()
.iter()
.filter(|m| m.pc == m.next_pc)
.map(|m| m.pc)
.collect();
assert_eq!(exits, vec![BASE + 8, BASE + 12], "{}", l.block);
let brcond = l
.block
.insts()
.iter()
.find(|i| i.op == Opcode::BRCOND)
.expect("a side exit branches");
assert_eq!(brcond.cond, Some(Cond::Eq.invert()));
assert_eq!(
l.block
.insts()
.iter()
.filter(|i| i.op == Opcode::EXIT_TB)
.count(),
2
);
}
#[test]
fn a_backward_branch_unrolls_the_loop_it_closes() {
let l = shaped(&[addi(5, 5, 1), beq(0, 0, -4)], Shape::Trace);
assert_eq!(l.insns, MAX_INSNS);
assert_eq!(l.stop, Stop::Limit);
let exits: Vec<u64> = l
.block
.marks()
.iter()
.filter(|m| m.pc == m.next_pc)
.map(|m| m.pc)
.collect();
assert_eq!(exits.len(), 33, "one per iteration, plus the block's own");
assert!(exits[..32].iter().all(|pc| *pc == BASE + 8), "{exits:x?}");
}
#[test]
fn a_backward_branch_off_the_entry_page_is_a_side_exit_rather_than_a_trace() {
let base = BASE + 0x1000;
let l = lift_shaped(
&Config::rv64i(),
base,
&[beq(0, 0, -0x100), addi(5, 0, 1), ECALL],
Shape::Trace,
);
assert_eq!(l.insns, 2, "the branch and the fall-through");
let exits: Vec<u64> = l
.block
.marks()
.iter()
.filter(|m| m.pc == m.next_pc)
.map(|m| m.pc)
.collect();
assert_eq!(exits, vec![base - 0x100, base + 8]);
}
#[test]
fn without_merging_jal_links_a_constant_and_exits_at_a_known_pc() {
for shape in [Shape::BasicBlock, Shape::Extended] {
let l = shaped(&[j_type(1, 8), addi(5, 0, 1)], shape);
assert_eq!(l.stop, Stop::Transfer);
let exit = l.block.marks().last().unwrap();
assert_eq!(exit.pc, BASE + 8);
assert!(exit.live.iter().any(|(s, _)| *s == x_slot(1)));
}
}
#[test]
fn a_trace_walks_straight_through_a_direct_jump() {
let l = shaped(
&[j_type(1, 8), addi(5, 0, 1), addi(6, 0, 2), ECALL],
Shape::Trace,
);
assert_eq!(l.insns, 2, "the jump and the instruction it jumped to");
let pcs: Vec<u64> = l.block.marks().iter().map(|m| m.pc).collect();
assert_eq!(pcs, vec![BASE, BASE + 8, BASE + 12]);
assert_eq!(
l.block.marks()[2].ticks,
4,
"two fetches each, nothing else"
);
let exit = l.block.marks().last().unwrap();
assert!(exit.live.iter().any(|(s, _)| *s == x_slot(1)));
}
#[test]
fn a_slot_a_boundary_shadows_stays_shadowed_at_every_later_boundary() {
let l = shaped(
&[
addi(5, 0, 1), addi(6, 5, 2), ld(7, 1, 0), beq(0, 0, -8), ],
Shape::Trace,
);
let mut seen: Vec<u16> = Vec::new();
let insts = l.block.insts();
for (i, inst) in insts.iter().enumerate() {
if inst.op != Opcode::INSN_START {
continue;
}
let mark = &l.block.marks()[inst.aux as usize];
let here: Vec<u16> = mark.live.iter().map(|(s, _)| s.0).collect();
for slot in &seen {
assert!(
here.contains(slot),
"boundary {i} at {:#x} dropped slot {slot}, whose host copy is stale:\n{}",
mark.pc,
l.block
);
}
let is_exit = insts.get(i + 1).is_some_and(|next| next.op.is_terminator());
if !is_exit {
for slot in here {
if !seen.contains(&slot) {
seen.push(slot);
}
}
}
}
assert!(seen.len() >= 3, "the trace bound x5, x6 and x7: {seen:?}");
}
#[test]
fn a_register_computed_before_a_merged_jump_is_still_in_a_temporary_after_it() {
let l = shaped(
&[
addi(5, 0, 7),
j_type(0, 8),
addi(9, 0, 1),
addi(6, 5, 1),
ECALL,
],
Shape::Trace,
);
assert_eq!(l.insns, 3);
let reads: Vec<u32> = l
.block
.insts()
.iter()
.filter(|i| i.op == Opcode::GET_SLOT)
.map(|i| i.aux)
.collect();
assert!(
!reads.contains(&5),
"x5 went out to a slot and came back: {}",
l.block
);
}
#[test]
fn jal_with_no_link_register_emits_no_link() {
for shape in [Shape::BasicBlock, Shape::Extended] {
let l = shaped(&[j_type(0, 8)], shape);
let exit = l.block.marks().last().unwrap();
assert_eq!(exit.live.len(), 1, "only the PC");
}
}
#[test]
fn jalr_clears_the_low_bit_and_needs_a_core_with_c() {
let l = rv64i(&[jalr(1, 2, 4)]);
assert_eq!(l.insns, 0);
assert_eq!(l.stop, Stop::Unsupported);
let mut cfg = Config::rv64gc();
cfg.pmp_count = 0;
let l = lift_at(&cfg, BASE, &[jalr(1, 2, 4)]);
assert_eq!(l.insns, 1);
assert_eq!(l.stop, Stop::Transfer);
let masks: Vec<u128> = l
.block
.insts()
.iter()
.filter_map(|i| i.imm.map(Const::bits))
.collect();
assert!(masks.contains(&u128::from(!1u64)), "{masks:x?}");
}
#[test]
fn a_branch_to_a_misaligned_target_is_out_of_the_subset_without_c() {
let l = rv64i(&[beq(1, 2, 2)]);
assert_eq!(l.insns, 0);
assert_eq!(l.stop, Stop::Unsupported);
}
#[test]
fn a_block_ends_cleanly_at_an_unsupported_opcode() {
for unsupported in [ECALL, MUL] {
let l = rv64i(&[addi(5, 0, 1), unsupported, addi(6, 0, 2)]);
assert_eq!(l.insns, 1);
assert_eq!(l.stop, Stop::Unsupported);
let exit = l.block.marks().last().unwrap();
assert_eq!(exit.pc, BASE + 4);
assert_eq!(l.block.insts().last().unwrap().op, Opcode::EXIT_TB);
}
}
#[test]
fn a_block_whose_very_first_instruction_is_unsupported_is_still_well_formed() {
let l = rv64i(&[ECALL]);
assert_eq!(l.insns, 0);
assert_eq!(ops(&l.block), vec!["mov", "insn_start", "exit_tb"]);
assert_eq!(l.block.marks()[0].pc, BASE);
assert_eq!(block_ticks(&l.block), 0);
}
#[test]
fn a_block_never_leaves_the_page_it_started_on() {
let base = BASE + 0x1000 - 8;
let l = lift_at(
&Config::rv64i(),
base,
&[addi(5, 0, 1), addi(6, 0, 2), addi(7, 0, 3)],
);
assert_eq!(l.insns, 2);
assert_eq!(l.stop, Stop::Page);
assert_eq!(l.block.marks().last().unwrap().pc, base + 8);
}
#[test]
fn the_instruction_limit_ends_a_block() {
let mut src = Bytes {
base: BASE,
words: vec![addi(5, 0, 1); 8],
};
let l = lift(
&Config::rv64i(),
Origin::Bare,
BASE,
&mut src,
3,
Shape::default(),
)
.expect("RV64 lifts");
verify(&l.block).expect("a limited block still verifies");
assert_eq!(l.insns, 3);
assert_eq!(l.stop, Stop::Limit);
}
#[test]
fn unreadable_bytes_end_a_block_rather_than_inventing_an_encoding() {
let l = rv64i(&[addi(5, 0, 1)]);
assert_eq!(l.insns, 1);
assert_eq!(l.stop, Stop::Unreadable);
}
#[test]
fn rv32_is_refused_rather_than_mis_widened() {
let mut src = Bytes {
base: BASE,
words: vec![addi(5, 0, 1)],
};
let err = lift(
&Config::rv32gc(),
Origin::Bare,
BASE,
&mut src,
MAX_INSNS,
Shape::default(),
)
.expect_err("RV32 is not lifted yet");
assert!(matches!(err, Error::Unimplemented(_)), "{err}");
}
fn paged_csrs() -> Csrs {
let mut csrs = Csrs::new(Xlen::Rv64, Extensions::GC, 0, 0);
csrs.priv_mode = Priv::Supervisor;
csrs.satp = (8u64 << 60) | 1;
csrs
}
#[test]
fn the_origin_agrees_with_the_mmu_about_whether_a_lift_is_virtual() {
let mut csrs = paged_csrs();
csrs.priv_mode = Priv::Machine;
assert_eq!(Origin::of(&csrs, Priv::Machine), Origin::Bare);
let csrs = paged_csrs();
assert_eq!(
Origin::of(&csrs, Priv::Supervisor),
Origin::Paged {
generation: csrs.translation_gen
}
);
let mut off = paged_csrs();
off.satp = 0;
assert_eq!(Origin::of(&off, Priv::Supervisor), Origin::Bare);
assert_eq!(Origin::of(&off, Priv::User), Origin::Bare);
}
#[test]
fn a_virtual_lift_and_a_physical_lift_of_the_same_address_are_different_blocks() {
let bare = rv64i(&[addi(5, 0, 1)]).block.key;
let mut src = Bytes {
base: BASE,
words: vec![addi(5, 0, 1)],
};
let paged = lift(
&Config::rv64i(),
Origin::Paged { generation: 7 },
BASE,
&mut src,
MAX_INSNS,
Shape::default(),
)
.expect("RV64 lifts")
.block
.key;
assert_ne!(bare, paged);
}
#[test]
fn changing_the_mapping_invalidates_a_block_lifted_under_the_old_one() {
let mut csrs = paged_csrs();
let before = Origin::of(&csrs, Priv::Supervisor);
csrs.bump_translation();
let after = Origin::of(&csrs, Priv::Supervisor);
assert_ne!(before, after);
let words = [addi(5, 0, 1)];
let key_of = |origin| {
let mut src = Bytes {
base: BASE,
words: words.to_vec(),
};
lift(
&Config::rv64i(),
origin,
BASE,
&mut src,
MAX_INSNS,
Shape::default(),
)
.expect("RV64 lifts")
.block
.key
};
assert_ne!(key_of(before), key_of(after));
}
#[test]
fn the_block_bound_is_the_mmus_smallest_page() {
assert_eq!(PAGE_MASK + 1, crate::cpu::riscv::mmu::PAGE_SIZE);
assert_eq!(crate::cpu::riscv::mmu::PAGE_BITS, 12);
}
#[test]
fn the_cache_key_separates_configurations_that_lift_differently() {
let plain = rv64i(&[addi(5, 0, 1)]).block.key;
let mut with_c = Config::rv64i();
with_c.ext = Extensions {
c: true,
..Extensions::I
};
let compressed = lift_at(&with_c, BASE, &[addi(5, 0, 1)]).block.key;
assert_ne!(plain, compressed);
let mut strict = Config::rv64i();
strict.misaligned = false;
assert_ne!(plain, lift_at(&strict, BASE, &[addi(5, 0, 1)]).block.key);
}
#[test]
fn every_boundary_names_only_temporaries_that_already_exist() {
let l = rv64i(&[addi(5, 1, 1), add(6, 5, 2), sd(6, 5, 0)]);
assert_eq!(l.insns, 3);
for mark in l.block.marks() {
for (_, t) in &mark.live {
assert!(t.index() < l.block.temp_count());
}
}
for mark in l.block.marks() {
assert!(mark.live.iter().all(|(s, _)| *s != RESERVATION));
}
}
#[test]
fn a_longer_straight_line_block_matches_the_interpreter_tick_for_tick() {
let program = [
lui(5, 0x1000),
addi(5, 5, 0x20),
slli(6, 5, 3),
slt(7, 5, 6),
addw(8, 5, 6),
sub(9, 6, 5),
ECALL,
];
let l = rv64i(&program);
assert_eq!(l.insns, 6);
assert_eq!(block_ticks(&l.block), 12);
assert_eq!(interpreter_ticks(Config::rv64i(), &program, 6), 12);
}
}