forked from libre-chip/cpu
1558 lines
50 KiB
Rust
1558 lines
50 KiB
Rust
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
// See Notices.txt for copyright information
|
|
|
|
use cpu::{
|
|
config::{CpuConfig, UnitConfig},
|
|
next_pc::{
|
|
CallStackOp, DecodeToPostDecodeInterface, DecodeToPostDecodeInterfaceInner,
|
|
FETCH_BLOCK_ID_WIDTH, NextPcToFetchInterface, NextPcToFetchInterfaceInner,
|
|
PostDecodeOutputInterface, RetireToNextPcInterface, RetireToNextPcInterfaceInner,
|
|
RetireToNextPcInterfacePerInsn, WipDecodedInsn, WipDecodedInsnKind, next_pc,
|
|
},
|
|
unit::UnitKind,
|
|
util::array_vec::ArrayVec,
|
|
};
|
|
use fayalite::{
|
|
intern::{Intern, Interned},
|
|
prelude::*,
|
|
sim::vcd::VcdWriterDecls,
|
|
util::{DebugAsDisplay, RcWriter},
|
|
};
|
|
use std::{
|
|
cell::Cell,
|
|
collections::{BTreeMap, BTreeSet, VecDeque},
|
|
fmt,
|
|
num::NonZeroUsize,
|
|
ops::Range,
|
|
u64,
|
|
};
|
|
|
|
macro_rules! make_regs {
|
|
(
|
|
$($Name:ident = $value:literal,)*
|
|
) => {
|
|
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[allow(dead_code)]
|
|
enum MockReg {
|
|
$($Name = $value,)*
|
|
}
|
|
|
|
impl MockReg {
|
|
const REGS: [Self; [$(Self::$Name),*].len()] = [$(Self::$Name),*];
|
|
}
|
|
|
|
const _: () = {
|
|
let mut i = 0;
|
|
while i < MockReg::REGS.len() {
|
|
// verify the values are correct
|
|
assert!(i == MockReg::REGS[i] as usize);
|
|
i += 1;
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
make_regs! {
|
|
Zero = 0,
|
|
R1 = 1,
|
|
R2 = 2,
|
|
R3 = 3,
|
|
R4 = 4,
|
|
R5 = 5,
|
|
R6 = 6,
|
|
R7 = 7,
|
|
}
|
|
|
|
#[derive(Clone, Hash, PartialEq, Eq)]
|
|
struct MockRegs([u64; MockReg::REGS.len()]);
|
|
|
|
impl fmt::Debug for MockRegs {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut debug_map = f.debug_map();
|
|
for reg in MockReg::REGS {
|
|
debug_map.entry(®, &format_args!("{:#x}", self.read(reg)));
|
|
}
|
|
debug_map.finish()
|
|
}
|
|
}
|
|
|
|
impl MockRegs {
|
|
fn new() -> Self {
|
|
Self([0; _])
|
|
}
|
|
fn write(&mut self, reg: MockReg, value: u64) {
|
|
match reg {
|
|
MockReg::Zero => {
|
|
// writing to the zero reg does nothing
|
|
}
|
|
MockReg::R1
|
|
| MockReg::R2
|
|
| MockReg::R3
|
|
| MockReg::R4
|
|
| MockReg::R5
|
|
| MockReg::R6
|
|
| MockReg::R7 => self.0[reg as usize] = value,
|
|
}
|
|
}
|
|
fn read(&self, reg: MockReg) -> u64 {
|
|
self.0[reg as usize]
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
#[allow(dead_code)]
|
|
enum MockIntCmp {
|
|
Eq,
|
|
Ne,
|
|
Ult,
|
|
Ule,
|
|
Ugt,
|
|
Uge,
|
|
Slt,
|
|
Sle,
|
|
Sgt,
|
|
Sge,
|
|
}
|
|
|
|
impl MockIntCmp {
|
|
fn cmp(self, lhs_u: u64, rhs_u: u64) -> bool {
|
|
let lhs_s = lhs_u as i64;
|
|
let rhs_s = rhs_u as i64;
|
|
match self {
|
|
MockIntCmp::Eq => lhs_u == rhs_u,
|
|
MockIntCmp::Ne => lhs_u != rhs_u,
|
|
MockIntCmp::Ult => lhs_u < rhs_u,
|
|
MockIntCmp::Ule => lhs_u <= rhs_u,
|
|
MockIntCmp::Ugt => lhs_u > rhs_u,
|
|
MockIntCmp::Uge => lhs_u >= rhs_u,
|
|
MockIntCmp::Slt => lhs_s < rhs_s,
|
|
MockIntCmp::Sle => lhs_s <= rhs_s,
|
|
MockIntCmp::Sgt => lhs_s > rhs_s,
|
|
MockIntCmp::Sge => lhs_s >= rhs_s,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
|
#[allow(dead_code)]
|
|
enum MockInsn {
|
|
ReadInputByte {
|
|
dest: MockReg,
|
|
},
|
|
WriteOutputByte {
|
|
src: MockReg,
|
|
},
|
|
AddI {
|
|
dest: MockReg,
|
|
src: MockReg,
|
|
immediate: u64,
|
|
},
|
|
Jump {
|
|
target: u64,
|
|
},
|
|
BrCond {
|
|
target: u64,
|
|
cond: MockIntCmp,
|
|
lhs: MockReg,
|
|
rhs: MockReg,
|
|
},
|
|
BrCondI {
|
|
target: u64,
|
|
cond: MockIntCmp,
|
|
lhs: MockReg,
|
|
rhs: u64,
|
|
},
|
|
Call {
|
|
target: u64,
|
|
},
|
|
Ret,
|
|
ExitSysCall,
|
|
Illegal,
|
|
}
|
|
|
|
impl fmt::Debug for MockInsn {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
macro_rules! u64_hex {
|
|
($v:expr) => {
|
|
&format_args!("{:#x}", $v)
|
|
};
|
|
}
|
|
match self {
|
|
Self::ReadInputByte { dest } => {
|
|
f.debug_struct("ReadInputByte").field("dest", dest).finish()
|
|
}
|
|
Self::WriteOutputByte { src } => {
|
|
f.debug_struct("WriteOutputByte").field("src", src).finish()
|
|
}
|
|
Self::AddI {
|
|
dest,
|
|
src,
|
|
immediate,
|
|
} => f
|
|
.debug_struct("AddI")
|
|
.field("dest", dest)
|
|
.field("src", src)
|
|
.field("immediate", u64_hex!(immediate))
|
|
.finish(),
|
|
Self::Jump { target } => f
|
|
.debug_struct("Jump")
|
|
.field("target", u64_hex!(target))
|
|
.finish(),
|
|
Self::BrCond {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
} => f
|
|
.debug_struct("BrCond")
|
|
.field("target", u64_hex!(target))
|
|
.field("cond", cond)
|
|
.field("lhs", lhs)
|
|
.field("rhs", rhs)
|
|
.finish(),
|
|
Self::BrCondI {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
} => f
|
|
.debug_struct("BrCondI")
|
|
.field("target", u64_hex!(target))
|
|
.field("cond", cond)
|
|
.field("lhs", lhs)
|
|
.field("rhs", u64_hex!(rhs))
|
|
.finish(),
|
|
Self::Call { target } => f
|
|
.debug_struct("Call")
|
|
.field("target", u64_hex!(target))
|
|
.finish(),
|
|
Self::Ret => write!(f, "Ret"),
|
|
Self::ExitSysCall => write!(f, "ExitSysCall"),
|
|
Self::Illegal => write!(f, "Illegal"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MockInsn {
|
|
fn byte_len(self) -> u64 {
|
|
// TODO: change later
|
|
4 // all instructions are 4 bytes long for now
|
|
}
|
|
const READ_INPUT_REG_EOF: u64 = u64::MAX;
|
|
fn targets_mut(&mut self) -> impl Iterator<Item = &mut u64> {
|
|
match self {
|
|
MockInsn::ReadInputByte { .. }
|
|
| MockInsn::WriteOutputByte { .. }
|
|
| MockInsn::Ret
|
|
| MockInsn::ExitSysCall
|
|
| MockInsn::Illegal
|
|
| MockInsn::AddI { .. } => None,
|
|
MockInsn::Jump { target }
|
|
| MockInsn::BrCond { target, .. }
|
|
| MockInsn::BrCondI { target, .. }
|
|
| MockInsn::Call { target } => Some(target),
|
|
}
|
|
.into_iter()
|
|
}
|
|
#[hdl]
|
|
fn wip_decoded_insn_kind(self) -> SimValue<WipDecodedInsnKind> {
|
|
match self {
|
|
MockInsn::ReadInputByte { .. }
|
|
| MockInsn::WriteOutputByte { .. }
|
|
| MockInsn::AddI { .. } =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::NonBranch()
|
|
}
|
|
MockInsn::Jump { target } =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::Branch(target)
|
|
}
|
|
MockInsn::BrCond { target, .. } | MockInsn::BrCondI { target, .. } =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::BranchCond(target)
|
|
}
|
|
MockInsn::Call { target } =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::Call(target)
|
|
}
|
|
MockInsn::Ret =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::Ret()
|
|
}
|
|
MockInsn::ExitSysCall =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::Call(EXIT_PC)
|
|
}
|
|
MockInsn::Illegal =>
|
|
{
|
|
#[hdl(sim)]
|
|
WipDecodedInsnKind::Interrupt(DEMO_ILLEGAL_INSN_TRAP)
|
|
}
|
|
}
|
|
}
|
|
#[hdl]
|
|
fn call_stack_op(self, pc: u64) -> SimValue<CallStackOp> {
|
|
match self {
|
|
MockInsn::ReadInputByte { .. }
|
|
| MockInsn::WriteOutputByte { .. }
|
|
| MockInsn::AddI { .. }
|
|
| MockInsn::Jump { .. }
|
|
| MockInsn::BrCond { .. }
|
|
| MockInsn::BrCondI { .. }
|
|
| MockInsn::Illegal =>
|
|
{
|
|
#[hdl(sim)]
|
|
CallStackOp::None()
|
|
}
|
|
MockInsn::Call { .. } | MockInsn::ExitSysCall =>
|
|
{
|
|
#[hdl(sim)]
|
|
CallStackOp::Push(pc.wrapping_add(self.byte_len()))
|
|
}
|
|
MockInsn::Ret =>
|
|
{
|
|
#[hdl(sim)]
|
|
CallStackOp::Pop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
struct MockMachineState {
|
|
reset_at_exit: bool,
|
|
call_stack: Vec<u64>,
|
|
orig_input: &'static [u8],
|
|
input: &'static [u8],
|
|
output: Vec<u8>,
|
|
regs: MockRegs,
|
|
pc: u64,
|
|
insns: MockInsns,
|
|
}
|
|
|
|
/// match Microwatt's reset PC
|
|
const RESET_PC: u64 = 0;
|
|
|
|
const EXIT_PC: u64 = u64::from_be_bytes(*b"ExitExit");
|
|
|
|
impl MockMachineState {
|
|
fn new(insns: MockInsns, input: impl AsRef<[u8]>, reset_at_exit: bool) -> Self {
|
|
let input = Interned::into_inner(input.as_ref().intern());
|
|
Self {
|
|
reset_at_exit,
|
|
call_stack: Vec::with_capacity(16),
|
|
orig_input: input,
|
|
input,
|
|
output: Vec::with_capacity(16),
|
|
regs: MockRegs::new(),
|
|
pc: RESET_PC,
|
|
insns,
|
|
}
|
|
}
|
|
fn reset(&mut self) {
|
|
let Self {
|
|
reset_at_exit: _,
|
|
call_stack,
|
|
orig_input,
|
|
input,
|
|
output,
|
|
regs,
|
|
pc,
|
|
insns: _,
|
|
} = self;
|
|
call_stack.clear();
|
|
*input = *orig_input;
|
|
output.clear();
|
|
*regs = MockRegs::new();
|
|
*pc = RESET_PC;
|
|
}
|
|
fn run_one(&mut self, trace: bool) -> RetireSeqEntry {
|
|
let orig_pc = self.pc;
|
|
let insn = self
|
|
.insns
|
|
.insns
|
|
.get(&self.pc)
|
|
.copied()
|
|
.unwrap_or(MockInsn::Illegal);
|
|
if trace {
|
|
println!("{orig_pc:#x}: {insn:?}");
|
|
}
|
|
let mut next_pc = self.pc.wrapping_add(insn.byte_len());
|
|
let mut cond_br_taken = None;
|
|
let mut cond_br = |target: u64, cond: MockIntCmp, lhs: u64, rhs: u64| {
|
|
let taken = cond.cmp(lhs, rhs);
|
|
cond_br_taken = Some(taken);
|
|
if taken {
|
|
next_pc = target;
|
|
}
|
|
};
|
|
let reg = |reg| {
|
|
let retval = self.regs.read(reg);
|
|
if trace {
|
|
println!("read: {reg:?} -> {retval:#x}");
|
|
}
|
|
retval
|
|
};
|
|
let write_reg = |this: &mut Self, reg, value| {
|
|
if trace {
|
|
println!("write: {reg:?} <- {value:#x}");
|
|
}
|
|
this.regs.write(reg, value);
|
|
};
|
|
match insn {
|
|
MockInsn::ReadInputByte { dest } => {
|
|
let value = self
|
|
.input
|
|
.split_off_first()
|
|
.copied()
|
|
.map(u64::from)
|
|
.unwrap_or(MockInsn::READ_INPUT_REG_EOF);
|
|
write_reg(self, dest, value);
|
|
}
|
|
MockInsn::WriteOutputByte { src } => self.output.push(reg(src) as u8),
|
|
MockInsn::AddI {
|
|
dest,
|
|
src,
|
|
immediate,
|
|
} => {
|
|
let value = reg(src).wrapping_add(immediate);
|
|
write_reg(self, dest, value);
|
|
}
|
|
MockInsn::Jump { target } => next_pc = target,
|
|
MockInsn::BrCond {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
} => cond_br(target, cond, reg(lhs), reg(rhs)),
|
|
MockInsn::BrCondI {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
} => cond_br(target, cond, reg(lhs), rhs),
|
|
MockInsn::Call { target } => {
|
|
self.call_stack.push(next_pc);
|
|
next_pc = target;
|
|
}
|
|
MockInsn::Ret => next_pc = self.call_stack.pop().unwrap_or(DEMO_ILLEGAL_INSN_TRAP),
|
|
MockInsn::ExitSysCall => {
|
|
if self.reset_at_exit {
|
|
self.reset();
|
|
return RetireSeqEntry {
|
|
pc: orig_pc,
|
|
cond_br_taken: None,
|
|
insn,
|
|
};
|
|
} else {
|
|
next_pc = EXIT_PC;
|
|
}
|
|
}
|
|
MockInsn::Illegal => next_pc = DEMO_ILLEGAL_INSN_TRAP,
|
|
}
|
|
self.pc = next_pc;
|
|
RetireSeqEntry {
|
|
pc: orig_pc,
|
|
cond_br_taken,
|
|
insn,
|
|
}
|
|
}
|
|
fn run_until_exit(&mut self, limit: u64, trace: bool) -> Result<u64, String> {
|
|
for _ in 0..limit {
|
|
let v = self.run_one(trace);
|
|
match v.insn {
|
|
MockInsn::ExitSysCall => return Ok(self.regs.read(MockReg::R1)),
|
|
MockInsn::Illegal => return Err(format!("illegal instruction at {:#x}", v.pc)),
|
|
_ => {}
|
|
}
|
|
}
|
|
Err(format!("reached instruction count limit at {:#x}", self.pc))
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
struct RetireSeqEntry {
|
|
pc: u64,
|
|
cond_br_taken: Option<bool>,
|
|
insn: MockInsn,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct RetireSeq(MockMachineState);
|
|
|
|
impl RetireSeq {
|
|
fn new(mock_machine: MockMachineState) -> Self {
|
|
Self(mock_machine)
|
|
}
|
|
}
|
|
|
|
impl Iterator for RetireSeq {
|
|
type Item = RetireSeqEntry;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
Some(self.0.run_one(false))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
struct MockInsns {
|
|
insns: Interned<BTreeMap<u64, MockInsn>>,
|
|
}
|
|
|
|
impl MockInsns {
|
|
fn fetch_block(&self, pc_range: std::ops::Range<u64>) -> impl Iterator<Item = (u64, MockInsn)> {
|
|
self.insns
|
|
.range(pc_range.clone())
|
|
.filter_map(move |(&pc, &insn)| {
|
|
if pc_range.end >= pc + insn.byte_len() {
|
|
Some((pc, insn))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
struct MockInsnsBuilder {
|
|
insns: BTreeMap<u64, MockInsn>,
|
|
labels: BTreeMap<u64, (&'static std::panic::Location<'static>, Option<u64>)>,
|
|
available_labels: Range<u64>,
|
|
next_pc: u64,
|
|
}
|
|
|
|
impl MockInsnsBuilder {
|
|
fn new() -> Self {
|
|
Self {
|
|
insns: BTreeMap::new(),
|
|
labels: BTreeMap::new(),
|
|
available_labels: Self::LABEL_RANGE,
|
|
next_pc: RESET_PC,
|
|
}
|
|
}
|
|
const LABEL_RANGE: Range<u64> = {
|
|
let start = u64::from_be_bytes(*b"Label\0\0\0");
|
|
start..start + 0xFFFFFFu64
|
|
};
|
|
#[track_caller]
|
|
fn new_label(&mut self) -> u64 {
|
|
let Some(label) = self.available_labels.next() else {
|
|
panic!("too many labels");
|
|
};
|
|
self.labels
|
|
.insert(label, (std::panic::Location::caller(), None));
|
|
label
|
|
}
|
|
#[track_caller]
|
|
fn define_label(&mut self, label: u64) {
|
|
let Some((location, address)) = self.labels.get_mut(&label) else {
|
|
panic!("invalid label id: {label:#x}");
|
|
};
|
|
if address.is_some() {
|
|
panic!("label already defined. at: {location}");
|
|
}
|
|
*address = Some(self.next_pc);
|
|
*location = std::panic::Location::caller();
|
|
}
|
|
#[track_caller]
|
|
fn advance_to(&mut self, pc: u64) {
|
|
assert!(self.next_pc < pc);
|
|
self.next_pc = pc;
|
|
}
|
|
#[track_caller]
|
|
fn insn(&mut self, insn: MockInsn) {
|
|
self.insns.insert(self.next_pc, insn);
|
|
self.next_pc = self.next_pc.wrapping_add(insn.byte_len());
|
|
}
|
|
#[track_caller]
|
|
fn finish(mut self) -> MockInsns {
|
|
for insn in self.insns.values_mut() {
|
|
for target in insn.targets_mut() {
|
|
let label = *target;
|
|
if !Self::LABEL_RANGE.contains(&label) {
|
|
continue;
|
|
}
|
|
match self.labels.get(&label) {
|
|
Some((_location, Some(address))) => *target = *address,
|
|
Some((location, None)) => {
|
|
panic!("label not defined. label created at: {location}");
|
|
}
|
|
None => panic!("invalid label id: {label:#x}"),
|
|
}
|
|
}
|
|
}
|
|
MockInsns {
|
|
insns: self.insns.intern_sized(),
|
|
}
|
|
}
|
|
#[track_caller]
|
|
fn call(&mut self, target: u64) {
|
|
self.insn(MockInsn::Call { target });
|
|
}
|
|
#[track_caller]
|
|
fn br_cond(&mut self, target: u64, cond: MockIntCmp, lhs: MockReg, rhs: MockReg) {
|
|
self.insn(MockInsn::BrCond {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
});
|
|
}
|
|
#[track_caller]
|
|
fn br_cond_i(&mut self, target: u64, cond: MockIntCmp, lhs: MockReg, rhs: u64) {
|
|
self.insn(MockInsn::BrCondI {
|
|
target,
|
|
cond,
|
|
lhs,
|
|
rhs,
|
|
});
|
|
}
|
|
#[track_caller]
|
|
fn add_i(&mut self, dest: MockReg, src: MockReg, immediate: u64) {
|
|
self.insn(MockInsn::AddI {
|
|
dest,
|
|
src,
|
|
immediate,
|
|
});
|
|
}
|
|
#[track_caller]
|
|
fn imm(&mut self, dest: MockReg, immediate: u64) {
|
|
self.add_i(dest, MockReg::Zero, immediate);
|
|
}
|
|
#[track_caller]
|
|
fn print_string(&mut self, temp_reg: MockReg, s: &str) {
|
|
for b in s.bytes() {
|
|
self.imm(temp_reg, b as u64);
|
|
self.insn(MockInsn::WriteOutputByte { src: temp_reg });
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mock_program_expr_parser() -> MockInsns {
|
|
let mut b = MockInsnsBuilder::new();
|
|
|
|
let peek_reg = MockReg::R7;
|
|
let expr = b.new_label();
|
|
let mul_div = b.new_label();
|
|
let unary = b.new_label();
|
|
let atom = b.new_label();
|
|
let space = b.new_label();
|
|
let error = b.new_label();
|
|
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.call(expr);
|
|
b.br_cond_i(
|
|
error,
|
|
MockIntCmp::Ne,
|
|
peek_reg,
|
|
MockInsn::READ_INPUT_REG_EOF,
|
|
);
|
|
b.print_string(MockReg::R1, "Success\n");
|
|
b.imm(MockReg::R1, 0);
|
|
b.insn(MockInsn::ExitSysCall);
|
|
|
|
b.advance_to(0x80);
|
|
b.define_label(error);
|
|
b.print_string(MockReg::R1, "Error\n");
|
|
b.imm(MockReg::R1, 1);
|
|
b.insn(MockInsn::ExitSysCall);
|
|
|
|
let not_add = b.new_label();
|
|
let not_sub = b.new_label();
|
|
b.advance_to(0x100);
|
|
b.define_label(expr);
|
|
b.call(mul_div);
|
|
b.br_cond_i(not_add, MockIntCmp::Ne, peek_reg, b'+' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.insn(MockInsn::Jump { target: expr });
|
|
|
|
b.define_label(not_add);
|
|
b.br_cond_i(not_sub, MockIntCmp::Ne, peek_reg, b'-' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.insn(MockInsn::Jump { target: expr });
|
|
|
|
b.define_label(not_sub);
|
|
b.insn(MockInsn::Ret);
|
|
|
|
let not_mul = b.new_label();
|
|
let not_div = b.new_label();
|
|
b.advance_to(0x200);
|
|
b.define_label(mul_div);
|
|
b.call(unary);
|
|
b.br_cond_i(not_mul, MockIntCmp::Ne, peek_reg, b'*' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.insn(MockInsn::Jump { target: expr });
|
|
|
|
b.define_label(not_mul);
|
|
b.br_cond_i(not_div, MockIntCmp::Ne, peek_reg, b'/' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.insn(MockInsn::Jump { target: expr });
|
|
|
|
b.define_label(not_div);
|
|
b.insn(MockInsn::Ret);
|
|
|
|
let not_neg = b.new_label();
|
|
b.advance_to(0x300);
|
|
b.define_label(unary);
|
|
b.br_cond_i(not_neg, MockIntCmp::Ne, peek_reg, b'-' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.insn(MockInsn::Jump { target: unary });
|
|
|
|
b.define_label(not_neg);
|
|
b.call(atom);
|
|
b.insn(MockInsn::Ret);
|
|
|
|
let not_num = b.new_label();
|
|
b.advance_to(0x400);
|
|
b.define_label(atom);
|
|
b.br_cond_i(not_num, MockIntCmp::Ult, peek_reg, b'0' as u64);
|
|
b.br_cond_i(not_num, MockIntCmp::Ugt, peek_reg, b'9' as u64);
|
|
let parse_num = b.new_label();
|
|
b.define_label(parse_num);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.br_cond_i(space, MockIntCmp::Ult, peek_reg, b'0' as u64);
|
|
b.br_cond_i(space, MockIntCmp::Ugt, peek_reg, b'9' as u64);
|
|
b.insn(MockInsn::Jump { target: parse_num });
|
|
|
|
b.define_label(not_num);
|
|
b.br_cond_i(error, MockIntCmp::Ne, peek_reg, b'(' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.call(space);
|
|
b.call(expr);
|
|
b.br_cond_i(error, MockIntCmp::Ne, peek_reg, b')' as u64);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.insn(MockInsn::Jump { target: space });
|
|
|
|
let found_space = b.new_label();
|
|
b.advance_to(0x500);
|
|
b.define_label(space);
|
|
b.br_cond_i(found_space, MockIntCmp::Eq, peek_reg, b' ' as u64);
|
|
b.br_cond_i(found_space, MockIntCmp::Eq, peek_reg, b'\t' as u64);
|
|
b.br_cond_i(found_space, MockIntCmp::Eq, peek_reg, b'\r' as u64);
|
|
b.br_cond_i(found_space, MockIntCmp::Eq, peek_reg, b'\n' as u64);
|
|
b.insn(MockInsn::Ret);
|
|
b.define_label(found_space);
|
|
b.insn(MockInsn::ReadInputByte { dest: peek_reg });
|
|
b.insn(MockInsn::Jump { target: space });
|
|
|
|
b.finish()
|
|
}
|
|
|
|
#[test]
|
|
fn test_program_expr_parser() {
|
|
#[track_caller]
|
|
fn test(expected_output: &str, expected_exit_code: u64, input: &str) {
|
|
println!("starting new test case: input={input:?}\n\n");
|
|
let mut state = MockMachineState::new(mock_program_expr_parser(), input, false);
|
|
let exit_code = state.run_until_exit(10000, true).unwrap();
|
|
println!("output: {:?}", str::from_utf8(&state.output));
|
|
println!("exit code: {exit_code}");
|
|
assert!(expected_output.as_bytes() == state.output);
|
|
assert_eq!(expected_exit_code, exit_code);
|
|
}
|
|
test("Success\n", 0, "123 + 456 * 789");
|
|
test("Success\n", 0, "123");
|
|
test("Success\n", 0, "(123 + 456) * 8 - 9");
|
|
test("Success\n", 0, "-5");
|
|
test("Success\n", 0, "3");
|
|
test("Success\n", 0, TEST_NEXT_PC_INPUT);
|
|
test("Error\n", 1, "3-");
|
|
test("Error\n", 1, "(123 + 456) + ");
|
|
test("Error\n", 1, "(123 + 456");
|
|
test("Error\n", 1, "123 ** 456");
|
|
}
|
|
|
|
const FETCH_PIPE_QUEUE_SIZE: usize = 5;
|
|
|
|
const DEMO_ILLEGAL_INSN_TRAP: u64 = 0xFF000000u64;
|
|
|
|
#[hdl]
|
|
struct FetchPipeQueueEntry {
|
|
start_pc: UInt<64>,
|
|
cycles_left: UInt<8>,
|
|
fetch_block_id: UInt<{ FETCH_BLOCK_ID_WIDTH }>,
|
|
}
|
|
|
|
impl FetchPipeQueueEntry {
|
|
#[hdl]
|
|
fn default_sim(self) -> SimValue<Self> {
|
|
#[hdl(sim)]
|
|
FetchPipeQueueEntry {
|
|
start_pc: 0u64,
|
|
cycles_left: 0u8,
|
|
fetch_block_id: 0u8,
|
|
}
|
|
}
|
|
fn get_next_delay(delay_sequence_index: &Cell<u64>) -> u8 {
|
|
let index = delay_sequence_index.get();
|
|
delay_sequence_index.set(delay_sequence_index.get().wrapping_add(1));
|
|
// make a pseudo-random number deterministically based on index
|
|
let random = index
|
|
.wrapping_add(1)
|
|
.wrapping_mul(0x18C49126EABE7A0D) // random prime
|
|
.rotate_left(32)
|
|
.wrapping_mul(0x92B38C197608A6B) // random prime
|
|
.rotate_right(60);
|
|
if random % 32 == 0 { 30 } else { 3 }
|
|
}
|
|
}
|
|
|
|
#[hdl_module(extern)]
|
|
fn mock_fetch_pipe(config: PhantomConst<CpuConfig>, insns: MockInsns) {
|
|
#[hdl]
|
|
let cd: ClockDomain = m.input();
|
|
#[hdl]
|
|
let from_fetch: NextPcToFetchInterface<PhantomConst<CpuConfig>> =
|
|
m.input(NextPcToFetchInterface[config]);
|
|
#[hdl]
|
|
let to_post_decode: DecodeToPostDecodeInterface<PhantomConst<CpuConfig>> =
|
|
m.output(DecodeToPostDecodeInterface[config]);
|
|
#[hdl]
|
|
let queue_debug: ArrayVec<FetchPipeQueueEntry, ConstUsize<{ FETCH_PIPE_QUEUE_SIZE }>> =
|
|
m.output();
|
|
m.register_clock_for_past(cd.clk);
|
|
m.extern_module_simulation_fn(
|
|
(cd, from_fetch, to_post_decode, queue_debug, insns),
|
|
|(cd, from_fetch, to_post_decode, queue_debug, insns), mut sim| async move {
|
|
// intentionally have a different sequence each time we're reset
|
|
let delay_sequence_index = Cell::new(0);
|
|
sim.resettable(
|
|
cd,
|
|
async |mut sim| {
|
|
sim.write(from_fetch.fetch.ready, false).await;
|
|
sim.write(from_fetch.cancel.ready, false).await;
|
|
sim.write(
|
|
from_fetch.next_fetch_block_ids,
|
|
#[hdl(sim)]
|
|
(from_fetch.next_fetch_block_ids.ty()).HdlNone(),
|
|
)
|
|
.await;
|
|
sim.write(
|
|
to_post_decode.inner.data,
|
|
to_post_decode.ty().inner.data.HdlNone(),
|
|
)
|
|
.await;
|
|
sim.write(
|
|
queue_debug,
|
|
queue_debug.ty().new_sim(FetchPipeQueueEntry.default_sim()),
|
|
)
|
|
.await;
|
|
},
|
|
|sim, ()| {
|
|
run_fn(
|
|
cd,
|
|
from_fetch,
|
|
to_post_decode,
|
|
queue_debug,
|
|
&delay_sequence_index,
|
|
insns,
|
|
sim,
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
},
|
|
);
|
|
#[hdl]
|
|
async fn run_fn(
|
|
cd: Expr<ClockDomain>,
|
|
from_fetch: Expr<NextPcToFetchInterface<PhantomConst<CpuConfig>>>,
|
|
to_post_decode: Expr<DecodeToPostDecodeInterface<PhantomConst<CpuConfig>>>,
|
|
queue_debug: Expr<ArrayVec<FetchPipeQueueEntry, ConstUsize<{ FETCH_PIPE_QUEUE_SIZE }>>>,
|
|
delay_sequence_index: &Cell<u64>,
|
|
mock_insns: MockInsns,
|
|
mut sim: ExternModuleSimulationState,
|
|
) {
|
|
let config = from_fetch.config.ty();
|
|
let mut queue: VecDeque<SimValue<FetchPipeQueueEntry>> = VecDeque::new();
|
|
let mut next_id = 0u32;
|
|
loop {
|
|
let mut sim_queue = queue_debug.ty().new_sim(FetchPipeQueueEntry.default_sim());
|
|
let mut next_fetch_block_ids =
|
|
from_fetch.next_fetch_block_ids.ty().HdlSome.new_sim(0u8);
|
|
for entry in &queue {
|
|
ArrayVec::try_push_sim(&mut sim_queue, entry)
|
|
.ok()
|
|
.expect("queue is known to be small enough");
|
|
let _ = ArrayVec::try_push_sim(&mut next_fetch_block_ids, &entry.fetch_block_id);
|
|
}
|
|
sim.write(queue_debug, sim_queue).await;
|
|
sim.write(
|
|
from_fetch.next_fetch_block_ids,
|
|
#[hdl(sim)]
|
|
(from_fetch.next_fetch_block_ids.ty()).HdlSome(next_fetch_block_ids),
|
|
)
|
|
.await;
|
|
if let Some(front) = queue.front().filter(|v| v.cycles_left.as_int() == 0) {
|
|
#[hdl(sim)]
|
|
let FetchPipeQueueEntry {
|
|
start_pc,
|
|
cycles_left: _,
|
|
fetch_block_id,
|
|
} = front;
|
|
let start_pc = start_pc.as_int();
|
|
let end_pc =
|
|
(start_pc + 1).next_multiple_of(config.get().fetch_width_in_bytes() as u64);
|
|
let insns = to_post_decode.ty().inner.data.HdlSome.insns;
|
|
let zeroed_insn = UInt[insns.element().canonical().bit_width()]
|
|
.zero()
|
|
.cast_bits_to(insns.element());
|
|
let mut insns = insns.new_sim(zeroed_insn);
|
|
let mut expected_pc = start_pc;
|
|
// TODO: handle instructions that go past the end of a fetch block
|
|
for (pc, insn) in mock_insns.fetch_block(start_pc..end_pc) {
|
|
let next_pc = pc + insn.byte_len();
|
|
if pc != expected_pc {
|
|
break;
|
|
}
|
|
expected_pc = next_pc;
|
|
let insn = #[hdl(sim)]
|
|
WipDecodedInsn {
|
|
fetch_block_id,
|
|
id: next_id.cast_to_static::<UInt<_>>(),
|
|
pc,
|
|
predicted_next_pc: 0u64,
|
|
size_in_bytes: insn.byte_len().cast_to_static::<UInt<_>>(),
|
|
kind: insn.wip_decoded_insn_kind(),
|
|
};
|
|
match ArrayVec::try_push_sim(&mut insns, insn) {
|
|
Ok(()) => next_id = next_id.wrapping_add(1),
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
if **ArrayVec::len_sim(&insns) == 0 {
|
|
let Ok(()) = ArrayVec::try_push_sim(
|
|
&mut insns,
|
|
#[hdl(sim)]
|
|
WipDecodedInsn {
|
|
fetch_block_id,
|
|
id: next_id.cast_to_static::<UInt<_>>(),
|
|
pc: start_pc,
|
|
predicted_next_pc: 0u64,
|
|
size_in_bytes: 0u8.cast_to_static::<UInt<_>>(),
|
|
kind: WipDecodedInsnKind.Interrupt(DEMO_ILLEGAL_INSN_TRAP),
|
|
},
|
|
) else {
|
|
unreachable!();
|
|
};
|
|
next_id = next_id.wrapping_add(1);
|
|
}
|
|
sim.write(
|
|
to_post_decode.inner.data,
|
|
HdlSome(
|
|
#[hdl(sim)]
|
|
DecodeToPostDecodeInterfaceInner::<_> { insns, config },
|
|
),
|
|
)
|
|
.await;
|
|
} else {
|
|
sim.write(
|
|
to_post_decode.inner.data,
|
|
to_post_decode.ty().inner.data.HdlNone(),
|
|
)
|
|
.await;
|
|
}
|
|
sim.write(from_fetch.fetch.ready, queue.len() < FETCH_PIPE_QUEUE_SIZE)
|
|
.await;
|
|
sim.write(from_fetch.cancel.ready, true).await;
|
|
sim.wait_for_clock_edge(cd.clk).await;
|
|
println!(
|
|
"Dump mock fetch decode pipe queue: {:#?}",
|
|
Vec::from_iter(queue.iter().map(|v| {
|
|
DebugAsDisplay(format!(
|
|
"fid={:#x} pc={:#x}",
|
|
v.fetch_block_id.as_int(),
|
|
v.start_pc.as_int(),
|
|
))
|
|
}))
|
|
);
|
|
if sim.read_past_bool(to_post_decode.inner.ready, cd.clk).await {
|
|
#[hdl(sim)]
|
|
if let HdlSome(_) = sim.read_past(to_post_decode.inner.data, cd.clk).await {
|
|
let Some(v) = queue.pop_front() else {
|
|
unreachable!();
|
|
};
|
|
println!(
|
|
"mock fetch decode pipe queue pop: fid={:#x} pc={:#x}",
|
|
v.fetch_block_id.as_int(),
|
|
v.start_pc.as_int(),
|
|
);
|
|
}
|
|
}
|
|
for entry in &mut queue {
|
|
if entry.cycles_left.as_int() > 0 {
|
|
entry.cycles_left = (entry.cycles_left.as_int() - 1u8).to_sim_value();
|
|
}
|
|
}
|
|
// handle cancels before pushing new fetch op
|
|
#[hdl(sim)]
|
|
if let HdlSome(in_progress_fetches_to_cancel) =
|
|
sim.read_past(from_fetch.cancel.data, cd.clk).await
|
|
{
|
|
// cancel in-progress fetches from newest to oldest
|
|
for _ in 0..*in_progress_fetches_to_cancel {
|
|
let Some(v) = queue.pop_back() else {
|
|
unreachable!();
|
|
};
|
|
println!(
|
|
"mock fetch decode pipe queue cancel unpush: fid={:#x} pc={:#x}",
|
|
v.fetch_block_id.as_int(),
|
|
v.start_pc.as_int(),
|
|
);
|
|
}
|
|
}
|
|
if !sim.read_past_bool(from_fetch.fetch.ready, cd.clk).await {
|
|
continue;
|
|
}
|
|
// handle pushing new fetch op after handling cancels
|
|
#[hdl(sim)]
|
|
if let HdlSome(inner) = sim.read_past(from_fetch.fetch.data, cd.clk).await {
|
|
#[hdl(sim)]
|
|
let NextPcToFetchInterfaceInner {
|
|
start_pc,
|
|
fetch_block_id,
|
|
} = &inner;
|
|
println!(
|
|
"mock fetch decode pipe queue push: fid={:#x} pc={:#x}",
|
|
fetch_block_id.as_int(),
|
|
start_pc.as_int(),
|
|
);
|
|
queue.push_back(
|
|
#[hdl(sim)]
|
|
FetchPipeQueueEntry {
|
|
start_pc,
|
|
cycles_left: FetchPipeQueueEntry::get_next_delay(delay_sequence_index),
|
|
fetch_block_id,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const EXECUTE_RETIRE_PIPE_QUEUE_SIZE: usize = 15;
|
|
|
|
#[hdl]
|
|
struct ExecuteRetirePipeQueueEntry {
|
|
insn: WipDecodedInsn,
|
|
cycles_left: UInt<8>,
|
|
}
|
|
|
|
impl ExecuteRetirePipeQueueEntry {
|
|
#[hdl]
|
|
fn default_sim(self) -> SimValue<Self> {
|
|
#[hdl(sim)]
|
|
Self {
|
|
insn: #[hdl(sim)]
|
|
WipDecodedInsn {
|
|
fetch_block_id: 0u8,
|
|
id: 0_hdl_u12,
|
|
pc: 0u64,
|
|
predicted_next_pc: 0u64,
|
|
size_in_bytes: 0_hdl_u4,
|
|
kind: WipDecodedInsnKind.NonBranch(),
|
|
},
|
|
cycles_left: 0u8,
|
|
}
|
|
}
|
|
fn get_next_delay(delay_sequence_index: &Cell<u64>) -> u8 {
|
|
let index = delay_sequence_index.get();
|
|
delay_sequence_index.set(delay_sequence_index.get().wrapping_add(1));
|
|
// make a pseudo-random number deterministically based on index
|
|
let random = index
|
|
.wrapping_add(1)
|
|
.wrapping_mul(0x39FF446D8BFB75BB) // random prime
|
|
.rotate_left(32)
|
|
.wrapping_mul(0x73161B54984B1C21) // random prime
|
|
.rotate_right(60);
|
|
const DELAYS: &[u8; 0x20] = &[
|
|
0, 0, 0, 0, 0, 0, 0, 0, //
|
|
1, 1, 1, 1, 1, 1, 1, 1, //
|
|
2, 2, 2, 2, 2, 2, 2, 2, //
|
|
3, 3, 3, 3, 4, 5, 6, 50, // 50 for simulating a cache miss or something
|
|
];
|
|
DELAYS[(random & 0x1F) as usize]
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct MockExecuteState {
|
|
queue: VecDeque<SimValue<ExecuteRetirePipeQueueEntry>>,
|
|
used_ids: BTreeSet<SimValue<UInt<12>>>,
|
|
retire_seq: RetireSeq,
|
|
canceling: bool,
|
|
config: PhantomConst<CpuConfig>,
|
|
}
|
|
|
|
impl MockExecuteState {
|
|
fn new(config: PhantomConst<CpuConfig>, retire_seq: RetireSeq) -> Self {
|
|
Self {
|
|
queue: VecDeque::new(),
|
|
used_ids: BTreeSet::new(),
|
|
retire_seq,
|
|
canceling: false,
|
|
config,
|
|
}
|
|
}
|
|
fn on_clock_cycle(&mut self) {
|
|
for entry in &mut self.queue {
|
|
if entry.cycles_left.as_int() > 0 {
|
|
entry.cycles_left = (entry.cycles_left.as_int() - 1u8).to_sim_value();
|
|
}
|
|
}
|
|
}
|
|
#[hdl]
|
|
fn do_retire(
|
|
&mut self,
|
|
entry: SimValue<ExecuteRetirePipeQueueEntry>,
|
|
passive: bool,
|
|
) -> Result<SimValue<RetireToNextPcInterfacePerInsn<PhantomConst<CpuConfig>>>, String> {
|
|
#[hdl(sim)]
|
|
let ExecuteRetirePipeQueueEntry {
|
|
insn,
|
|
cycles_left: _,
|
|
} = entry;
|
|
self.used_ids.remove(&insn.id);
|
|
let RetireSeqEntry {
|
|
pc,
|
|
cond_br_taken,
|
|
insn: mock_insn,
|
|
} = self
|
|
.retire_seq
|
|
.next()
|
|
.ok_or_else(|| "expected no more instructions to retire")?;
|
|
let next_pc = self.retire_seq.0.pc;
|
|
let expected_insn = #[hdl(sim)]
|
|
WipDecodedInsn {
|
|
fetch_block_id: &insn.fetch_block_id,
|
|
id: &insn.id,
|
|
pc,
|
|
predicted_next_pc: &insn.predicted_next_pc,
|
|
size_in_bytes: mock_insn.byte_len().cast_to_static::<UInt<4>>(),
|
|
kind: mock_insn.wip_decoded_insn_kind(),
|
|
};
|
|
if *expected_insn.cmp_ne(&insn) {
|
|
return Err(format!(
|
|
"insn doesn't match expected:\ninsn: {insn:?}\nexpected insn: {expected_insn:?}"
|
|
));
|
|
}
|
|
if next_pc != insn.predicted_next_pc.as_int() {
|
|
self.canceling = true;
|
|
if !passive {
|
|
println!(
|
|
"MockExecuteState: starting canceling {} instruction(s): next_pc={next_pc:#x}, mis-predicted next_pc={}",
|
|
self.queue.len(),
|
|
insn.predicted_next_pc
|
|
);
|
|
}
|
|
}
|
|
Ok(
|
|
#[hdl(sim)]
|
|
RetireToNextPcInterfacePerInsn::<_> {
|
|
id: &insn.id,
|
|
next_pc,
|
|
call_stack_op: mock_insn.call_stack_op(pc),
|
|
cond_br_taken: if let Some(cond_br_taken) = cond_br_taken {
|
|
#[hdl(sim)]
|
|
HdlSome(cond_br_taken)
|
|
} else {
|
|
#[hdl(sim)]
|
|
HdlNone()
|
|
},
|
|
config: self.config,
|
|
},
|
|
)
|
|
}
|
|
#[hdl]
|
|
fn try_retire(
|
|
&mut self,
|
|
passive: bool,
|
|
) -> Option<(
|
|
SimValue<RetireToNextPcInterfacePerInsn<PhantomConst<CpuConfig>>>,
|
|
Result<(), String>,
|
|
)> {
|
|
if self.canceling {
|
|
return None;
|
|
}
|
|
if self.queue.front()?.cycles_left.as_int() != 0 {
|
|
return None;
|
|
}
|
|
let entry = self.queue.pop_front()?;
|
|
let id = entry.insn.id.clone();
|
|
Some(match self.do_retire(entry, passive) {
|
|
Ok(v) => (v, Ok(())),
|
|
Err(e) => (
|
|
#[hdl(sim)]
|
|
RetireToNextPcInterfacePerInsn::<_> {
|
|
id,
|
|
next_pc: u64::from_be_bytes(*b"ErrError"),
|
|
call_stack_op: #[hdl(sim)]
|
|
CallStackOp::None(),
|
|
cond_br_taken: #[hdl(sim)]
|
|
HdlNone(),
|
|
config: self.config,
|
|
},
|
|
Err(e),
|
|
),
|
|
})
|
|
}
|
|
fn space_available(&self) -> usize {
|
|
EXECUTE_RETIRE_PIPE_QUEUE_SIZE.saturating_sub(self.queue.len())
|
|
}
|
|
#[hdl]
|
|
fn start(&mut self, insn: &SimValue<WipDecodedInsn>, delay_sequence_index: &Cell<u64>) {
|
|
if !self.used_ids.insert(insn.id.clone()) {
|
|
panic!("next_pc gave a duplicate insn id: {insn:?}");
|
|
}
|
|
println!(
|
|
"MockExecutionState::start fid={} id={} pc={}",
|
|
insn.fetch_block_id, insn.id, insn.pc
|
|
);
|
|
self.queue.push_back(
|
|
#[hdl(sim)]
|
|
ExecuteRetirePipeQueueEntry {
|
|
insn,
|
|
cycles_left: ExecuteRetirePipeQueueEntry::get_next_delay(delay_sequence_index),
|
|
},
|
|
);
|
|
}
|
|
#[hdl]
|
|
fn finish_cancel(&mut self) {
|
|
println!(
|
|
"MockExecuteState: finishing canceling {} instruction(s)",
|
|
self.queue.len(),
|
|
);
|
|
self.queue.clear();
|
|
self.used_ids.clear();
|
|
self.canceling = false;
|
|
}
|
|
}
|
|
|
|
#[hdl_module(extern)]
|
|
fn mock_execute_retire_pipe(
|
|
config: PhantomConst<CpuConfig>,
|
|
mock_insns: MockInsns,
|
|
mock_input: Interned<str>,
|
|
) {
|
|
#[hdl]
|
|
let cd: ClockDomain = m.input();
|
|
#[hdl]
|
|
let from_post_decode: PostDecodeOutputInterface<PhantomConst<CpuConfig>> =
|
|
m.input(PostDecodeOutputInterface[config]);
|
|
#[hdl]
|
|
let retire_output: RetireToNextPcInterface<PhantomConst<CpuConfig>> =
|
|
m.output(RetireToNextPcInterface[config]);
|
|
#[hdl]
|
|
let queue_debug: ArrayVec<
|
|
ExecuteRetirePipeQueueEntry,
|
|
ConstUsize<{ EXECUTE_RETIRE_PIPE_QUEUE_SIZE }>,
|
|
> = m.output();
|
|
m.register_clock_for_past(cd.clk);
|
|
m.extern_module_simulation_fn(
|
|
(
|
|
cd,
|
|
from_post_decode,
|
|
retire_output,
|
|
queue_debug,
|
|
mock_insns,
|
|
mock_input,
|
|
),
|
|
|args, mut sim| async move {
|
|
let (cd, from_post_decode, retire_output, queue_debug, mock_insns, mock_input) = args;
|
|
// intentionally have a different sequence each time we're reset
|
|
let delay_sequence_index = Cell::new(0);
|
|
sim.resettable(
|
|
cd,
|
|
async |mut sim| {
|
|
sim.write(from_post_decode.ready, 0usize).await;
|
|
sim.write(from_post_decode.cancel.ready, false).await;
|
|
sim.write(
|
|
retire_output.inner.data,
|
|
retire_output.ty().inner.data.HdlNone(),
|
|
)
|
|
.await;
|
|
sim.write(
|
|
retire_output.next_insns,
|
|
retire_output.next_insns.ty().HdlNone(),
|
|
)
|
|
.await;
|
|
sim.write(
|
|
queue_debug,
|
|
queue_debug
|
|
.ty()
|
|
.new_sim(ExecuteRetirePipeQueueEntry.default_sim()),
|
|
)
|
|
.await;
|
|
},
|
|
|sim, ()| {
|
|
run_fn(
|
|
cd,
|
|
from_post_decode,
|
|
retire_output,
|
|
queue_debug,
|
|
&delay_sequence_index,
|
|
mock_insns,
|
|
mock_input,
|
|
sim,
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
},
|
|
);
|
|
#[hdl]
|
|
async fn run_fn(
|
|
cd: Expr<ClockDomain>,
|
|
from_post_decode: Expr<PostDecodeOutputInterface<PhantomConst<CpuConfig>>>,
|
|
retire_output: Expr<RetireToNextPcInterface<PhantomConst<CpuConfig>>>,
|
|
queue_debug: Expr<
|
|
ArrayVec<ExecuteRetirePipeQueueEntry, ConstUsize<{ EXECUTE_RETIRE_PIPE_QUEUE_SIZE }>>,
|
|
>,
|
|
delay_sequence_index: &Cell<u64>,
|
|
mock_insns: MockInsns,
|
|
mock_input: Interned<str>,
|
|
mut sim: ExternModuleSimulationState,
|
|
) {
|
|
let config = from_post_decode.config.ty();
|
|
let mut state = MockExecuteState::new(
|
|
config,
|
|
RetireSeq::new(MockMachineState::new(mock_insns, mock_input, true)),
|
|
);
|
|
let empty_retire_insn = #[hdl(sim)]
|
|
RetireToNextPcInterfacePerInsn::<_> {
|
|
id: 0_hdl_u12,
|
|
next_pc: 0u64,
|
|
call_stack_op: #[hdl(sim)]
|
|
CallStackOp::None(),
|
|
cond_br_taken: #[hdl(sim)]
|
|
HdlNone(),
|
|
config,
|
|
};
|
|
let retire_vec_ty = retire_output.inner.data.ty().HdlSome.insns;
|
|
loop {
|
|
state.on_clock_cycle();
|
|
let mut sim_queue = queue_debug
|
|
.ty()
|
|
.new_sim(ExecuteRetirePipeQueueEntry.default_sim());
|
|
let mut next_insns = retire_output.next_insns.ty().HdlSome.new_sim(
|
|
#[hdl(sim)]
|
|
WipDecodedInsn {
|
|
fetch_block_id: 0u8,
|
|
id: 0_hdl_u12,
|
|
pc: 0u64,
|
|
predicted_next_pc: 0u64,
|
|
size_in_bytes: 0_hdl_u4,
|
|
kind: WipDecodedInsnKind.NonBranch(),
|
|
},
|
|
);
|
|
for entry in &state.queue {
|
|
ArrayVec::try_push_sim(&mut sim_queue, entry)
|
|
.ok()
|
|
.expect("queue is known to be small enough");
|
|
let _ = ArrayVec::try_push_sim(&mut next_insns, &entry.insn);
|
|
}
|
|
sim.write(queue_debug, sim_queue).await;
|
|
sim.write(
|
|
retire_output.next_insns,
|
|
if state.canceling {
|
|
#[hdl(sim)]
|
|
(retire_output.next_insns.ty()).HdlNone()
|
|
} else {
|
|
#[hdl(sim)]
|
|
(retire_output.next_insns.ty()).HdlSome(next_insns)
|
|
},
|
|
)
|
|
.await;
|
|
let mut retiring = retire_vec_ty.new_sim(&empty_retire_insn);
|
|
let mut peek_state = state.clone();
|
|
while let Some((peek_retire, result)) = peek_state.try_retire(true) {
|
|
if result.is_err() && **ArrayVec::len_sim(&retiring) > 0 {
|
|
break;
|
|
}
|
|
let Ok(_) = ArrayVec::try_push_sim(&mut retiring, peek_retire) else {
|
|
break;
|
|
};
|
|
}
|
|
sim.write(
|
|
retire_output.inner.data,
|
|
if **ArrayVec::len_sim(&retiring) > 0 {
|
|
#[hdl(sim)]
|
|
(retire_output.inner.data.ty()).HdlSome(
|
|
#[hdl(sim)]
|
|
RetireToNextPcInterfaceInner::<_> {
|
|
insns: retiring,
|
|
config,
|
|
},
|
|
)
|
|
} else {
|
|
#[hdl(sim)]
|
|
(retire_output.inner.data.ty()).HdlNone()
|
|
},
|
|
)
|
|
.await;
|
|
sim.write(
|
|
from_post_decode.ready,
|
|
if state.canceling {
|
|
0
|
|
} else {
|
|
state.space_available().min(config.get().fetch_width.get())
|
|
},
|
|
)
|
|
.await;
|
|
sim.write(from_post_decode.cancel.ready, state.canceling)
|
|
.await;
|
|
sim.wait_for_clock_edge(cd.clk).await;
|
|
println!(
|
|
"Dump mock execute retire pipe queue: {:#?}",
|
|
Vec::from_iter(state.queue.iter().map(|v| {
|
|
DebugAsDisplay(format!(
|
|
"fid={:#x} id={} pc={:#x}",
|
|
v.insn.fetch_block_id.as_int(),
|
|
v.insn.id,
|
|
v.insn.pc.as_int(),
|
|
))
|
|
}))
|
|
);
|
|
#[hdl(sim)]
|
|
if let HdlSome(v) = sim.read_past(from_post_decode.cancel.data, cd.clk).await {
|
|
#[hdl(sim)]
|
|
let () = v;
|
|
if sim
|
|
.read_past_bool(from_post_decode.cancel.ready, cd.clk)
|
|
.await
|
|
{
|
|
assert!(state.canceling);
|
|
state.finish_cancel();
|
|
}
|
|
}
|
|
if sim.read_past_bool(retire_output.inner.ready, cd.clk).await {
|
|
for _ in 0..**ArrayVec::len_sim(&retiring) {
|
|
match state.try_retire(false) {
|
|
Some((_, Ok(_))) => {}
|
|
Some((_, Err(e))) => panic!("retire error: {e}"),
|
|
None => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
let mut new_insns = sim.read_past(from_post_decode.insns, cd.clk).await;
|
|
ArrayVec::truncate_sim(
|
|
&mut new_insns,
|
|
*sim.read_past(from_post_decode.ready, cd.clk).await,
|
|
);
|
|
for insn in ArrayVec::elements_sim_ref(&new_insns) {
|
|
state.start(insn, delay_sequence_index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[hdl_module]
|
|
fn dut(config: PhantomConst<CpuConfig>, mock_insns: MockInsns, mock_input: Interned<str>) {
|
|
#[hdl]
|
|
let cd: ClockDomain = m.input();
|
|
#[hdl]
|
|
let next_pc = instance(next_pc(config));
|
|
#[hdl]
|
|
let next_pc {
|
|
cd: next_pc_cd,
|
|
to_fetch: next_pc_to_fetch,
|
|
from_decode: next_pc_from_decode,
|
|
post_decode_output: next_pc_post_decode_output,
|
|
from_retire: next_pc_from_retire,
|
|
state_for_debug: _,
|
|
} = next_pc;
|
|
connect(next_pc_cd, cd);
|
|
#[hdl]
|
|
let mock_fetch_pipe = instance(mock_fetch_pipe(config, mock_insns));
|
|
#[hdl]
|
|
let mock_fetch_pipe {
|
|
cd: mock_fetch_pipe_cd,
|
|
from_fetch: mock_fetch_pipe_from_fetch,
|
|
to_post_decode: mock_fetch_pipe_to_post_decode,
|
|
queue_debug: _,
|
|
} = mock_fetch_pipe;
|
|
connect(mock_fetch_pipe_cd, cd);
|
|
connect(mock_fetch_pipe_from_fetch, next_pc_to_fetch);
|
|
connect(next_pc_from_decode, mock_fetch_pipe_to_post_decode);
|
|
#[hdl]
|
|
let mock_execute_retire_pipe =
|
|
instance(mock_execute_retire_pipe(config, mock_insns, mock_input));
|
|
#[hdl]
|
|
let mock_execute_retire_pipe {
|
|
cd: mock_execute_retire_pipe_cd,
|
|
from_post_decode: mock_execute_retire_pipe_from_post_decode,
|
|
retire_output: mock_execute_retire_pipe_retire_output,
|
|
queue_debug: _,
|
|
} = mock_execute_retire_pipe;
|
|
connect(mock_execute_retire_pipe_cd, cd);
|
|
connect(next_pc_from_retire, mock_execute_retire_pipe_retire_output);
|
|
connect(
|
|
mock_execute_retire_pipe_from_post_decode,
|
|
next_pc_post_decode_output,
|
|
);
|
|
}
|
|
|
|
const TEST_NEXT_PC_INPUT: &str = "(123 + -(456 + 3)) * 7 / 5 + 3";
|
|
|
|
#[hdl]
|
|
#[test]
|
|
fn test_next_pc() {
|
|
let _n = SourceLocation::normalize_files_for_tests();
|
|
let mut config = CpuConfig::new(
|
|
vec![
|
|
UnitConfig::new(UnitKind::AluBranch),
|
|
UnitConfig::new(UnitKind::AluBranch),
|
|
],
|
|
NonZeroUsize::new(20).unwrap(),
|
|
);
|
|
config.fetch_width = NonZeroUsize::new(2).unwrap();
|
|
let m = dut(
|
|
PhantomConst::new_sized(config),
|
|
mock_program_expr_parser(),
|
|
TEST_NEXT_PC_INPUT.intern(),
|
|
);
|
|
let mut sim = Simulation::new(m);
|
|
let writer = RcWriter::default();
|
|
sim.add_trace_writer(VcdWriterDecls::new(writer.clone()));
|
|
struct DumpVcdOnDrop {
|
|
writer: Option<RcWriter>,
|
|
}
|
|
impl Drop for DumpVcdOnDrop {
|
|
fn drop(&mut self) {
|
|
if let Some(mut writer) = self.writer.take() {
|
|
let vcd = String::from_utf8(writer.take()).unwrap();
|
|
println!("####### VCD:\n{vcd}\n#######");
|
|
}
|
|
}
|
|
}
|
|
let mut writer = DumpVcdOnDrop {
|
|
writer: Some(writer),
|
|
};
|
|
sim.write_clock(sim.io().cd.clk, false);
|
|
sim.write_reset(sim.io().cd.rst, true);
|
|
for cycle in 0..2000 {
|
|
sim.advance_time(SimDuration::from_nanos(500));
|
|
println!("clock tick: {cycle}");
|
|
sim.write_clock(sim.io().cd.clk, true);
|
|
sim.advance_time(SimDuration::from_nanos(500));
|
|
sim.write_clock(sim.io().cd.clk, false);
|
|
sim.write_reset(sim.io().cd.rst, false);
|
|
}
|
|
// FIXME: vcd is just whatever next_pc does now, which isn't known to be correct
|
|
let vcd = String::from_utf8(writer.writer.take().unwrap().take()).unwrap();
|
|
println!("####### VCD:\n{vcd}\n#######");
|
|
if vcd != include_str!("expected/next_pc.vcd") {
|
|
panic!();
|
|
}
|
|
}
|