use qcode::{
context::Context,
space::{MemorySpaceId, Space, SpaceType},
value::{
BasicBlock, BlockId, Instruction, ValueId, ValueRef,
insn::{InstructionId, Load, Mnemonic, Store},
},
};
use rustc_hash::{FxHashMap, FxHashSet};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cleanup {
pub forwarded_loads: usize,
pub removed_stores: usize,
}
impl Cleanup {
pub fn is_empty(&self) -> bool {
self.forwarded_loads == 0 && self.removed_stores == 0
}
}
fn is_temporary(ctx: &Context<'_>, space: MemorySpaceId) -> bool {
match space {
MemorySpaceId::Temp(_) => true,
MemorySpaceId::Shared(id) => {
matches!(Space::from_id(ctx, id).ty, SpaceType::Unique)
}
}
}
fn constant_address(ctx: &Context<'_>, ptr: ValueId) -> Option<u64> {
match ValueRef::new(ptr, ctx) {
ValueRef::Literal(literal) => Some(literal.value()),
ValueRef::Temp(temp) => Some(temp.address() as u64),
ValueRef::Varnode(varnode) => Some(varnode.address() as u64),
_ => None,
}
}
pub fn forward_temp_stores(ctx: &mut Context<'_>, block_id: BlockId) -> Cleanup {
let func = block_id.func;
let insn_ids: Vec<InstructionId> = BasicBlock::from_id(ctx, block_id).instruction_ids();
enum Access {
Store {
space: qcode::space::LocalMemorySpaceId,
ptr: qcode::value::LocalValueId,
size: usize,
src: qcode::value::LocalValueId,
},
Load {
space: qcode::space::LocalMemorySpaceId,
ptr: qcode::value::LocalValueId,
size: usize,
},
}
struct Stored {
value: ValueId,
size: usize,
store: InstructionId,
}
let mut available: FxHashMap<(MemorySpaceId, u64), Stored> = FxHashMap::default();
let mut redundant: FxHashSet<(InstructionId, (MemorySpaceId, u64))> = FxHashSet::default();
let mut poisoned: FxHashSet<MemorySpaceId> = FxHashSet::default();
let mut read_otherwise: FxHashSet<(MemorySpaceId, u64)> = FxHashSet::default();
let mut consumed: FxHashSet<InstructionId> = FxHashSet::default();
let mut forwards: Vec<(ValueId, ValueId)> = Vec::new();
for &insn_id in &insn_ids {
let accessed = match *Instruction::from_id(ctx, insn_id).mnemonic() {
Mnemonic::Store(Store {
space,
ptr,
size,
src,
}) => Access::Store {
space,
ptr,
size,
src,
},
Mnemonic::Load(Load { space, ptr, size }) => Access::Load { space, ptr, size },
_ => continue,
};
match accessed {
Access::Store {
space,
ptr,
size,
src,
} => {
let space = space.qualify(func);
if !is_temporary(ctx, space) {
continue;
}
match constant_address(ctx, ptr.qualify(func)) {
Some(addr) => {
available.insert(
(space, addr),
Stored {
value: src.qualify(func),
size,
store: insn_id,
},
);
}
None => {
poisoned.insert(space);
available.retain(|(other, _), _| *other != space);
}
}
}
Access::Load { space, ptr, size } => {
let space = space.qualify(func);
if !is_temporary(ctx, space) {
continue;
}
let Some(addr) = constant_address(ctx, ptr.qualify(func)) else {
poisoned.insert(space);
available.retain(|(other, _), _| *other != space);
continue;
};
match available.get(&(space, addr)) {
Some(stored) if stored.size == size => {
forwards.push((ValueId::Instruction(insn_id), stored.value));
consumed.insert(insn_id);
redundant.insert((stored.store, (space, addr)));
}
Some(_) => {
read_otherwise.insert((space, addr));
available.remove(&(space, addr));
}
None => {
read_otherwise.insert((space, addr));
}
}
}
}
}
if forwards.is_empty() {
return Cleanup::default();
}
let forwarded_loads = forwards.len();
let mut resolved: FxHashMap<ValueId, ValueId> = FxHashMap::default();
for &(load_result, stored_value) in &forwards {
let survivor = resolved.get(&stored_value).copied().unwrap_or(stored_value);
resolved.insert(load_result, survivor);
}
let body = ctx.function_mut(func);
for (load_result, _) in forwards {
let survivor = resolved[&load_result];
body.replace_all_uses_with(load_result, survivor);
}
let dead_stores: Vec<InstructionId> = redundant
.iter()
.filter(|(_, slot)| !poisoned.contains(&slot.0) && !read_otherwise.contains(slot))
.map(|(store, _)| *store)
.collect();
let dead: FxHashSet<_> = consumed
.iter()
.chain(dead_stores.iter())
.map(|id| id.localize(func))
.collect();
body.remove_block_instructions(block_id, &dead);
Cleanup {
forwarded_loads,
removed_stores: dead_stores.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use qcode::value::FunctionBody;
#[test]
fn only_temporary_spaces_are_forwarded() {
let mut ctx = Context::new();
let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
let block = BasicBlock::make(&mut ctx, function).with_address(0x1000).id;
let ram = MemorySpaceId::Shared(ctx.shared.default_space);
assert!(!is_temporary(&ctx, ram));
assert_eq!(forward_temp_stores(&mut ctx, block), Cleanup::default());
}
#[test]
fn an_empty_block_is_unchanged() {
let mut ctx = Context::new();
let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
let block = BasicBlock::make(&mut ctx, function).with_address(0x1000).id;
assert!(forward_temp_stores(&mut ctx, block).is_empty());
}
}