use std::collections::BTreeSet;
use crate::{
graph::{
GraphBase, NodeId, Predecessors, RootedGraph, Successors,
algorithms::{postorder, reverse_postorder},
},
ir::function::SsaFunction,
target::Target,
};
#[derive(Debug)]
pub struct SsaCfg<'a, T: Target> {
ssa: &'a SsaFunction<T>,
successor_offsets: Vec<u32>,
successor_values: Vec<usize>,
predecessor_offsets: Vec<u32>,
predecessor_values: Vec<usize>,
}
impl<'a, T: Target> SsaCfg<'a, T> {
#[must_use]
pub fn from_ssa(ssa: &'a SsaFunction<T>) -> Self {
let block_count = ssa.block_count();
let mut edges: Vec<(usize, usize)> = Vec::with_capacity(block_count.saturating_mul(2));
for block_idx in 0..block_count {
let Some(block) = ssa.block(block_idx) else {
continue;
};
let terminator = block.instructions().iter().rev().find_map(|instr| {
let op = instr.op();
if op.is_terminator() { Some(op) } else { None }
});
if let Some(op) = terminator {
op.for_each_successor(|succ| {
if succ < block_count {
edges.push((block_idx, succ));
}
});
}
}
let mut handler_pairs: BTreeSet<(usize, usize)> = BTreeSet::new();
let mut explicit_targets: BTreeSet<(usize, usize)> = BTreeSet::new();
for handler in ssa.exception_handlers() {
if let (Some(try_start), Some(handler_start)) =
(handler.try_start_block, handler.handler_start_block)
&& handler_start < block_count
&& try_start < block_count
{
handler_pairs.insert((try_start, handler_start));
}
}
if !handler_pairs.is_empty() {
for &edge in &edges {
if handler_pairs.contains(&edge) {
explicit_targets.insert(edge);
}
}
for pair in handler_pairs {
if !explicit_targets.contains(&pair) {
edges.push(pair);
}
}
}
let mut successor_offsets: Vec<u32> = vec![0; block_count.saturating_add(1)];
let mut predecessor_offsets: Vec<u32> = vec![0; block_count.saturating_add(1)];
for (from, to) in &edges {
if let Some(slot) = from
.checked_add(1)
.and_then(|next| successor_offsets.get_mut(next))
{
*slot = slot.saturating_add(1);
}
if let Some(slot) = to
.checked_add(1)
.and_then(|next| predecessor_offsets.get_mut(next))
{
*slot = slot.saturating_add(1);
}
}
let mut running_succ: u32 = 0;
for slot in &mut successor_offsets {
running_succ = running_succ.saturating_add(*slot);
*slot = running_succ;
}
let mut running_pred: u32 = 0;
for slot in &mut predecessor_offsets {
running_pred = running_pred.saturating_add(*slot);
*slot = running_pred;
}
let mut successor_values: Vec<usize> = vec![0; running_succ as usize];
let mut predecessor_values: Vec<usize> = vec![0; running_pred as usize];
let mut succ_cursors: Vec<u32> = successor_offsets.clone();
let mut pred_cursors: Vec<u32> = predecessor_offsets.clone();
for (from, to) in &edges {
if let Some(cursor) = succ_cursors.get_mut(*from) {
if let Some(slot) = successor_values.get_mut(*cursor as usize) {
*slot = *to;
}
*cursor = cursor.saturating_add(1);
}
if let Some(cursor) = pred_cursors.get_mut(*to) {
if let Some(slot) = predecessor_values.get_mut(*cursor as usize) {
*slot = *from;
}
*cursor = cursor.saturating_add(1);
}
}
Self {
ssa,
successor_offsets,
successor_values,
predecessor_offsets,
predecessor_values,
}
}
fn csr_row<'s>(offsets: &[u32], values: &'s [usize], block: usize) -> &'s [usize] {
let Some(start) = offsets.get(block).map(|offset| *offset as usize) else {
return &[];
};
let Some(end) = block
.checked_add(1)
.and_then(|next| offsets.get(next))
.map(|offset| *offset as usize)
else {
return &[];
};
values.get(start..end).unwrap_or(&[])
}
#[must_use]
pub const fn ssa(&self) -> &'a SsaFunction<T> {
self.ssa
}
#[must_use]
pub fn block_count(&self) -> usize {
self.ssa.block_count()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ssa.is_empty()
}
#[must_use]
pub fn block_successors(&self, block_idx: usize) -> &[usize] {
Self::csr_row(&self.successor_offsets, &self.successor_values, block_idx)
}
#[must_use]
pub fn block_predecessors(&self, block_idx: usize) -> &[usize] {
Self::csr_row(
&self.predecessor_offsets,
&self.predecessor_values,
block_idx,
)
}
#[must_use]
pub fn exits(&self) -> Vec<NodeId> {
let mut exits = Vec::new();
for idx in 0..self.ssa.block_count() {
if self.block_successors(idx).is_empty() {
exits.push(NodeId::new(idx));
}
}
exits
}
#[must_use]
pub fn postorder(&self) -> Vec<NodeId> {
postorder(self, self.entry())
}
#[must_use]
pub fn reverse_postorder(&self) -> Vec<NodeId> {
reverse_postorder(self, self.entry())
}
}
impl<T: Target> GraphBase for SsaCfg<'_, T> {
fn node_count(&self) -> usize {
self.ssa.block_count()
}
fn node_ids(&self) -> impl Iterator<Item = NodeId> {
(0..self.ssa.block_count()).map(NodeId::new)
}
}
impl<T: Target> Successors for SsaCfg<'_, T> {
fn successors(&self, node: NodeId) -> impl Iterator<Item = NodeId> {
self.block_successors(node.index())
.iter()
.copied()
.map(NodeId::new)
}
}
impl<T: Target> Predecessors for SsaCfg<'_, T> {
fn predecessors(&self, node: NodeId) -> impl Iterator<Item = NodeId> {
self.block_predecessors(node.index())
.iter()
.copied()
.map(NodeId::new)
}
}
impl<T: Target> RootedGraph for SsaCfg<'_, T> {
fn entry(&self) -> NodeId {
NodeId::new(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
graph::{GraphBase, Predecessors, RootedGraph, Successors},
ir::{
block::SsaBlock, exception::SsaExceptionHandler, instruction::SsaInstruction,
ops::SsaOp,
},
testing::MockTarget,
};
fn block(id: usize, op: SsaOp<MockTarget>) -> SsaBlock<MockTarget> {
let mut block = SsaBlock::new(id);
block.add_instruction(SsaInstruction::synthetic(op));
block
}
#[test]
fn cfg_extracts_successors_and_predecessors_from_terminators() {
let mut ssa = crate::ir::SsaFunction::<MockTarget>::new(0, 0);
ssa.add_block(block(
0,
SsaOp::Branch {
condition: crate::ir::SsaVarId::from_index(0),
true_target: 1,
false_target: 2,
},
));
ssa.add_block(block(1, SsaOp::Jump { target: 3 }));
ssa.add_block(block(2, SsaOp::Return { value: None }));
ssa.add_block(block(3, SsaOp::Return { value: None }));
let cfg = SsaCfg::from_ssa(&ssa);
assert_eq!(cfg.ssa().block_count(), 4);
assert_eq!(cfg.block_count(), 4);
assert!(!cfg.is_empty());
assert_eq!(cfg.block_successors(0), &[1, 2]);
assert_eq!(cfg.block_predecessors(3), &[1]);
assert_eq!(cfg.exits(), vec![NodeId::new(2), NodeId::new(3)]);
assert_eq!(cfg.entry(), NodeId::new(0));
assert_eq!(cfg.node_count(), 4);
assert_eq!(
cfg.node_ids().collect::<Vec<_>>(),
vec![
NodeId::new(0),
NodeId::new(1),
NodeId::new(2),
NodeId::new(3)
]
);
assert_eq!(
cfg.successors(NodeId::new(0)).collect::<Vec<_>>(),
vec![NodeId::new(1), NodeId::new(2)]
);
assert_eq!(
cfg.predecessors(NodeId::new(3)).collect::<Vec<_>>(),
vec![NodeId::new(1)]
);
assert_eq!(cfg.block_successors(99), &[]);
assert_eq!(cfg.block_predecessors(99), &[]);
}
#[test]
fn cfg_adds_exception_handler_edges_without_duplicates() {
let mut ssa = crate::ir::SsaFunction::<MockTarget>::new(0, 0);
ssa.add_block(block(0, SsaOp::Jump { target: 1 }));
ssa.add_block(block(1, SsaOp::Return { value: None }));
ssa.add_block(block(2, SsaOp::Return { value: None }));
ssa.set_exception_handlers(vec![SsaExceptionHandler {
flags: 0,
try_offset: 0,
try_length: 1,
handler_offset: 2,
handler_length: 1,
class_token_or_filter: 0,
try_start_block: Some(0),
try_end_block: Some(1),
handler_start_block: Some(2),
handler_end_block: None,
filter_start_block: None,
}]);
let cfg = SsaCfg::from_ssa(&ssa);
assert_eq!(cfg.block_successors(0), &[1, 2]);
assert_eq!(cfg.block_predecessors(2), &[0]);
}
#[test]
fn traversal_helpers_handle_empty_cfg() {
let ssa = crate::ir::SsaFunction::<MockTarget>::new(0, 0);
let cfg = SsaCfg::from_ssa(&ssa);
assert!(cfg.is_empty());
assert!(cfg.exits().is_empty());
assert!(cfg.postorder().is_empty());
assert!(cfg.reverse_postorder().is_empty());
}
}