use std::collections::{BTreeMap, BTreeSet};
use crate::{
BitSet,
analysis::consts::ConstEvaluator,
graph::NodeId,
ir::{
block::SsaBlock,
function::SsaFunction,
ops::SsaOp,
phi::{PhiNode, PhiOperand},
value::ConstValue,
variable::{SsaVarId, VariableOrigin},
},
target::Target,
};
pub struct PhiAnalyzer<'a, T: Target> {
ssa: &'a SsaFunction<T>,
}
impl<'a, T: Target> PhiAnalyzer<'a, T> {
#[must_use]
pub fn new(ssa: &'a SsaFunction<T>) -> Self {
Self { ssa }
}
#[must_use]
pub fn ssa(&self) -> &SsaFunction<T> {
self.ssa
}
#[must_use]
pub fn is_trivial(&self, phi: &PhiNode) -> Option<SsaVarId> {
let result = phi.result();
let unique_sources: BTreeSet<SsaVarId> = phi
.operands()
.iter()
.map(PhiOperand::value)
.filter(|&v| v != result)
.collect();
if unique_sources.len() == 1 {
let source = unique_sources.into_iter().next()?;
if self.ssa.would_create_self_reference(source, result) {
return None;
}
Some(source)
} else {
None
}
}
#[must_use]
pub fn is_fully_self_referential(&self, phi: &PhiNode) -> bool {
let result = phi.result();
!phi.operands().is_empty() && phi.operands().iter().all(|op| op.value() == result)
}
#[must_use]
pub fn analyze_trivial(&self, phi: &PhiNode) -> Option<Option<SsaVarId>> {
if let Some(source) = self.is_trivial(phi) {
return Some(Some(source));
}
if self.is_fully_self_referential(phi) {
return Some(None);
}
None
}
#[must_use]
pub fn find_all_trivial(
&self,
reachable: &BTreeSet<usize>,
) -> Vec<(usize, usize, Option<SsaVarId>)> {
let mut trivial = Vec::new();
for &block_idx in reachable {
if let Some(block) = self.ssa.block(block_idx) {
for (phi_idx, phi) in block.phi_nodes().iter().enumerate() {
if let Some(replacement) = self.analyze_trivial(phi) {
let same_block_instr_def = match replacement {
Some(source) => self.ssa.variable(source).is_some_and(|var| {
let site = var.def_site();
site.block == block_idx && site.instruction.is_some()
}),
None => false,
};
if same_block_instr_def {
continue;
}
trivial.push((block_idx, phi_idx, replacement));
}
}
}
}
trivial
}
#[must_use]
pub fn collect_all_copies(&self) -> BTreeMap<SsaVarId, SsaVarId> {
let mut copies = BTreeMap::new();
for block in self.ssa.blocks() {
for instr in block.instructions() {
if let SsaOp::Copy { dest, src } = instr.op() {
copies.insert(*dest, *src);
}
}
for phi in block.phi_nodes() {
if let Some(source) = self.is_trivial(phi) {
copies.insert(phi.result(), source);
}
}
}
copies
}
pub fn uniform_constant(
&self,
phi: &PhiNode,
evaluator: &mut ConstEvaluator<'_, T>,
) -> Option<ConstValue<T>> {
let operands = phi.operands();
if operands.is_empty() {
return None;
}
let first_value = evaluator.evaluate_var(operands.first()?.value())?;
for operand in operands.iter().skip(1) {
let value = evaluator.evaluate_var(operand.value())?;
if value != first_value {
return None;
}
}
Some(first_value)
}
#[must_use]
pub fn find_phi_defining(&self, var: SsaVarId) -> Option<(usize, &PhiNode)> {
self.ssa.find_phi_defining(var)
}
}
pub(crate) type LeaveTargetFn<'a, T> = dyn Fn(usize, &[SsaBlock<T>]) -> Option<usize> + 'a;
pub struct PhiPlacementConfig<'a, T: Target> {
pub defs: &'a BTreeMap<u32, BitSet>,
pub live_in: &'a BTreeMap<u32, BitSet>,
pub dominance_frontiers: &'a [BitSet],
pub reachable: Option<&'a BitSet>,
pub group_filter: &'a dyn Fn(u32) -> bool,
pub group_to_origin: &'a dyn Fn(u32) -> VariableOrigin,
pub leave_target_fn: Option<&'a LeaveTargetFn<'a, T>>,
}
pub fn place_pruned_phis<T: Target>(
blocks: &mut [SsaBlock<T>],
config: PhiPlacementConfig<'_, T>,
) -> Vec<(usize, u32)> {
let PhiPlacementConfig {
defs,
live_in,
dominance_frontiers,
reachable,
group_filter,
group_to_origin,
leave_target_fn,
} = config;
let block_count = blocks.len();
let mut placements: Vec<(usize, u32)> = Vec::new();
for (&group, def_blocks) in defs {
if !group_filter(group) {
continue;
}
let mut phi_blocks = BitSet::new(block_count);
let mut worklist: Vec<usize> = def_blocks.iter().collect();
while let Some(block_idx) = worklist.pop() {
let node_id = NodeId::new(block_idx);
if let Some(frontier) = dominance_frontiers.get(node_id.index()) {
for frontier_idx in frontier.iter() {
let is_reachable = reachable.is_none_or(|r| r.contains_checked(frontier_idx));
if frontier_idx < block_count && is_reachable && phi_blocks.insert(frontier_idx)
{
worklist.push(frontier_idx);
}
}
}
if let Some(leave_fn) = leave_target_fn
&& let Some(target) = leave_fn(block_idx, blocks)
{
let is_reachable = reachable.is_none_or(|r| r.contains_checked(target));
if target < block_count && is_reachable && phi_blocks.insert(target) {
worklist.push(target);
}
}
}
let group_live_in = live_in.get(&group);
for phi_block_idx in phi_blocks.iter() {
if let Some(live_set) = group_live_in
&& !live_set.contains(phi_block_idx)
{
continue;
}
if let Some(block) = blocks.get_mut(phi_block_idx) {
let origin = group_to_origin(group);
let phi = PhiNode::new(SsaVarId::PLACEHOLDER, origin);
block.add_phi(phi);
placements.push((phi_block_idx, group));
}
}
}
placements
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::*;
use crate::{
ir::{
block::SsaBlock,
function::SsaFunction,
instruction::SsaInstruction,
ops::SsaOp,
phi::{PhiNode, PhiOperand},
value::ConstValue,
variable::{DefSite, SsaVarId, VariableOrigin},
},
testing::{MockTarget, MockType},
};
#[test]
fn trivial_phi_single_operand() {
let ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let analyzer = PhiAnalyzer::new(&ssa);
let result = SsaVarId::from_index(0);
let v0 = SsaVarId::from_index(1);
let mut phi = PhiNode::new(result, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(v0, 0));
assert_eq!(analyzer.is_trivial(&phi), Some(v0));
}
#[test]
fn trivial_phi_all_same_operands() {
let ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let analyzer = PhiAnalyzer::new(&ssa);
let result = SsaVarId::from_index(0);
let v0 = SsaVarId::from_index(1);
let mut phi = PhiNode::new(result, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v0, 1));
phi.add_operand(PhiOperand::new(v0, 2));
assert_eq!(analyzer.is_trivial(&phi), Some(v0));
}
#[test]
fn trivial_phi_with_self_references() {
let ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let analyzer = PhiAnalyzer::new(&ssa);
let result = SsaVarId::from_index(0);
let v0 = SsaVarId::from_index(1);
let mut phi = PhiNode::new(result, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(result, 0)); phi.add_operand(PhiOperand::new(v0, 1));
phi.add_operand(PhiOperand::new(result, 2)); assert_eq!(analyzer.is_trivial(&phi), Some(v0));
}
#[test]
fn non_trivial_phi_different_operands() {
let ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let analyzer = PhiAnalyzer::new(&ssa);
let result = SsaVarId::from_index(0);
let v0 = SsaVarId::from_index(1);
let v1 = SsaVarId::from_index(2);
let mut phi = PhiNode::new(result, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v1, 1));
assert_eq!(analyzer.is_trivial(&phi), None);
}
#[test]
fn find_all_trivial_skips_same_block_instruction_replacement() {
let make = |src_def: DefSite| {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
let phi_res = SsaVarId::from_index(0);
ssa.create_variable(VariableOrigin::Local(0), 0, DefSite::phi(0), MockType::I32);
let v_src = SsaVarId::from_index(1);
ssa.create_variable(VariableOrigin::Local(1), 0, src_def, MockType::I32);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v_src,
value: ConstValue::I32(1),
}));
let mut phi = PhiNode::new(phi_res, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(v_src, 0));
b0.add_phi(phi);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(phi_res),
}));
ssa.add_block(b0);
ssa.recompute_uses();
ssa
};
let reachable: BTreeSet<usize> = [0usize].into_iter().collect();
let ssa = make(DefSite::instruction(0, 0));
assert!(
PhiAnalyzer::new(&ssa)
.find_all_trivial(&reachable)
.is_empty(),
"same-block-instruction replacement must not be reported trivial"
);
let ssa = make(DefSite::instruction(1, 0));
assert_eq!(
PhiAnalyzer::new(&ssa).find_all_trivial(&reachable).len(),
1,
"a replacement defined outside the phi's block stays trivial"
);
}
#[test]
fn trivial_phi_all_self_references() {
let ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let analyzer = PhiAnalyzer::new(&ssa);
let result = SsaVarId::from_index(0);
let mut phi = PhiNode::new(result, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(result, 0));
phi.add_operand(PhiOperand::new(result, 1));
assert!(analyzer.is_trivial(&phi).is_none());
assert_eq!(analyzer.analyze_trivial(&phi), Some(None));
}
}