use crate::cfg::analysis::find_entry;
use crate::cfg::Cfg;
use petgraph::algo::dominators::simple_fast;
use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef;
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone)]
pub struct NaturalLoop {
pub header: NodeIndex,
pub back_edge: (NodeIndex, NodeIndex),
pub body: HashSet<NodeIndex>,
}
impl NaturalLoop {
pub fn contains(&self, node: NodeIndex) -> bool {
self.body.contains(&node)
}
pub fn size(&self) -> usize {
self.body.len()
}
pub fn nesting_level(&self, all_loops: &[NaturalLoop]) -> usize {
let mut level = 0;
for other in all_loops {
if other.header != self.header && other.body.contains(&self.header) {
level = level.max(other.nesting_level(all_loops) + 1);
}
}
level
}
}
pub fn detect_natural_loops(cfg: &Cfg) -> Vec<NaturalLoop> {
let entry = match find_entry(cfg) {
Some(e) => e,
None => return vec![],
};
let dominators = simple_fast(cfg, entry);
let mut loops = Vec::new();
for edge in cfg.edge_references() {
let tail = edge.source();
let header = edge.target();
if let Some(mut tail_dominators) = dominators.dominators(tail) {
if tail_dominators.any(|d| d == header) {
let body = compute_loop_body(cfg, header, tail);
loops.push(NaturalLoop {
header,
back_edge: (tail, header),
body,
});
}
}
}
loops
}
pub fn apply_loop_nesting_depths(cfg: &mut Cfg) {
let loops = detect_natural_loops(cfg);
for node in cfg.node_indices() {
if let Some(block) = cfg.node_weight_mut(node) {
block.coord_y = 0;
}
}
for loop_ in &loops {
for node in &loop_.body {
if let Some(block) = cfg.node_weight_mut(*node) {
block.coord_y += 1;
}
}
}
}
pub fn detect_natural_loops_with_depths(cfg: &mut Cfg) -> Vec<NaturalLoop> {
let loops = detect_natural_loops(cfg);
apply_loop_nesting_depths_from_loops(cfg, &loops);
loops
}
fn apply_loop_nesting_depths_from_loops(cfg: &mut Cfg, loops: &[NaturalLoop]) {
for node in cfg.node_indices() {
if let Some(block) = cfg.node_weight_mut(node) {
block.coord_y = 0;
}
}
for loop_ in loops {
for node in &loop_.body {
if let Some(block) = cfg.node_weight_mut(*node) {
block.coord_y += 1;
}
}
}
}
fn compute_loop_body(cfg: &Cfg, header: NodeIndex, tail: NodeIndex) -> HashSet<NodeIndex> {
let mut body = HashSet::new();
let mut worklist = VecDeque::new();
worklist.push_back(tail);
while let Some(node) = worklist.pop_front() {
if node == header {
continue;
}
if body.contains(&node) {
continue;
}
body.insert(node);
for pred in cfg.neighbors_directed(node, petgraph::Direction::Incoming) {
if pred != header && !body.contains(&pred) {
worklist.push_back(pred);
}
}
}
body.insert(header); body
}
pub fn find_loop_headers(cfg: &Cfg) -> HashSet<NodeIndex> {
detect_natural_loops(cfg)
.into_iter()
.map(|loop_| loop_.header)
.collect()
}
pub fn is_loop_header(cfg: &Cfg, node: NodeIndex) -> bool {
find_loop_headers(cfg).contains(&node)
}
pub fn loops_containing(cfg: &Cfg, node: NodeIndex) -> Vec<NaturalLoop> {
detect_natural_loops(cfg)
.into_iter()
.filter(|loop_| loop_.body.contains(&node))
.collect()
}
pub fn find_nested_loops(cfg: &Cfg) -> Vec<(NaturalLoop, NaturalLoop)> {
let loops = detect_natural_loops(cfg);
let mut nested = Vec::new();
for (i, outer) in loops.iter().enumerate() {
for inner in loops.iter().skip(i + 1) {
if outer.body.contains(&inner.header) {
nested.push((outer.clone(), inner.clone()));
}
}
}
nested
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
fn create_simple_loop_cfg() -> 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::SwitchInt {
targets: vec![2],
otherwise: 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!["loop body".to_string()],
terminator: Terminator::Goto { target: 1 },
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::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
g
}
#[test]
fn test_detect_simple_loop() {
let cfg = create_simple_loop_cfg();
let loops = detect_natural_loops(&cfg);
assert_eq!(loops.len(), 1);
let loop_ = &loops[0];
assert_eq!(loop_.header.index(), 1); assert_eq!(loop_.back_edge.0.index(), 2); assert_eq!(loop_.back_edge.1.index(), 1); assert!(loop_.contains(NodeIndex::new(1)));
assert!(loop_.contains(NodeIndex::new(2)));
assert!(!loop_.contains(NodeIndex::new(0))); assert!(!loop_.contains(NodeIndex::new(3))); }
#[test]
fn test_find_loop_headers() {
let cfg = create_simple_loop_cfg();
let headers = find_loop_headers(&cfg);
assert_eq!(headers.len(), 1);
assert!(headers.contains(&NodeIndex::new(1)));
}
#[test]
fn test_is_loop_header() {
let cfg = create_simple_loop_cfg();
assert!(is_loop_header(&cfg, NodeIndex::new(1)));
assert!(!is_loop_header(&cfg, NodeIndex::new(0)));
assert!(!is_loop_header(&cfg, NodeIndex::new(2)));
}
#[test]
fn test_no_loops() {
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);
let loops = detect_natural_loops(&g);
assert!(loops.is_empty());
}
#[test]
fn test_nested_loops() {
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::SwitchInt {
targets: vec![2],
otherwise: 4,
},
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![3],
otherwise: 1,
},
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::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
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::TrueBranch);
g.add_edge(b1, b4, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b3, b2, EdgeType::LoopBack);
let loops = detect_natural_loops(&g);
assert_eq!(loops.len(), 2);
let nested = find_nested_loops(&g);
assert_eq!(nested.len(), 1); }
#[test]
fn test_empty_cfg() {
let cfg: Cfg = DiGraph::new();
assert!(detect_natural_loops(&cfg).is_empty());
assert!(find_loop_headers(&cfg).is_empty());
}
#[test]
fn test_loops_containing() {
let cfg = create_simple_loop_cfg();
let loops_2 = loops_containing(&cfg, NodeIndex::new(2));
assert_eq!(loops_2.len(), 1);
let loops_0 = loops_containing(&cfg, NodeIndex::new(0));
assert_eq!(loops_0.len(), 0);
}
#[test]
fn test_loop_size() {
let cfg = create_simple_loop_cfg();
let loops = detect_natural_loops(&cfg);
assert_eq!(loops.len(), 1);
assert_eq!(loops[0].size(), 2); }
#[test]
fn test_nesting_level() {
let cfg = create_simple_loop_cfg();
let loops = detect_natural_loops(&cfg);
assert_eq!(loops.len(), 1);
assert_eq!(loops[0].nesting_level(&loops), 0);
}
#[test]
fn test_nesting_level_nested() {
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::SwitchInt {
targets: vec![2],
otherwise: 4,
},
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![3],
otherwise: 1,
},
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::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
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::TrueBranch);
g.add_edge(b1, b4, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
g.add_edge(b3, b2, EdgeType::LoopBack);
let loops = detect_natural_loops(&g);
assert_eq!(loops.len(), 2);
let outer_loop = loops.iter().find(|l| l.header.index() == 1).unwrap();
let inner_loop = loops.iter().find(|l| l.header.index() == 2).unwrap();
assert_eq!(outer_loop.nesting_level(&loops), 0);
assert_eq!(inner_loop.nesting_level(&loops), 1);
}
#[test]
fn test_apply_loop_nesting_depths_simple_loop() {
let mut cfg = create_simple_loop_cfg();
apply_loop_nesting_depths(&mut cfg);
assert_eq!(
cfg[NodeIndex::new(0)].coord_y,
0,
"Entry should not be in loop"
);
assert_eq!(
cfg[NodeIndex::new(1)].coord_y,
1,
"Loop header should have depth 1"
);
assert_eq!(
cfg[NodeIndex::new(2)].coord_y,
1,
"Loop body should have depth 1"
);
assert_eq!(
cfg[NodeIndex::new(3)].coord_y,
0,
"Exit should not be in loop"
);
}
#[test]
fn test_apply_loop_nesting_depths_nested_loops() {
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::SwitchInt {
targets: vec![2],
otherwise: 4,
},
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![3],
otherwise: 1,
},
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::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
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::TrueBranch);
g.add_edge(b1, b4, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::Fallthrough); g.add_edge(b3, b2, EdgeType::Fallthrough);
apply_loop_nesting_depths(&mut g);
assert_eq!(g[b0].coord_y, 0, "Entry should not be in a loop");
assert_eq!(g[b1].coord_y, 1, "Outer loop header should have depth 1");
assert_eq!(g[b2].coord_y, 2, "Inner loop header should have depth 2");
assert_eq!(g[b3].coord_y, 2, "Inner loop body should have depth 2");
assert_eq!(g[b4].coord_y, 0, "Exit should not be in a loop");
}
#[test]
fn test_detect_natural_loops_with_depths_simple_loop() {
let mut cfg = create_simple_loop_cfg();
let loops = detect_natural_loops_with_depths(&mut cfg);
assert_eq!(loops.len(), 1, "Should detect exactly one loop");
assert_eq!(
cfg[NodeIndex::new(0)].coord_y,
0,
"Entry should not be in loop"
);
assert_eq!(
cfg[NodeIndex::new(1)].coord_y,
1,
"Loop header should have depth 1"
);
assert_eq!(
cfg[NodeIndex::new(2)].coord_y,
1,
"Loop body should have depth 1"
);
}
#[test]
fn test_apply_loop_nesting_depths_no_loops() {
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);
apply_loop_nesting_depths(&mut g);
assert_eq!(g[b0].coord_y, 0, "Entry should not be in a loop");
assert_eq!(g[b1].coord_y, 0, "Exit should not be in a loop");
}
#[test]
fn test_loop_nesting_matches_nesting_level_method() {
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::SwitchInt {
targets: vec![2],
otherwise: 4,
},
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![3],
otherwise: 1,
},
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::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
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::TrueBranch);
g.add_edge(b1, b4, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::Fallthrough);
g.add_edge(b3, b2, EdgeType::Fallthrough);
let loops = detect_natural_loops_with_depths(&mut g);
for loop_ in &loops {
let expected_coord_y = (loop_.nesting_level(&loops) + 1) as i64;
let actual_coord_y = g[loop_.header].coord_y;
assert_eq!(
actual_coord_y, expected_coord_y,
"Loop header {:?} coord_y should match nesting_level + 1",
loop_.header
);
}
}
}