use crate::cfg::analysis::find_entry;
use crate::cfg::{BlockId, Cfg};
use petgraph::algo::has_path_connecting;
use petgraph::algo::DfsSpace;
use petgraph::graph::NodeIndex;
use petgraph::visit::Dfs;
use std::collections::HashSet;
pub fn find_reachable(cfg: &Cfg) -> Vec<NodeIndex> {
let entry = match find_entry(cfg) {
Some(e) => e,
None => return vec![],
};
let mut dfs = Dfs::new(cfg, entry);
let mut reachable = Vec::new();
while let Some(node) = dfs.next(cfg) {
reachable.push(node);
}
reachable
}
pub fn find_unreachable(cfg: &Cfg) -> Vec<NodeIndex> {
if find_entry(cfg).is_none() {
return vec![];
}
let reachable: HashSet<_> = find_reachable(cfg).into_iter().collect();
cfg.node_indices()
.filter(|&n| !reachable.contains(&n))
.collect()
}
pub fn is_reachable_from_entry(cfg: &Cfg, block: NodeIndex) -> bool {
let entry = match find_entry(cfg) {
Some(e) => e,
None => return false,
};
has_path_connecting(cfg, entry, block, None)
}
pub fn unreachable_block_ids(cfg: &Cfg) -> Vec<BlockId> {
find_unreachable(cfg)
.iter()
.filter_map(|&idx| cfg.node_weight(idx))
.map(|block| block.id)
.collect()
}
pub fn can_reach(cfg: &Cfg, from: NodeIndex, to: NodeIndex) -> bool {
has_path_connecting(cfg, from, to, None)
}
pub fn can_reach_cached(
cfg: &Cfg,
from: NodeIndex,
to: NodeIndex,
space: &mut DfsSpace<NodeIndex, <Cfg as petgraph::visit::Visitable>::Map>,
) -> bool {
has_path_connecting(cfg, from, to, Some(space))
}
pub struct ReachabilityCache {
space: DfsSpace<NodeIndex, <Cfg as petgraph::visit::Visitable>::Map>,
}
impl ReachabilityCache {
pub fn new(cfg: &Cfg) -> Self {
Self {
space: DfsSpace::new(cfg),
}
}
pub fn can_reach(&mut self, cfg: &Cfg, from: NodeIndex, to: NodeIndex) -> bool {
can_reach_cached(cfg, from, to, &mut self.space)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct BlockImpact {
pub source_block_id: BlockId,
pub reachable_blocks: Vec<BlockId>,
pub reachable_count: usize,
pub max_depth_reached: usize,
pub has_cycles: bool,
}
pub fn find_reachable_from_block(
cfg: &Cfg,
start_block_id: BlockId,
max_depth: Option<usize>,
) -> BlockImpact {
use std::collections::{HashSet, VecDeque};
let start_node = match cfg.node_indices().find(|&n| cfg[n].id == start_block_id) {
Some(n) => n,
None => {
return BlockImpact {
source_block_id: start_block_id,
reachable_blocks: vec![],
reachable_count: 0,
max_depth_reached: 0,
has_cycles: false,
};
}
};
let max_depth = max_depth.unwrap_or(usize::MAX);
let mut visited: HashSet<NodeIndex> = HashSet::new();
let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
let mut reachable_blocks = Vec::new();
let mut max_depth_reached = 0;
let mut has_cycles = false;
queue.push_back((start_node, 0));
visited.insert(start_node);
while let Some((node, depth)) = queue.pop_front() {
max_depth_reached = max_depth_reached.max(depth);
let block_id = cfg[node].id;
reachable_blocks.push(block_id);
if depth >= max_depth {
continue;
}
for neighbor in cfg.neighbors(node) {
if visited.contains(&neighbor) {
has_cycles = true;
} else {
visited.insert(neighbor);
queue.push_back((neighbor, depth + 1));
}
}
}
reachable_blocks.retain(|&id| id != start_block_id);
let reachable_count = reachable_blocks.len();
BlockImpact {
source_block_id: start_block_id,
reachable_blocks,
reachable_count,
max_depth_reached,
has_cycles,
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PathImpact {
pub path_id: String,
pub path_length: usize,
pub unique_blocks_affected: Vec<BlockId>,
pub impact_count: usize,
}
pub fn compute_path_impact(
cfg: &Cfg,
path_block_ids: &[BlockId],
max_depth: Option<usize>,
) -> PathImpact {
use std::collections::HashSet;
let mut all_affected: HashSet<BlockId> = HashSet::new();
for &block_id in path_block_ids {
let impact = find_reachable_from_block(cfg, block_id, max_depth);
all_affected.extend(impact.reachable_blocks);
}
for &block_id in path_block_ids {
all_affected.remove(&block_id);
}
let mut affected_vec: Vec<BlockId> = all_affected.into_iter().collect();
affected_vec.sort();
let impact_count = affected_vec.len();
PathImpact {
path_id: "[computed]".to_string(), path_length: path_block_ids.len(),
unique_blocks_affected: affected_vec,
impact_count,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
fn create_test_cfg_with_unreachable() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let _b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["unreachable code".to_string()],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g
}
#[test]
fn test_find_unreachable() {
let cfg = create_test_cfg_with_unreachable();
let unreachable = find_unreachable(&cfg);
assert_eq!(unreachable.len(), 1);
let block_id = cfg.node_weight(unreachable[0]).unwrap().id;
assert_eq!(block_id, 3);
}
#[test]
fn test_find_reachable() {
let cfg = create_test_cfg_with_unreachable();
let reachable = find_reachable(&cfg);
assert_eq!(reachable.len(), 3);
let ids: Vec<_> = reachable
.iter()
.map(|&idx| cfg.node_weight(idx).unwrap().id)
.collect();
assert!(ids.contains(&0));
assert!(ids.contains(&1));
assert!(ids.contains(&2));
assert!(!ids.contains(&3));
}
#[test]
fn test_is_reachable_from_entry() {
let cfg = create_test_cfg_with_unreachable();
let b0 = NodeIndex::new(0);
let b3 = NodeIndex::new(3);
assert!(is_reachable_from_entry(&cfg, b0));
assert!(!is_reachable_from_entry(&cfg, b3));
}
#[test]
fn test_empty_cfg() {
let cfg: Cfg = DiGraph::new();
assert!(find_unreachable(&cfg).is_empty());
assert!(find_reachable(&cfg).is_empty());
}
#[test]
fn test_fully_reachable_cfg() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
assert!(find_unreachable(&g).is_empty());
}
#[test]
fn test_can_reach_simple() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
assert!(can_reach(&g, b0, b0));
assert!(can_reach(&g, b1, b1));
assert!(can_reach(&g, b2, b2));
assert!(can_reach(&g, b0, b1));
assert!(can_reach(&g, b0, b2));
assert!(can_reach(&g, b1, b2));
assert!(!can_reach(&g, b1, b0));
assert!(!can_reach(&g, b2, b0));
assert!(!can_reach(&g, b2, b1));
}
#[test]
fn test_can_reach_diamond() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
assert!(can_reach(&g, b0, b1));
assert!(can_reach(&g, b0, b2));
assert!(can_reach(&g, b0, b3));
assert!(!can_reach(&g, b1, b2));
assert!(!can_reach(&g, b2, b1));
}
#[test]
fn test_can_reach_cached() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
let mut space = DfsSpace::new(&g);
assert!(can_reach_cached(&g, b0, b1, &mut space));
assert!(!can_reach_cached(&g, b1, b0, &mut space));
}
#[test]
fn test_reachability_cache() {
let mut g = DiGraph::new();
let nodes = (0..4)
.map(|i| {
g.add_node(BasicBlock {
id: i,
db_id: None,
kind: if i == 0 {
BlockKind::Entry
} else if i == 3 {
BlockKind::Exit
} else {
BlockKind::Normal
},
statements: vec![],
terminator: if i < 3 {
Terminator::Goto { target: i + 1 }
} else {
Terminator::Return
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
})
})
.collect::<Vec<_>>();
for i in 0..3 {
g.add_edge(nodes[i], nodes[i + 1], EdgeType::Fallthrough);
}
let mut cache = ReachabilityCache::new(&g);
assert!(cache.can_reach(&g, nodes[0], nodes[3]));
assert!(cache.can_reach(&g, nodes[1], nodes[3]));
assert!(!cache.can_reach(&g, nodes[3], nodes[0]));
}
#[test]
fn test_find_reachable_from_block_linear() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let impact = find_reachable_from_block(&g, 0, None);
assert_eq!(impact.source_block_id, 0);
assert_eq!(impact.reachable_count, 3);
assert!(impact.reachable_blocks.contains(&1));
assert!(impact.reachable_blocks.contains(&2));
assert!(impact.reachable_blocks.contains(&3));
assert!(!impact.has_cycles);
}
#[test]
fn test_find_reachable_from_block_diamond() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let impact = find_reachable_from_block(&g, 0, None);
assert_eq!(impact.source_block_id, 0);
assert_eq!(impact.reachable_count, 3);
assert!(impact.reachable_blocks.contains(&1));
assert!(impact.reachable_blocks.contains(&2));
assert!(impact.reachable_blocks.contains(&3));
}
#[test]
fn test_find_reachable_from_block_max_depth() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let impact = find_reachable_from_block(&g, 0, Some(1));
assert_eq!(impact.source_block_id, 0);
assert_eq!(impact.reachable_count, 1);
assert!(impact.reachable_blocks.contains(&1));
assert!(!impact.reachable_blocks.contains(&2));
assert!(!impact.reachable_blocks.contains(&3));
assert_eq!(impact.max_depth_reached, 1);
}
#[test]
fn test_find_reachable_from_block_not_found() {
let g = DiGraph::new();
let impact = find_reachable_from_block(&g, 99, None);
assert_eq!(impact.source_block_id, 99);
assert_eq!(impact.reachable_count, 0);
assert!(impact.reachable_blocks.is_empty());
}
#[test]
fn test_find_reachable_from_block_with_loop() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b2, b3, EdgeType::LoopExit);
let impact = find_reachable_from_block(&g, 1, Some(10));
assert_eq!(impact.source_block_id, 1);
assert!(impact.has_cycles);
}
#[test]
fn test_compute_path_impact() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let impact = compute_path_impact(&g, &[0, 1, 3], None);
assert_eq!(impact.path_length, 3);
assert!(impact.unique_blocks_affected.contains(&2));
}
}