use crate::mir::{BlockId, Function, InstId, Terminator, Value, ValueId};
use smallvec::SmallVec;
use solar_data_structures::{bit_set::GrowableBitSet, map::FxHashMap};
use std::collections::VecDeque;
pub type LiveSet = GrowableBitSet<ValueId>;
#[derive(Clone, Debug)]
pub struct LivenessInfo {
pub live_before: LiveSet,
pub live_after: LiveSet,
}
#[derive(Clone, Debug)]
struct BlockLiveness {
live_in: LiveSet,
live_out: LiveSet,
}
#[derive(Debug)]
pub struct Liveness {
block_liveness: Vec<BlockLiveness>,
last_use_in_block: FxHashMap<(ValueId, BlockId), Option<usize>>,
pub(crate) inst_to_value: FxHashMap<InstId, ValueId>,
#[allow(dead_code)]
num_values: usize,
}
impl Liveness {
#[must_use]
pub fn compute(func: &Function) -> Self {
let num_values = func.values.len();
let num_blocks = func.blocks.len();
let mut inst_to_value: FxHashMap<InstId, ValueId> = FxHashMap::default();
for (val_id, val) in func.values.iter_enumerated() {
if let Value::Inst(inst_id) = val {
inst_to_value.insert(*inst_id, val_id);
}
}
let mut block_liveness: Vec<BlockLiveness> = (0..num_blocks)
.map(|_| BlockLiveness {
live_in: LiveSet::with_capacity(num_values),
live_out: LiveSet::with_capacity(num_values),
})
.collect();
let mut block_defs: Vec<LiveSet> =
(0..num_blocks).map(|_| LiveSet::with_capacity(num_values)).collect();
let mut block_uses: Vec<LiveSet> =
(0..num_blocks).map(|_| LiveSet::with_capacity(num_values)).collect();
let mut operand_buf = SmallVec::<[ValueId; 8]>::new();
for (block_id, block) in func.blocks.iter_enumerated() {
let bidx = block_id.index();
for &inst_id in &block.instructions {
let inst = func.instruction(inst_id);
operand_buf.clear();
inst.kind.collect_operands(&mut operand_buf);
for &operand in &operand_buf {
if !block_defs[bidx].contains(operand) {
block_uses[bidx].insert(operand);
}
}
if let Some(&val_id) = inst_to_value.get(&inst_id) {
block_defs[bidx].insert(val_id);
}
}
if let Some(term) = &block.terminator {
operand_buf.clear();
collect_terminator_uses(term, &mut operand_buf);
for &operand in &operand_buf {
if !block_defs[bidx].contains(operand) {
block_uses[bidx].insert(operand);
}
}
}
}
let mut worklist: VecDeque<BlockId> = func.blocks.indices().collect();
while let Some(block_id) = worklist.pop_front() {
let bidx = block_id.index();
let block = &func.blocks[block_id];
let mut new_live_out = LiveSet::with_capacity(num_values);
let successors =
block.terminator.as_ref().map(Terminator::successors).unwrap_or_default();
for succ in successors {
new_live_out.union(&block_liveness[succ.index()].live_in);
}
let mut new_live_in = block_uses[bidx].clone();
for val in new_live_out.iter() {
if !block_defs[bidx].contains(val) {
new_live_in.insert(val);
}
}
if new_live_out != block_liveness[bidx].live_out
|| new_live_in != block_liveness[bidx].live_in
{
block_liveness[bidx].live_out = new_live_out;
block_liveness[bidx].live_in = new_live_in;
for &pred in &block.predecessors {
worklist.push_back(pred);
}
}
}
let mut last_use_in_block: FxHashMap<(ValueId, BlockId), Option<usize>> =
FxHashMap::default();
for (block_id, block) in func.blocks.iter_enumerated() {
if let Some(term) = &block.terminator {
operand_buf.clear();
collect_terminator_uses(term, &mut operand_buf);
for &operand in &operand_buf {
last_use_in_block.entry((operand, block_id)).or_insert(None);
}
}
for (inst_idx, &inst_id) in block.instructions.iter().enumerate().rev() {
let inst = func.instruction(inst_id);
operand_buf.clear();
inst.kind.collect_operands(&mut operand_buf);
for &operand in &operand_buf {
last_use_in_block.entry((operand, block_id)).or_insert(Some(inst_idx));
}
}
}
Self { block_liveness, last_use_in_block, inst_to_value, num_values }
}
#[must_use]
pub fn live_in(&self, block: BlockId) -> &LiveSet {
&self.block_liveness[block.index()].live_in
}
#[must_use]
pub fn live_out(&self, block: BlockId) -> &LiveSet {
&self.block_liveness[block.index()].live_out
}
#[must_use]
pub fn live_at_inst(
&self,
func: &Function,
block_id: BlockId,
inst_idx: usize,
) -> LivenessInfo {
let bidx = block_id.index();
let block = &func.blocks[block_id];
let mut live = self.block_liveness[bidx].live_out.clone();
if let Some(term) = &block.terminator {
let mut term_uses = SmallVec::<[ValueId; 8]>::new();
collect_terminator_uses(term, &mut term_uses);
for operand in term_uses {
live.insert(operand);
}
}
let mut operand_buf = SmallVec::<[ValueId; 8]>::new();
let mut live_after = None;
for (idx, &inst_id) in block.instructions.iter().enumerate().rev() {
if idx == inst_idx {
live_after = Some(live.clone());
}
if let Some(&val_id) = self.inst_to_value.get(&inst_id) {
live.remove(val_id);
}
operand_buf.clear();
func.instruction(inst_id).kind.collect_operands(&mut operand_buf);
for &operand in &operand_buf {
live.insert(operand);
}
if idx == inst_idx {
return LivenessInfo { live_before: live, live_after: live_after.unwrap() };
}
}
LivenessInfo { live_before: live.clone(), live_after: live }
}
#[must_use]
pub fn last_use_in_block(&self, val: ValueId, block: BlockId) -> Option<Option<usize>> {
self.last_use_in_block.get(&(val, block)).copied()
}
#[must_use]
pub fn is_dead_after(&self, val: ValueId, block: BlockId, inst_idx: usize) -> bool {
if self.block_liveness[block.index()].live_out.contains(val) {
return false;
}
match self.last_use_in_block.get(&(val, block)) {
Some(&Some(last_idx)) => last_idx == inst_idx,
Some(&None) => false,
None => true,
}
}
}
fn collect_terminator_uses(term: &Terminator, out: &mut SmallVec<[ValueId; 8]>) {
match term {
Terminator::Jump(_) => {}
Terminator::Branch { condition, .. } => {
out.push(*condition);
}
Terminator::Switch { value, cases, .. } => {
out.push(*value);
for (case_val, _) in cases {
out.push(*case_val);
}
}
Terminator::Return { values } => {
out.extend(values.iter().copied());
}
Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
out.push(*offset);
out.push(*size);
}
Terminator::Stop | Terminator::Invalid => {}
Terminator::SelfDestruct { recipient } => {
out.push(*recipient);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_liveset_basic() {
let mut set = LiveSet::with_capacity(100);
let v0 = ValueId::from_usize(0);
let v42 = ValueId::from_usize(42);
let v99 = ValueId::from_usize(99);
assert!(!set.contains(v0));
assert!(!set.contains(v42));
assert!(set.insert(v0));
assert!(set.contains(v0));
assert!(!set.insert(v0));
assert!(set.insert(v42));
assert!(set.contains(v42));
assert!(set.insert(v99));
assert_eq!(set.count(), 3);
set.remove(v42);
assert!(!set.contains(v42));
assert_eq!(set.count(), 2);
}
#[test]
fn test_liveset_union() {
let mut set1 = LiveSet::with_capacity(64);
let mut set2 = LiveSet::with_capacity(64);
set1.insert(ValueId::from_usize(1));
set1.insert(ValueId::from_usize(3));
set2.insert(ValueId::from_usize(2));
set2.insert(ValueId::from_usize(3));
assert!(set1.union(&set2));
assert!(set1.contains(ValueId::from_usize(1)));
assert!(set1.contains(ValueId::from_usize(2)));
assert!(set1.contains(ValueId::from_usize(3)));
assert_eq!(set1.count(), 3);
assert!(!set1.union(&set2));
}
#[test]
fn test_liveset_boundary() {
let mut set = LiveSet::with_capacity(200);
for i in [0, 1, 62, 63, 64, 65, 126, 127, 128, 129, 199] {
assert!(set.insert(ValueId::from_usize(i)));
assert!(set.contains(ValueId::from_usize(i)));
}
assert_eq!(set.count(), 11);
}
#[test]
fn test_liveset_clear() {
let mut set = LiveSet::with_capacity(128);
set.insert(ValueId::from_usize(0));
set.insert(ValueId::from_usize(63));
set.insert(ValueId::from_usize(64));
set.insert(ValueId::from_usize(127));
assert_eq!(set.count(), 4);
set.clear();
assert_eq!(set.count(), 0);
assert!(!set.contains(ValueId::from_usize(0)));
}
#[test]
fn test_liveset_iter() {
let mut set = LiveSet::with_capacity(200);
let indices = [0, 5, 63, 64, 127, 128, 199];
for &i in &indices {
set.insert(ValueId::from_usize(i));
}
let collected: Vec<usize> = set.iter().map(|v| v.index()).collect();
assert_eq!(collected, indices);
}
use crate::mir::{Function, FunctionBuilder, MirType};
use solar_interface::Ident;
fn make_func() -> Function {
Function::new(Ident::DUMMY)
}
#[test]
fn test_linear_code() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let x = b.add_param(MirType::uint256());
let one = b.imm_u64(1);
let sum = b.add(x, one);
b.ret([sum]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
assert!(liveness.live_in(entry).contains(x));
assert!(liveness.live_in(entry).contains(one));
assert!(!liveness.live_out(entry).contains(sum));
assert_eq!(liveness.live_out(entry).count(), 0);
}
#[test]
fn test_diamond_cfg() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let x = b.add_param(MirType::uint256());
let cond = b.add_param(MirType::Bool);
let then_bb = b.create_block();
let else_bb = b.create_block();
let merge = b.create_block();
b.branch(cond, then_bb, else_bb);
b.switch_to_block(then_bb);
let c1 = b.imm_u64(1);
let v_then = b.add(x, c1);
b.jump(merge);
b.switch_to_block(else_bb);
let c2 = b.imm_u64(1);
let v_else = b.sub(x, c2);
b.jump(merge);
b.switch_to_block(merge);
b.ret([v_then]);
let liveness = Liveness::compute(&func);
assert!(liveness.live_in(then_bb).contains(x));
assert!(liveness.live_in(else_bb).contains(x));
assert!(liveness.live_out(func.entry_block).contains(x));
assert!(liveness.live_out(then_bb).contains(v_then));
assert!(!liveness.live_out(else_bb).contains(v_else));
}
#[test]
fn test_simple_loop() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let i = b.add_param(MirType::uint256());
let limit = b.imm_u64(10);
let header = b.create_block();
let body = b.create_block();
let exit = b.create_block();
b.jump(header);
b.switch_to_block(header);
let cond = b.lt(i, limit);
b.branch(cond, body, exit);
b.switch_to_block(body);
let step = b.imm_u64(1);
let _i_next = b.add(i, step);
b.jump(header);
b.switch_to_block(exit);
b.ret([i]);
let liveness = Liveness::compute(&func);
assert!(liveness.live_in(header).contains(i), "i live-in to header");
assert!(liveness.live_in(body).contains(i), "i live-in to body");
assert!(liveness.live_in(exit).contains(i), "i live-in to exit");
assert!(liveness.live_out(header).contains(i), "i live-out of header");
assert!(liveness.live_out(body).contains(i), "i live-out of body");
}
#[test]
fn test_dead_instruction_result() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let x = b.add_param(MirType::uint256());
let y = b.add_param(MirType::uint256());
let dead = b.add(x, y); b.ret([x]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
assert!(liveness.live_in(entry).contains(x));
assert!(!liveness.live_out(entry).contains(dead));
assert!(liveness.is_dead_after(dead, entry, 0), "dead inst result should be dead");
}
#[test]
fn test_unused_param() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let _x = b.add_param(MirType::uint256()); let y = b.add_param(MirType::uint256());
b.ret([y]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
assert!(!liveness.live_in(entry).contains(_x), "unused param not live");
assert!(liveness.live_in(entry).contains(y), "used param is live");
}
#[test]
fn test_value_used_in_two_successors() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let x = b.add_param(MirType::uint256());
let cond = b.add_param(MirType::Bool);
let left = b.create_block();
let right = b.create_block();
b.branch(cond, left, right);
b.switch_to_block(left);
b.ret([x]);
b.switch_to_block(right);
b.ret([x]);
let liveness = Liveness::compute(&func);
assert!(liveness.live_in(left).contains(x));
assert!(liveness.live_in(right).contains(x));
assert!(liveness.live_out(func.entry_block).contains(x));
}
#[test]
fn test_empty_function() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
b.stop();
let liveness = Liveness::compute(&func);
assert_eq!(liveness.live_in(func.entry_block).count(), 0);
assert_eq!(liveness.live_out(func.entry_block).count(), 0);
}
#[test]
fn test_side_effect_op_keeps_operands_live() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let slot = b.add_param(MirType::uint256());
let val = b.add_param(MirType::uint256());
b.sstore(slot, val);
let loaded = b.sload(slot);
b.ret([loaded]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
let info_0 = liveness.live_at_inst(&func, entry, 0);
assert!(info_0.live_before.contains(slot), "slot live before sstore");
assert!(info_0.live_before.contains(val), "val live before sstore");
let info_1 = liveness.live_at_inst(&func, entry, 1);
assert!(info_1.live_before.contains(slot), "slot live before sload");
assert!(!info_1.live_before.contains(val), "val not live before sload");
}
#[test]
fn test_live_at_inst() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let v0 = b.add_param(MirType::uint256());
let v1 = b.add_param(MirType::uint256());
let v2 = b.add(v0, v1);
let v3 = b.mul(v2, v0);
b.ret([v3]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
let info_0 = liveness.live_at_inst(&func, entry, 0);
assert!(info_0.live_before.contains(v0));
assert!(info_0.live_before.contains(v1));
assert!(!info_0.live_before.contains(v2));
assert!(!info_0.live_before.contains(v3));
assert!(info_0.live_after.contains(v0));
assert!(info_0.live_after.contains(v2));
assert!(!info_0.live_after.contains(v1), "v1 dead after add");
let info_1 = liveness.live_at_inst(&func, entry, 1);
assert!(info_1.live_before.contains(v0));
assert!(info_1.live_before.contains(v2));
assert!(info_1.live_after.contains(v3));
assert!(!info_1.live_after.contains(v0));
assert!(!info_1.live_after.contains(v2));
}
#[test]
fn test_is_dead_after() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let v0 = b.add_param(MirType::uint256());
let v1 = b.add_param(MirType::uint256());
let v2 = b.add(v0, v1);
b.ret([v2]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
assert!(liveness.is_dead_after(v0, entry, 0), "v0 dead after add");
assert!(liveness.is_dead_after(v1, entry, 0), "v1 dead after add");
assert!(!liveness.is_dead_after(v2, entry, 0), "v2 alive after add");
}
#[test]
fn test_last_use_in_block() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let v0 = b.add_param(MirType::uint256());
let v1 = b.add_param(MirType::uint256());
let _v2 = b.add(v0, v1);
let v3 = b.mul(_v2, v0);
b.ret([v3]);
let liveness = Liveness::compute(&func);
let entry = func.entry_block;
assert_eq!(liveness.last_use_in_block(v0, entry), Some(Some(1)));
assert_eq!(liveness.last_use_in_block(v1, entry), Some(Some(0)));
assert_eq!(liveness.last_use_in_block(v3, entry), Some(None));
}
#[test]
fn test_value_live_across_multiple_blocks() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let v0 = b.add_param(MirType::uint256());
let v1 = b.add_param(MirType::uint256());
let bb1 = b.create_block();
let bb2 = b.create_block();
b.jump(bb1);
b.switch_to_block(bb1);
let _v2 = b.add(v0, v1);
b.jump(bb2);
b.switch_to_block(bb2);
b.ret([v0]);
let liveness = Liveness::compute(&func);
assert!(liveness.live_out(bb1).contains(v0));
assert!(!liveness.live_out(bb1).contains(v1));
assert!(liveness.live_in(bb2).contains(v0));
}
#[test]
fn test_multiple_returns() {
let mut func = make_func();
let mut b = FunctionBuilder::new(&mut func);
let v0 = b.add_param(MirType::uint256());
let v1 = b.add_param(MirType::uint256());
let cond = b.add_param(MirType::Bool);
let left = b.create_block();
let right = b.create_block();
b.branch(cond, left, right);
b.switch_to_block(left);
b.ret([v0]);
b.switch_to_block(right);
b.ret([v1]);
let liveness = Liveness::compute(&func);
assert!(liveness.live_in(left).contains(v0));
assert!(!liveness.live_in(right).contains(v0));
assert!(liveness.live_in(right).contains(v1));
assert!(!liveness.live_in(left).contains(v1));
}
#[test]
fn test_phi_liveness() {
let mut func = make_func();
let entry;
let header;
let body;
let exit;
let init;
let updated;
let phi_placeholder; {
let mut b = FunctionBuilder::new(&mut func);
init = b.imm_u64(0);
entry = b.current_block();
header = b.create_block();
body = b.create_block();
exit = b.create_block();
b.jump(header);
b.switch_to_block(header);
phi_placeholder = b.undef(MirType::uint256());
let limit = b.imm_u64(10);
let cond = b.lt(phi_placeholder, limit);
b.branch(cond, body, exit);
b.switch_to_block(body);
let step = b.imm_u64(1);
updated = b.add(phi_placeholder, step);
b.jump(header);
b.switch_to_block(exit);
b.ret([phi_placeholder]);
}
let phi_val = phi_placeholder;
let phi_inst = func.alloc_inst(crate::mir::Instruction::new(
crate::mir::InstKind::Phi(vec![(entry, init), (body, updated)]),
Some(MirType::uint256()),
));
func.blocks[header].instructions.insert(0, phi_inst);
func.values[phi_val] = crate::mir::Value::Inst(phi_inst);
let liveness = Liveness::compute(&func);
assert!(liveness.live_out(entry).contains(init), "init live-out of entry");
assert!(liveness.live_out(body).contains(updated), "updated live-out of body");
assert!(liveness.live_in(body).contains(phi_val), "phi_val live-in to body");
assert!(liveness.live_in(exit).contains(phi_val), "phi_val live-in to exit");
assert!(!liveness.live_in(entry).contains(phi_val), "phi_val not live-in to entry");
}
#[test]
fn test_inst_to_value_map_consistency() {
let mut func = make_func();
{
let mut b = FunctionBuilder::new(&mut func);
let x = b.add_param(MirType::uint256());
let y = b.add_param(MirType::uint256());
let sum = b.add(x, y);
let product = b.mul(sum, x);
b.sstore(x, product); let loaded = b.sload(y);
b.ret([loaded]);
}
let liveness = Liveness::compute(&func);
use solar_data_structures::map::FxHashMap;
let mut expected: FxHashMap<crate::mir::InstId, ValueId> = FxHashMap::default();
for (val_id, val) in func.values.iter_enumerated() {
if let crate::mir::Value::Inst(inst_id) = val {
expected.insert(*inst_id, val_id);
}
}
assert_eq!(liveness.inst_to_value, expected, "inst_to_value map diverged from linear scan");
}
}