use qcode::value::LocalInsnId;
use qcode::{
address_index::{AddressIndex, AddressTarget},
context::Context,
value::{BasicBlock, BlockId},
};
use qcode_emulator::{EmulatorErrorKind, EmulatorMemory, StandaloneEmulator};
use rustc_hash::FxHashSet;
use crate::{memory::VmMemory, mmu::MemFault, stats::Stats};
#[derive(Debug, Clone)]
pub enum CodeError {
Fault(MemFault),
Decode(Box<str>),
}
pub trait CodeSource {
fn lift(
&mut self,
ctx: &mut Context<'static>,
memory: &VmMemory,
index: &mut AddressIndex,
addr: u64,
stats: &mut Stats,
) -> Result<(), CodeError>;
}
pub trait BlockExecutor {
fn run_block(
&mut self,
ctx: &Context<'_>,
emu: &mut StandaloneEmulator<VmMemory>,
block: BlockId,
chain: bool,
) -> Result<Option<Executed>, EmulatorErrorKind>;
}
#[derive(Debug, Clone, Copy)]
pub struct Executed {
pub block: BlockId,
pub body: usize,
pub retired: u64,
}
#[derive(Debug, Clone)]
pub enum VmExit {
InstructionLimit,
Breakpoint(u64),
Fault(MemFault),
Unlifted { addr: u64, error: CodeError },
Error(Box<str>),
}
pub struct Vm<S> {
ctx: Context<'static>,
emu: StandaloneEmulator<VmMemory>,
source: S,
pub stats: Stats,
executor: Option<Box<dyn BlockExecutor>>,
absorbed_into: Option<BlockId>,
pub optimize: bool,
dirty: Option<BlockId>,
breakpoints: FxHashSet<u64>,
}
impl<S: CodeSource> Vm<S> {
pub fn new(ctx: Context<'static>, entry: BlockId, source: S) -> Self {
let mut emu = StandaloneEmulator::<VmMemory>::new_in(entry);
emu.memory.configure_spaces(&ctx);
Self {
ctx,
emu,
source,
optimize: true,
stats: Stats::default(),
executor: None,
absorbed_into: None,
dirty: None,
breakpoints: FxHashSet::default(),
}
}
pub fn at_address(
mut ctx: Context<'static>,
addr: u64,
mut source: S,
memory: VmMemory,
) -> Result<Self, CodeError> {
let mut index = AddressIndex::analyze(&ctx);
let mut stats = Stats::default();
if resolve(&ctx, &index, addr).is_none() {
stats.lifts += 1;
source.lift(&mut ctx, &memory, &mut index, addr, &mut stats)?;
}
let entry = resolve(&ctx, &index, addr).ok_or_else(|| {
CodeError::Decode(format!("no block at {addr:#x} after lifting").into())
})?;
let mut vm = Self::new(ctx, entry, source);
vm.emu.memory = memory;
vm.emu.memory.configure_spaces(&vm.ctx);
vm.emu.set_address_index(index);
vm.stats = stats;
Ok(vm)
}
pub fn set_block_executor(&mut self, executor: Box<dyn BlockExecutor>) {
self.executor = Some(executor);
}
pub fn clear_block_executor(&mut self) {
self.executor = None;
}
pub fn context(&self) -> &Context<'static> {
&self.ctx
}
pub fn memory(&self) -> &VmMemory {
&self.emu.memory
}
pub fn memory_mut(&mut self) -> &mut VmMemory {
&mut self.emu.memory
}
pub fn emulator(&mut self) -> &mut StandaloneEmulator<VmMemory> {
&mut self.emu
}
pub fn pc(&self) -> Option<u64> {
BasicBlock::from_id(&self.ctx, self.emu.block).address()
}
pub fn add_breakpoint(&mut self, addr: u64) -> bool {
self.breakpoints.insert(addr)
}
pub fn remove_breakpoint(&mut self, addr: u64) -> bool {
self.breakpoints.remove(&addr)
}
pub fn step(&mut self) -> Option<VmExit> {
for attempt in 0..2 {
if self.emu.idx == 0 {
let block = self.emu.block;
self.clean_before_entering(block);
}
if self.emu.idx == 0
&& let Some(executor) = self.executor.as_mut()
{
let block = self.emu.block;
let chain = self.breakpoints.is_empty();
match executor.run_block(&self.ctx, &mut self.emu, block, chain) {
Ok(Some(run)) => {
self.stats.steps += run.retired;
self.stats.native_bodies += 1;
self.emu.invalidate_block_cache();
self.emu.block = run.block;
self.emu.idx = run.body;
}
Ok(None) => {}
Err(kind) => {
let fault = self.emu.memory.take_fault();
return Some(match fault {
Some(fault) => VmExit::Fault(fault),
None => VmExit::Error(kind.to_string().into()),
});
}
}
}
match self.emu.step(&self.ctx) {
Ok(()) => {
self.stats.steps += 1;
return None;
}
Err(error) => match error.kind {
EmulatorErrorKind::InvalidBlockAddress(addr)
| EmulatorErrorKind::UnknownAddress(addr)
if attempt == 0 =>
{
if let Some(exit) = self.discover(addr) {
return Some(exit);
}
}
EmulatorErrorKind::EmptyBlock(block) if attempt == 0 => {
let Some(addr) = BasicBlock::from_id(&self.ctx, block).address() else {
return Some(VmExit::Error(
EmulatorErrorKind::EmptyBlock(block).to_string().into(),
));
};
let before = self.emu.block;
self.reposition(addr);
if self.emu.block != before {
self.stats.resolves += 1;
continue;
}
if let Some(exit) = self.discover(addr) {
return Some(exit);
}
if self.absorbed_into.take().is_none() {
self.reposition(addr);
}
}
EmulatorErrorKind::MemoryReadError(addr)
| EmulatorErrorKind::MemoryWriteError(addr) => {
let fault = self.emu.memory.take_fault().unwrap_or(MemFault {
kind: crate::mmu::FaultKind::ReadUnmapped,
addr,
});
return Some(VmExit::Fault(fault));
}
kind => return Some(VmExit::Error(kind.to_string().into())),
},
}
}
None
}
fn reposition(&mut self, addr: u64) {
if let Some(block) = self.emu.block_at_address(&self.ctx, addr)
&& block != self.emu.block
{
self.emu.block = block;
self.emu.idx = 0;
}
}
fn discover(&mut self, addr: u64) -> Option<VmExit> {
let mut index = self
.emu
.take_address_index()
.unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
self.stats.lifts += 1;
let result = self.source.lift(
&mut self.ctx,
&self.emu.memory,
&mut index,
addr,
&mut self.stats,
);
self.emu.set_address_index(index);
if let Err(error) = result {
return Some(VmExit::Unlifted { addr, error });
}
if self.optimize
&& let Some(block) = self.emu.block_at_address(&self.ctx, addr)
{
let started = std::time::Instant::now();
let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
qcode_passes::remove_dead_insns(&mut self.ctx, block);
self.stats.optimize += started.elapsed();
self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
self.stats.removed_stores += cleanup.removed_stores as u64;
}
self.absorbed_into = self.absorb_into_basic_block(addr);
None
}
fn reindex_absorbed(&mut self, block: BlockId) {
let covered = self.ctx.block(block).extra_addresses.clone();
if covered.is_empty() {
return;
}
let mut index = self
.emu
.take_address_index()
.unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
for addr in covered {
index.set_block(addr, block);
}
self.emu.set_address_index(index);
}
fn mark_dirty(&mut self, block: BlockId) {
if !self.optimize {
return;
}
let previous = self.dirty.replace(block);
if let Some(previous) = previous
&& previous != block
&& self.ctx.contains_block(previous)
{
self.reoptimize(previous);
}
}
fn clean_before_entering(&mut self, block: BlockId) {
if self.dirty == Some(block) {
self.dirty = None;
self.reoptimize(block);
}
}
fn reoptimize(&mut self, block: BlockId) {
if !self.optimize {
return;
}
let started = std::time::Instant::now();
let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
qcode_passes::remove_dead_insns(&mut self.ctx, block);
self.stats.optimize += started.elapsed();
self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
self.stats.removed_stores += cleanup.removed_stores as u64;
self.emu.invalidate_block_cache();
}
fn absorb_into_basic_block(&mut self, addr: u64) -> Option<BlockId> {
let filled = self.emu.block_at_address(&self.ctx, addr)?;
let forward = qcode_passes::absorb_straight_line(&mut self.ctx, filled);
self.stats.absorbed += forward as u64;
if forward > 0 {
self.reindex_absorbed(filled);
self.mark_dirty(filled);
}
if self
.emu
.address_index()
.is_some_and(|index| index.is_boundary(addr))
{
return (forward > 0).then_some(filled);
}
let preds: Vec<BlockId> = BasicBlock::from_id(&self.ctx, filled)
.predecessors()
.map(|(_, block)| block)
.take(2)
.collect();
let [head] = preds[..] else {
return None;
};
if head == filled {
return None;
}
if self.ctx.block(head).address.is_none() {
return (forward > 0).then_some(filled);
}
let offset = self
.ctx
.block(head)
.instruction_ids()
.len()
.saturating_sub(1);
if qcode_passes::absorb_straight_line(&mut self.ctx, head) == 0 {
return (forward > 0).then_some(filled);
}
self.stats.absorbed += 1;
let resume: Vec<LocalInsnId> = self.ctx.block(head).instruction_ids()[offset..].to_vec();
self.mark_dirty(head);
self.reindex_absorbed(head);
if self.emu.block == filled {
let now = self.ctx.block(head).instruction_ids();
let resumed = resume
.iter()
.find_map(|wanted| now.iter().position(|have| have == wanted))
.unwrap_or(now.len().saturating_sub(1));
self.emu.block = head;
self.emu.idx = resumed;
self.emu.invalidate_block_cache();
}
Some(head)
}
pub fn run(&mut self, budget: u64) -> VmExit {
let deadline = self.stats.steps + budget;
while self.stats.steps < deadline {
if !self.breakpoints.is_empty()
&& let Some(pc) = self.pc()
&& self.breakpoints.contains(&pc)
&& self.stats.steps > 0
{
return VmExit::Breakpoint(pc);
}
if let Some(exit) = self.step() {
return exit;
}
}
VmExit::InstructionLimit
}
}
fn resolve(ctx: &Context<'_>, index: &AddressIndex, addr: u64) -> Option<BlockId> {
match index.get(addr) {
Some(AddressTarget::Block(block)) => Some(block),
Some(AddressTarget::Function(function)) => {
qcode::value::FunctionBody::from_id(ctx, function)
.root()
.map(|root| root.id)
}
None => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mmu::{PAGE_SIZE, perm};
use qcode::value::FunctionBody;
#[derive(Default)]
struct Planned {
available: Vec<u64>,
pub calls: Vec<u64>,
}
impl CodeSource for Planned {
fn lift(
&mut self,
ctx: &mut Context<'static>,
memory: &VmMemory,
index: &mut AddressIndex,
addr: u64,
_stats: &mut Stats,
) -> Result<(), CodeError> {
self.calls.push(addr);
let mut byte = [0u8; 1];
memory
.mmu
.read_code(addr, &mut byte)
.map_err(CodeError::Fault)?;
if !self.available.contains(&addr) {
return Err(CodeError::Decode("no plan for this address".into()));
}
let function = FunctionBody::make_at_addr(ctx, addr, None).id;
let block = BasicBlock::make(ctx, function).with_address(addr).id;
index
.register(ctx, addr, AddressTarget::Block(block))
.map_err(|error| CodeError::Decode(format!("{error:?}").into()))?;
Ok(())
}
}
fn module(addr: u64) -> (Context<'static>, BlockId) {
let mut ctx = Context::new();
let function = FunctionBody::make_at_addr(&mut ctx, addr, None).id;
let block = BasicBlock::make(&mut ctx, function).with_address(addr).id;
(ctx, block)
}
fn executable_memory() -> VmMemory {
let mut memory = VmMemory::new();
memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
memory
}
#[test]
fn an_empty_addressed_block_asks_the_source_for_code() {
let (ctx, block) = module(0x1000);
let mut vm = Vm::new(ctx, block, Planned::default());
let exit = vm.run(16);
assert!(
matches!(exit, VmExit::Unlifted { addr: 0x1000, .. }),
"expected a discovery attempt, got {exit:?}"
);
assert_eq!(vm.source.calls, vec![0x1000]);
}
#[test]
fn an_empty_block_with_no_address_is_an_error() {
let mut ctx = Context::new();
let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
let block = BasicBlock::make(&mut ctx, function).id;
let mut vm = Vm::new(ctx, block, Planned::default());
assert!(matches!(vm.run(16), VmExit::Error(_)));
}
#[test]
fn pc_reports_the_block_about_to_run() {
let (ctx, block) = module(0x1000);
let vm = Vm::new(ctx, block, Planned::default());
assert_eq!(vm.pc(), Some(0x1000));
}
#[test]
fn at_address_lifts_an_entry_the_module_lacks() {
let ctx = Context::new();
let source = Planned {
available: vec![0x1000],
calls: Vec::new(),
};
let vm = Vm::at_address(ctx, 0x1000, source, executable_memory())
.expect("the source can supply this address");
assert_eq!(vm.pc(), Some(0x1000));
}
#[test]
fn at_address_reuses_a_block_the_module_already_has() {
let (ctx, _) = module(0x1000);
let vm = Vm::at_address(ctx, 0x1000, Planned::default(), executable_memory())
.expect("no lifting is needed");
assert_eq!(vm.pc(), Some(0x1000));
assert!(vm.source.calls.is_empty());
}
#[test]
fn fetching_from_non_executable_memory_reports_the_fault() {
let ctx = Context::new();
let mut memory = VmMemory::new();
memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
let source = Planned {
available: vec![0x1000],
calls: Vec::new(),
};
let error = Vm::at_address(ctx, 0x1000, source, memory)
.err()
.expect("the page is not executable");
assert!(matches!(
error,
CodeError::Fault(MemFault {
kind: crate::mmu::FaultKind::ExecViolation,
addr: 0x1000
})
));
}
#[test]
fn an_unsuppliable_address_reports_where_it_stopped() {
let ctx = Context::new();
let error = Vm::at_address(ctx, 0x2000, Planned::default(), executable_memory())
.err()
.expect("nothing is mapped or planned at 0x2000");
assert!(matches!(error, CodeError::Fault(_)));
}
#[test]
fn breakpoints_are_recorded_and_removable() {
let (ctx, block) = module(0x1000);
let mut vm = Vm::new(ctx, block, Planned::default());
assert!(vm.add_breakpoint(0x2000));
assert!(!vm.add_breakpoint(0x2000));
assert!(vm.remove_breakpoint(0x2000));
assert!(!vm.remove_breakpoint(0x2000));
}
#[test]
fn memory_is_reachable_and_backed_by_the_mmu() {
let (ctx, block) = module(0x1000);
let mut vm = Vm::new(ctx, block, Planned::default());
vm.memory_mut()
.mmu
.map(0x4000, PAGE_SIZE, perm::RW_INIT)
.unwrap();
vm.memory_mut().mmu.write(0x4000, &[1, 2, 3]).unwrap();
let mut out = [0; 3];
vm.memory().mmu.read(0x4000, &mut out).unwrap();
assert_eq!(out, [1, 2, 3]);
}
}