use crate::ir::block::{Block, InsnStart};
use crate::ir::op::{MemOp, Opcode};
use crate::ir::types::Temp;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TempLife {
pub def: Option<u32>,
pub last_use: Option<u32>,
pub uses: u32,
pub at_boundary: bool,
}
impl TempLife {
#[inline]
#[must_use]
pub const fn is_live(self) -> bool {
self.uses > 0 || self.at_boundary
}
#[must_use]
pub fn interval(self) -> Option<(u32, u32)> {
let def = self.def?;
Some((def, self.last_use.unwrap_or(def).max(def)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Liveness {
lives: Vec<TempLife>,
}
impl Liveness {
#[must_use]
pub fn compute(block: &Block) -> Liveness {
let mut lives = vec![TempLife::default(); block.temp_count()];
for (i, inst) in block.insts().iter().enumerate() {
let at = i as u32;
for src in block.srcs(i) {
if let Some(life) = lives.get_mut(src.index()) {
life.uses += 1;
life.last_use = Some(at);
}
}
for dst in [inst.dst, inst.dst2].into_iter().flatten() {
if let Some(life) = lives.get_mut(dst.index())
&& life.def.is_none()
{
life.def = Some(at);
}
}
if inst.op == Opcode::INSN_START
&& let Some(mark) = block.marks().get(inst.aux as usize)
{
for (_, temp) in &mark.live {
if let Some(life) = lives.get_mut(temp.index()) {
life.at_boundary = true;
life.last_use = Some(match life.last_use {
Some(prev) => prev.max(at),
None => at,
});
}
}
}
}
Liveness { lives }
}
#[inline]
#[must_use]
pub fn life(&self, temp: Temp) -> Option<TempLife> {
self.lives.get(temp.index()).copied()
}
#[inline]
#[must_use]
pub fn is_live(&self, temp: Temp) -> bool {
self.life(temp).is_some_and(TempLife::is_live)
}
pub fn iter(&self) -> impl Iterator<Item = (Temp, TempLife)> + '_ {
self.lives
.iter()
.enumerate()
.map(|(i, life)| (Temp(i as u32), *life))
}
#[must_use]
pub fn intervals(&self) -> Vec<(Temp, u32, u32)> {
let mut out: Vec<(Temp, u32, u32)> = self
.iter()
.filter_map(|(temp, life)| life.interval().map(|(lo, hi)| (temp, lo, hi)))
.collect();
out.sort_by_key(|(temp, lo, _)| (*lo, *temp));
out
}
}
fn must_keep(op: Opcode, mem: Option<MemOp>, has_result: bool) -> bool {
!has_result
|| op.is_terminator()
|| op.has_side_effect()
|| mem.is_some_and(|m| m.volatile)
}
#[must_use]
pub fn eliminate_dead_code(block: &Block) -> Block {
let insts = block.insts();
let mut needed = vec![false; block.temp_count()];
for mark in block.marks() {
seed_boundary(mark, &mut needed);
}
let mut keep = vec![false; insts.len()];
for (i, inst) in insts.iter().enumerate().rev() {
let results = [inst.dst, inst.dst2];
let has_result = results.iter().any(Option::is_some);
let result_live = results
.into_iter()
.flatten()
.any(|t| needed.get(t.index()).copied().unwrap_or(true));
if !must_keep(inst.op, inst.mem, has_result) && !result_live {
continue;
}
keep[i] = true;
for src in block.srcs(i) {
if let Some(slot) = needed.get_mut(src.index()) {
*slot = true;
}
}
}
block.retain(&keep)
}
fn seed_boundary(mark: &InsnStart, needed: &mut [bool]) {
for (_, temp) in &mark.live {
if let Some(slot) = needed.get_mut(temp.index()) {
*slot = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::value::Width;
use crate::ir::block::{BlockBuilder, RegSlot};
use crate::ir::op::Cond;
use crate::ir::types::{Const, Type};
use crate::ir::verify;
fn mark(pc: u64, ticks: u64, live: &[(RegSlot, Temp)]) -> InsnStart {
InsnStart {
pc,
next_pc: pc + 2,
ticks,
live: live.to_vec(),
}
}
fn count(block: &Block, op: Opcode) -> usize {
block.insts().iter().filter(|i| i.op == op).count()
}
fn parity_block(name_parity_live: bool) -> Block {
let mut b = BlockBuilder::new(0x1000, 0);
b.insn_start(mark(0x1000, 0, &[]));
b.charge(4);
let a = b.imm(Type::I32, Const::Int(0x5a));
let v = b.imm(Type::I32, Const::Int(0x0f));
let result = b.binary(Opcode::XOR, Type::I32, a, v);
let ones = b.unary(Opcode::POPCOUNT, Type::I32, result);
let one = b.imm(Type::I32, Const::Int(1));
let odd = b.binary(Opcode::AND, Type::I32, ones, one);
let zero = b.imm(Type::I32, Const::Int(0));
let parity = b.setcond(Cond::Eq, Type::I32, odd, zero);
let addr = b.imm(Type::I64, Const::Int(0x2000));
b.store(Type::I32, addr, result, MemOp::store(Width::U32));
let live: Vec<(RegSlot, Temp)> = if name_parity_live {
vec![(RegSlot(0), parity)]
} else {
Vec::new()
};
b.insn_start(mark(0x1002, 4, &live));
b.charge(4);
b.exit_tb();
b.finish()
}
#[test]
fn a_parity_flag_nothing_reads_is_removed() {
let block = parity_block(false);
verify(&block).expect("the input block is well formed");
let before = block.insts().len();
let out = eliminate_dead_code(&block);
verify(&out).expect("dead-code elimination must not break the block");
assert_eq!(count(&out, Opcode::POPCOUNT), 0, "{out}");
assert_eq!(count(&out, Opcode::SETCOND), 0, "{out}");
assert_eq!(count(&out, Opcode::AND), 0, "{out}");
assert_eq!(out.insts().len(), before - 5, "{out}");
assert_eq!(count(&out, Opcode::XOR), 1);
assert_eq!(count(&out, Opcode::ST), 1);
assert_eq!(count(&out, Opcode::CHARGE), 2);
assert_eq!(count(&out, Opcode::INSN_START), 2);
assert_eq!(count(&out, Opcode::EXIT_TB), 1);
assert_eq!(out.marks(), block.marks(), "the records must be untouched");
}
#[test]
fn the_same_flag_named_live_at_a_boundary_stays() {
let block = parity_block(true);
verify(&block).expect("the input block is well formed");
let out = eliminate_dead_code(&block);
verify(&out).expect("dead-code elimination must not break the block");
assert_eq!(out.insts(), block.insts(), "nothing here is dead:\n{out}");
assert_eq!(count(&out, Opcode::POPCOUNT), 1);
assert_eq!(count(&out, Opcode::SETCOND), 1);
}
#[test]
fn a_volatile_load_survives_and_a_plain_one_does_not() {
let mut b = BlockBuilder::new(0x1000, 0);
b.insn_start(mark(0x1000, 0, &[]));
b.charge(1);
let addr = b.imm(Type::I64, Const::Int(0x20ff));
let mut dummy = MemOp::load(Width::U8);
dummy.volatile = true;
let _ = b.load(Type::I32, addr, dummy);
let _ = b.load(Type::I32, addr, MemOp::load(Width::U8));
b.exit_tb();
let block = b.finish();
verify(&block).expect("well formed");
let out = eliminate_dead_code(&block);
verify(&out).expect("still well formed");
assert_eq!(count(&out, Opcode::LD), 1, "{out}");
let ld = out
.insts()
.iter()
.find(|i| i.op == Opcode::LD)
.expect("the volatile load is the one that stayed");
assert!(ld.mem.expect("a load carries its descriptor").volatile);
}
#[test]
fn effects_and_control_flow_are_never_removed() {
let mut b = BlockBuilder::new(0x1000, 0);
b.insn_start(mark(0x1000, 0, &[]));
b.charge(1);
let flag = b.imm(Type::I1, Const::Int(1));
let branch = b.emit_raw(Opcode::BRCOND, Type::I1, None, None, &[flag], None, None, 0);
let _ = b.emit(Opcode::CALL_HELPER, Type::I64, &[]);
let addr = b.imm(Type::I64, Const::Int(0x40));
let one = b.imm(Type::I64, Const::Int(1));
let _ = b.emit(Opcode::FETCH_ADD, Type::I64, &[addr, one]);
b.patch_aux(branch, b.next_index() as u32);
b.exit_tb();
let block = b.finish();
verify(&block).expect("well formed");
let out = eliminate_dead_code(&block);
verify(&out).expect("still well formed");
assert_eq!(out.insts(), block.insts(), "nothing here may go:\n{out}");
}
#[test]
fn eliminating_dead_code_repoints_a_forward_branch_at_what_is_left() {
let mut b = BlockBuilder::new(0x1000, 0);
b.insn_start(mark(0x1000, 0, &[]));
b.charge(1);
let flag = b.imm(Type::I1, Const::Int(0));
let branch = b.emit_raw(Opcode::BRCOND, Type::I1, None, None, &[flag], None, None, 0);
let a = b.imm(Type::I64, Const::Int(3));
let n = b.unary(Opcode::NEG, Type::I64, a);
let _ = b.unary(Opcode::NOT, Type::I64, n);
b.patch_aux(branch, b.next_index() as u32);
b.charge(2);
b.exit_tb();
let block = b.finish();
verify(&block).expect("well formed");
let out = eliminate_dead_code(&block);
verify(&out).expect("still well formed");
let (at, brcond) = out
.insts()
.iter()
.enumerate()
.find(|(_, i)| i.op == Opcode::BRCOND)
.expect("the branch stayed");
assert_eq!(
out.insts()[brcond.aux as usize].op,
Opcode::CHARGE,
"the branch lost its target:\n{out}"
);
assert_eq!(brcond.aux as usize, at + 1, "{out}");
}
#[test]
fn a_dead_chain_goes_all_the_way_down() {
let mut b = BlockBuilder::new(0x1000, 0);
b.insn_start(mark(0x1000, 0, &[]));
b.charge(1);
let a = b.imm(Type::I64, Const::Int(3));
let x = b.unary(Opcode::NEG, Type::I64, a);
let y = b.unary(Opcode::NOT, Type::I64, x);
let _ = b.binary(Opcode::MUL, Type::I64, y, y);
b.exit_tb();
let block = b.finish();
verify(&block).expect("well formed");
let out = eliminate_dead_code(&block);
verify(&out).expect("still well formed");
assert_eq!(out.insts().len(), 3, "only the frame survives:\n{out}");
assert_eq!(count(&out, Opcode::MOV), 0);
}
#[test]
fn eliminating_twice_changes_nothing_the_first_pass_left() {
let block = parity_block(false);
let once = eliminate_dead_code(&block);
let twice = eliminate_dead_code(&once);
assert_eq!(once.insts(), twice.insts(), "the pass is not idempotent");
}
#[test]
fn liveness_reports_uses_and_intervals() {
let block = parity_block(true);
let live = Liveness::compute(&block);
let a = live.life(Temp(0)).expect("t0 is allocated");
assert_eq!(a.def, Some(2));
assert_eq!(a.uses, 1);
assert_eq!(a.interval(), Some((2, 4)));
assert!(!a.at_boundary);
let result = live.life(Temp(2)).expect("t2 is allocated");
assert_eq!(result.uses, 2);
assert!(result.is_live());
let parity = live.life(Temp(7)).expect("t7 is the parity flag");
assert_eq!(parity.uses, 0);
assert!(parity.at_boundary);
assert!(parity.is_live());
let (def, end) = parity.interval().expect("it is assigned");
assert!(end > def, "the range must reach the boundary");
let intervals = live.intervals();
assert!(
intervals.windows(2).all(|w| w[0].1 <= w[1].1),
"{intervals:?}"
);
assert_eq!(intervals.len(), block.temp_count());
}
#[test]
fn liveness_after_elimination_has_no_dead_temporaries_left() {
let out = eliminate_dead_code(&parity_block(false));
let live = Liveness::compute(&out);
for (temp, life) in live.iter() {
assert!(
life.def.is_none() || life.is_live(),
"{temp} is still assigned and still dead:\n{out}"
);
}
}
}