use ahash::{AHashMap, AHashSet};
use std::collections::VecDeque;
use crate::algo::dominators::DominatorResult;
use crate::errors::SqliteGraphError;
use crate::graph::SqliteGraph;
use crate::progress::ProgressCallback;
#[derive(Debug, Clone)]
pub struct NaturalLoop {
pub header: i64,
pub back_edges: Vec<(i64, i64)>,
pub body: AHashSet<i64>,
}
impl NaturalLoop {
pub fn contains(&self, node: i64) -> bool {
self.header == node || self.body.contains(&node)
}
pub fn all_nodes(&self) -> AHashSet<i64> {
let mut nodes = self.body.clone();
nodes.insert(self.header);
nodes
}
pub fn is_nested_in(&self, parent: &NaturalLoop) -> bool {
parent.contains(self.header)
}
pub fn nesting_depth_in(&self, parent: &NaturalLoop) -> usize {
if !parent.contains(self.header) {
return 0;
}
1
}
pub fn back_edge_count(&self) -> usize {
self.back_edges.len()
}
pub fn body_size(&self) -> usize {
self.body.len()
}
pub fn size(&self) -> usize {
self.body.len() + 1
}
}
#[derive(Debug, Clone)]
pub struct NaturalLoopsResult {
pub loops: AHashMap<i64, NaturalLoop>,
}
impl NaturalLoopsResult {
pub fn loop_with_header(&self, header: i64) -> Option<&NaturalLoop> {
self.loops.get(&header)
}
pub fn is_loop_header(&self, node: i64) -> bool {
self.loops.contains_key(&node)
}
pub fn nesting_depth(&self, node: i64) -> usize {
self.loops.values().filter(|l| l.contains(node)).count()
}
pub fn loops_at_depth(&self, depth: usize) -> Vec<&NaturalLoop> {
self.loops
.values()
.filter(|l| self.nesting_depth(l.header) == depth)
.collect()
}
pub fn nesting_tree(&self) -> AHashMap<i64, Vec<i64>> {
let mut tree: AHashMap<i64, Vec<i64>> = AHashMap::new();
for (&header, _loop_) in &self.loops {
for (&potential_parent, parent_loop) in &self.loops {
if header == potential_parent {
continue;
}
if parent_loop.contains(header) {
let is_direct = !self.loops.values().any(|other| {
other.header != potential_parent
&& other.header != header
&& parent_loop.contains(other.header)
&& other.contains(header)
});
if is_direct {
tree.entry(potential_parent).or_default().push(header);
}
}
}
}
tree
}
pub fn count(&self) -> usize {
self.loops.len()
}
pub fn headers(&self) -> Vec<i64> {
self.loops.keys().copied().collect()
}
pub fn iter(&self) -> impl Iterator<Item = (i64, &NaturalLoop)> {
self.loops.iter().map(|(&header, loop_)| (header, loop_))
}
}
pub fn natural_loops(
graph: &SqliteGraph,
dom_result: &DominatorResult,
) -> Result<NaturalLoopsResult, SqliteGraphError> {
let all_nodes = graph.all_entity_ids()?;
if all_nodes.is_empty() {
return Ok(NaturalLoopsResult {
loops: AHashMap::new(),
});
}
natural_loops_internal(graph, dom_result, &all_nodes)
}
pub fn natural_loops_from_exit(
graph: &SqliteGraph,
) -> Result<NaturalLoopsResult, SqliteGraphError> {
let all_nodes = graph.all_entity_ids()?;
if all_nodes.is_empty() {
return Ok(NaturalLoopsResult {
loops: AHashMap::new(),
});
}
let mut entries = Vec::new();
for &node in &all_nodes {
let incoming = graph.fetch_incoming(node)?;
if incoming.is_empty() {
entries.push(node);
}
}
if entries.is_empty() {
return Err(SqliteGraphError::query(
"Cannot compute natural loops: graph has no entry nodes (all nodes have incoming edges)",
));
}
if entries.len() > 1 {
return Err(SqliteGraphError::query(format!(
"Cannot compute natural loops: graph has {} entry nodes (expected 1)",
entries.len()
)));
}
let entry = entries[0];
let dom_result = super::dominators::dominators(graph, entry)?;
natural_loops(graph, &dom_result)
}
pub fn natural_loops_with_progress<F>(
graph: &SqliteGraph,
dom_result: &DominatorResult,
progress: &F,
) -> Result<NaturalLoopsResult, SqliteGraphError>
where
F: ProgressCallback,
{
let all_nodes = graph.all_entity_ids()?;
if all_nodes.is_empty() {
return Ok(NaturalLoopsResult {
loops: AHashMap::new(),
});
}
let mut total_edges = 0;
for &node in &all_nodes {
let outgoing = graph.fetch_outgoing(node)?;
total_edges += outgoing.len();
}
progress.on_progress(
0,
Some(total_edges),
&format!("Finding natural loops: 0/{} edges checked", total_edges),
);
let mut loops: AHashMap<i64, NaturalLoop> = AHashMap::new();
let mut edges_checked = 0;
for &node in &all_nodes {
let outgoing = graph.fetch_outgoing(node)?;
for &target in &outgoing {
if dom_result.dominates(target, node) {
if node == target {
edges_checked += 1;
continue;
}
let header = target;
let loop_ = loops.entry(header).or_insert_with(|| NaturalLoop {
header,
back_edges: Vec::new(),
body: AHashSet::new(),
});
loop_.back_edges.push((node, header));
let body = compute_loop_body(graph, header, node)?;
loop_.body.extend(body);
}
edges_checked += 1;
if edges_checked % 100 == 0 || edges_checked == total_edges {
progress.on_progress(
edges_checked,
Some(total_edges),
&format!(
"Finding natural loops: {}/{} edges checked",
edges_checked, total_edges
),
);
}
}
}
progress.on_complete();
Ok(NaturalLoopsResult { loops })
}
fn natural_loops_internal(
graph: &SqliteGraph,
dom_result: &DominatorResult,
all_nodes: &[i64],
) -> Result<NaturalLoopsResult, SqliteGraphError> {
let mut loops: AHashMap<i64, NaturalLoop> = AHashMap::new();
for &node in all_nodes {
let outgoing = graph.fetch_outgoing(node)?;
for &target in &outgoing {
if dom_result.dominates(target, node) {
if node == target {
continue;
}
let header = target;
let loop_ = loops.entry(header).or_insert_with(|| NaturalLoop {
header,
back_edges: Vec::new(),
body: AHashSet::new(),
});
loop_.back_edges.push((node, header));
let body = compute_loop_body(graph, header, node)?;
loop_.body.extend(body);
}
}
}
Ok(NaturalLoopsResult { loops })
}
fn compute_loop_body(
graph: &SqliteGraph,
header: i64,
tail: i64,
) -> Result<AHashSet<i64>, SqliteGraphError> {
let mut body = AHashSet::new();
let mut visited = AHashSet::new();
let mut queue = VecDeque::new();
queue.push_back(tail);
visited.insert(tail);
while let Some(node) = queue.pop_front() {
if node == header {
continue;
}
body.insert(node);
let successors = graph.fetch_outgoing(node)?;
for &succ in &successors {
if !visited.contains(&succ) {
visited.insert(succ);
queue.push_back(succ);
}
}
}
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GraphEdge, GraphEntity};
fn create_single_loop_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..3 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (1, 2), (2, 1)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
fn create_nested_loops_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (1, 2), (2, 3), (3, 2), (3, 1)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
fn create_while_loop_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (1, 2), (2, 1), (1, 3)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
fn create_diamond_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (0, 2), (1, 3), (2, 3)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
fn create_irreducible_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 1), (3, 2)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
fn create_multiple_back_edges_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let edges = vec![(0, 1), (1, 2), (2, 1), (1, 3), (3, 1)];
for (from_idx, to_idx) in edges {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[from_idx],
to_id: entity_ids[to_idx],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
graph
}
#[test]
fn test_natural_loops_single_loop() {
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 1, "Should find 1 loop");
let loop_ = loops
.loop_with_header(entity_ids[1])
.expect("Should have loop with header 1");
assert_eq!(loop_.header, entity_ids[1], "Loop header should be 1");
assert_eq!(loop_.back_edges.len(), 1, "Should have 1 back-edge");
assert_eq!(
loop_.back_edges[0],
(entity_ids[2], entity_ids[1]),
"Back-edge should be (2, 1)"
);
assert!(
loop_.body.contains(&entity_ids[2]),
"Body should contain node 2"
);
assert_eq!(loop_.body.len(), 1, "Body should have 1 node");
}
#[test]
fn test_natural_loop_contains() {
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
let loop_ = loops
.loop_with_header(entity_ids[1])
.expect("Should have loop");
assert!(loop_.contains(entity_ids[1]), "Header should be in loop");
assert!(loop_.contains(entity_ids[2]), "Body node should be in loop");
assert!(
!loop_.contains(entity_ids[0]),
"Entry node should not be in loop"
);
}
#[test]
fn test_natural_loop_all_nodes() {
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
let loop_ = loops
.loop_with_header(entity_ids[1])
.expect("Should have loop");
let all_nodes = loop_.all_nodes();
assert_eq!(all_nodes.len(), 2, "Loop should have 2 nodes total");
assert!(all_nodes.contains(&entity_ids[1]), "Should contain header");
assert!(all_nodes.contains(&entity_ids[2]), "Should contain body");
}
#[test]
fn test_natural_loops_nested() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 2, "Should find 2 loops");
let outer = loops
.loop_with_header(entity_ids[1])
.expect("Should have outer loop");
assert_eq!(outer.back_edges.len(), 1, "Outer should have 1 back-edge");
assert_eq!(
outer.back_edges[0],
(entity_ids[3], entity_ids[1]),
"Outer back-edge should be (3, 1)"
);
let inner = loops
.loop_with_header(entity_ids[2])
.expect("Should have inner loop");
assert_eq!(inner.back_edges.len(), 1, "Inner should have 1 back-edge");
assert_eq!(
inner.back_edges[0],
(entity_ids[3], entity_ids[2]),
"Inner back-edge should be (3, 2)"
);
}
#[test]
fn test_natural_loop_nesting() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert!(loops.count() >= 1, "Should find at least one loop");
let loops_vec: Vec<_> = loops.loops_at_depth(1);
for loop_result in loops_vec {
assert!(
!loop_result.body.is_empty(),
"Loop body should not be empty"
);
}
}
#[test]
fn test_natural_loop_nesting_tree() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
let tree = loops.nesting_tree();
assert!(
tree.contains_key(&entity_ids[1]),
"Tree should have outer loop"
);
let children = tree.get(&entity_ids[1]).expect("Should have children");
assert_eq!(children.len(), 1, "Outer should have 1 child");
assert_eq!(children[0], entity_ids[2], "Child should be inner loop");
}
#[test]
fn test_natural_loops_nesting_depth() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(
loops.nesting_depth(entity_ids[0]),
0,
"Entry should have depth 0"
);
if loops.count() > 0 {
for &header in &loops.headers() {
assert!(
loops.nesting_depth(header) >= 1,
"Loop header should have depth >= 1"
);
}
}
}
#[test]
fn test_natural_loops_multiple_back_edges() {
let graph = create_multiple_back_edges_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 1, "Should find 1 loop");
let loop_ = loops
.loop_with_header(entity_ids[1])
.expect("Should have loop");
assert_eq!(loop_.back_edges.len(), 2, "Should have 2 back-edges");
assert!(
loop_.back_edges.contains(&(entity_ids[2], entity_ids[1])),
"Should have back-edge (2, 1)"
);
assert!(
loop_.back_edges.contains(&(entity_ids[3], entity_ids[1])),
"Should have back-edge (3, 1)"
);
}
#[test]
fn test_natural_loops_grouped_by_header() {
let graph = create_multiple_back_edges_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert!(
loops.is_loop_header(entity_ids[1]),
"Node 1 should be loop header"
);
assert!(
!loops.is_loop_header(entity_ids[2]),
"Node 2 should not be loop header"
);
assert!(
!loops.is_loop_header(entity_ids[3]),
"Node 3 should not be loop header"
);
}
#[test]
fn test_natural_loops_empty_graph() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let dom_result = DominatorResult {
dom: AHashMap::new(),
idom: AHashMap::new(),
};
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 0, "Should have 0 loops in empty graph");
assert_eq!(loops.headers().len(), 0, "Should have 0 headers");
}
#[test]
fn test_natural_loops_single_node() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: "single".to_string(),
file_path: Some("single.rs".to_string()),
data: serde_json::json!({}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
let entity_ids = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 0, "Should have 0 loops with single node");
}
#[test]
fn test_natural_loops_linear_chain() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
for i in 0..entity_ids.len().saturating_sub(1) {
let edge = GraphEdge {
id: 0,
from_id: entity_ids[i],
to_id: entity_ids[i + 1],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 0, "Linear chain should have 0 loops");
}
#[test]
fn test_natural_loops_diamond() {
let graph = create_diamond_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 0, "Diamond CFG should have 0 natural loops");
}
#[test]
fn test_natural_loops_irreducible_cfg() {
let graph = create_irreducible_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(
loops.count(),
0,
"Irreducible CFG should have 0 natural loops"
);
}
#[test]
fn test_natural_loops_no_dominance_no_loop() {
let graph = create_diamond_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
assert!(
dom_result.dominates(entity_ids[0], entity_ids[3]),
"0 should dominate 3"
);
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 0, "Should have no loops");
}
#[test]
fn test_natural_loops_loop_with_header() {
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
let loop_ = loops.loop_with_header(entity_ids[1]);
assert!(loop_.is_some(), "Should find loop with header 1");
assert_eq!(loop_.unwrap().header, entity_ids[1]);
let loop_ = loops.loop_with_header(entity_ids[0]);
assert!(loop_.is_none(), "Should not find loop with header 0");
}
#[test]
fn test_natural_loops_is_loop_header() {
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert!(
loops.is_loop_header(entity_ids[1]),
"Node 1 should be loop header"
);
assert!(
!loops.is_loop_header(entity_ids[0]),
"Node 0 should not be loop header"
);
assert!(
!loops.is_loop_header(entity_ids[2]),
"Node 2 should not be loop header"
);
}
#[test]
fn test_natural_loops_count() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 2, "Should have 2 loops");
}
#[test]
fn test_natural_loops_headers() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
let headers = loops.headers();
assert_eq!(headers.len(), 2, "Should have 2 headers");
assert!(headers.contains(&entity_ids[1]), "Should contain header 1");
assert!(headers.contains(&entity_ids[2]), "Should contain header 2");
}
#[test]
fn test_natural_loops_loops_at_depth() {
let graph = create_nested_loops_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert!(loops.count() >= 1, "Should find at least one loop");
let depth1 = loops.loops_at_depth(1);
for loop_result in &depth1 {
assert!(
!loop_result.body.is_empty(),
"Loop should have non-empty body"
);
}
}
#[test]
fn test_natural_loops_with_progress_reports() {
use crate::progress::NoProgress;
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let progress = NoProgress;
let loops = natural_loops_with_progress(&graph, &dom_result, &progress)
.expect("Failed to compute natural loops with progress");
assert_eq!(loops.count(), 1, "Should find 1 loop");
}
#[test]
fn test_natural_loops_progress_completes() {
use crate::progress::NoProgress;
let graph = create_single_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let progress = NoProgress;
let result = natural_loops_with_progress(&graph, &dom_result, &progress);
assert!(result.is_ok(), "Progress variant should succeed");
assert_eq!(result.unwrap().count(), 1, "Should find 1 loop");
}
#[test]
fn test_natural_loops_while_loop() {
let graph = create_while_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result =
crate::algo::dominators(&graph, entry).expect("Failed to compute dominators");
let loops = natural_loops(&graph, &dom_result).expect("Failed to compute natural loops");
assert_eq!(loops.count(), 1, "Should find 1 loop");
let loop_ = loops
.loop_with_header(entity_ids[1])
.expect("Should have loop");
assert_eq!(loop_.back_edges.len(), 1, "Should have 1 back-edge");
assert_eq!(
loop_.back_edges[0],
(entity_ids[2], entity_ids[1]),
"Back-edge should be (2, 1)"
);
assert!(
loop_.body.contains(&entity_ids[2]),
"Body should contain node 2"
);
assert!(
!loop_.contains(entity_ids[3]),
"Exit node should not be in loop"
);
}
}