use ahash::{AHashMap, AHashSet};
use crate::errors::SqliteGraphError;
use crate::graph::SqliteGraph;
use crate::progress::ProgressCallback;
#[derive(Debug, Clone)]
pub struct DominatorResult {
pub dom: AHashMap<i64, AHashSet<i64>>,
pub idom: AHashMap<i64, Option<i64>>,
}
impl DominatorResult {
pub fn dominates(&self, dominator: i64, node: i64) -> bool {
self.dom
.get(&node)
.map(|set| set.contains(&dominator))
.unwrap_or(false)
}
pub fn immediate_dominator(&self, node: i64) -> Option<i64> {
self.idom.get(&node).copied().flatten()
}
pub fn is_entry(&self, node: i64) -> bool {
self.idom
.get(&node)
.map(|idom| idom.is_none())
.unwrap_or(false)
}
}
pub fn dominators(graph: &SqliteGraph, entry: i64) -> Result<DominatorResult, SqliteGraphError> {
let all_nodes = graph.all_entity_ids()?;
if !all_nodes.contains(&entry) {
return Err(SqliteGraphError::not_found(format!(
"Entry node {} not found in graph",
entry
)));
}
let mut dom = initialize_dominators(&all_nodes, entry);
let order = reverse_postorder(graph, entry)?;
let max_iterations = 1000;
let mut iteration = 0;
loop {
let changed = iterate_dominators(graph, &mut dom, &order)?;
if !changed {
break;
}
iteration += 1;
if iteration >= max_iterations {
return Err(SqliteGraphError::query(format!(
"Dominator computation failed to converge after {} iterations",
max_iterations
)));
}
}
let idom = extract_immediate_dominators(&dom, entry);
Ok(DominatorResult { dom, idom })
}
pub fn dominators_with_progress<F>(
graph: &SqliteGraph,
entry: i64,
progress: &F,
) -> Result<DominatorResult, SqliteGraphError>
where
F: ProgressCallback,
{
let all_nodes = graph.all_entity_ids()?;
if !all_nodes.contains(&entry) {
return Err(SqliteGraphError::not_found(format!(
"Entry node {} not found in graph",
entry
)));
}
let mut dom = initialize_dominators(&all_nodes, entry);
let order = reverse_postorder(graph, entry)?;
let max_iterations = 1000;
let mut iteration = 0;
loop {
let changed = iterate_dominators(graph, &mut dom, &order)?;
progress.on_progress(
iteration + 1,
None,
&format!(
"Dominator iteration {}: {} nodes processed",
iteration + 1,
order.len()
),
);
if !changed {
break;
}
iteration += 1;
if iteration >= max_iterations {
return Err(SqliteGraphError::query(format!(
"Dominator computation failed to converge after {} iterations",
max_iterations
)));
}
}
progress.on_complete();
let idom = extract_immediate_dominators(&dom, entry);
Ok(DominatorResult { dom, idom })
}
fn initialize_dominators(all_nodes: &[i64], entry: i64) -> AHashMap<i64, AHashSet<i64>> {
let mut dom = AHashMap::new();
let universal: AHashSet<i64> = all_nodes.iter().copied().collect();
for &node in all_nodes {
if node == entry {
let mut entry_dom = AHashSet::new();
entry_dom.insert(entry);
dom.insert(entry, entry_dom);
} else {
dom.insert(node, universal.clone());
}
}
dom
}
fn reverse_postorder(graph: &SqliteGraph, entry: i64) -> Result<Vec<i64>, SqliteGraphError> {
let mut visited = AHashSet::new();
let mut postorder = Vec::new();
let mut stack = vec![(entry, false)];
while let Some((node, visited_children)) = stack.pop() {
if visited_children {
postorder.push(node);
} else {
if !visited.insert(node) {
continue;
}
stack.push((node, true));
let successors = graph.fetch_outgoing(node)?;
for &succ in successors.iter().rev() {
if !visited.contains(&succ) {
stack.push((succ, false));
}
}
}
}
postorder.reverse();
Ok(postorder)
}
fn iterate_dominators(
graph: &SqliteGraph,
dom: &mut AHashMap<i64, AHashSet<i64>>,
order: &[i64],
) -> Result<bool, SqliteGraphError> {
let mut changed = false;
for &node in order.iter().skip(1) {
if node == order.first().copied().unwrap_or(node) {
continue;
}
let predecessors = graph.fetch_incoming(node)?;
if predecessors.is_empty() {
continue;
}
let mut new_dom_set: Option<AHashSet<i64>> = None;
for &pred in &predecessors {
if let Some(pred_dom) = dom.get(&pred) {
if let Some(current) = &mut new_dom_set {
current.retain(|n| pred_dom.contains(n));
} else {
new_dom_set = Some(pred_dom.clone());
}
}
}
if let Some(mut intersected) = new_dom_set {
intersected.insert(node);
let old_dom_set = dom.get(&node);
if old_dom_set.map(|old| old != &intersected).unwrap_or(true) {
dom.insert(node, intersected);
changed = true;
}
}
}
Ok(changed)
}
fn extract_immediate_dominators(
dom: &AHashMap<i64, AHashSet<i64>>,
entry: i64,
) -> AHashMap<i64, Option<i64>> {
let mut idom = AHashMap::new();
idom.insert(entry, None);
for (&node, dom_set) in dom {
if node == entry {
continue;
}
let strict_dominators: Vec<i64> = dom_set.iter().copied().filter(|&d| d != node).collect();
if strict_dominators.is_empty() {
idom.insert(node, None);
continue;
}
let mut immediate_dominator = None;
let mut max_dom_size = 0;
for &candidate in &strict_dominators {
if let Some(candidate_dom) = dom.get(&candidate) {
let is_dominated_by_all_others = strict_dominators
.iter()
.all(|&other| other == candidate || candidate_dom.contains(&other));
if is_dominated_by_all_others {
immediate_dominator = Some(candidate);
break;
}
let dom_size = candidate_dom.len();
if dom_size > max_dom_size {
max_dom_size = dom_size;
immediate_dominator = Some(candidate);
}
}
}
idom.insert(node, immediate_dominator);
}
idom
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GraphEdge, GraphEntity};
fn create_linear_chain() -> 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");
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");
}
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_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() -> 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_multiple_exits() -> 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), (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
}
#[test]
fn test_dominators_linear_chain() {
let graph = create_linear_chain();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let result = dominators(&graph, entry).expect("Failed to compute dominators");
for &node in &entity_ids {
assert!(
result.dominates(entry, node),
"Entry should dominate node {}",
node
);
}
assert!(result.dominates(entity_ids[1], entity_ids[1]));
assert!(result.dominates(entity_ids[1], entity_ids[2]));
assert!(result.dominates(entity_ids[1], entity_ids[3]));
assert!(result.dominates(entity_ids[2], entity_ids[2]));
assert!(result.dominates(entity_ids[2], entity_ids[3]));
assert!(result.dominates(entity_ids[3], entity_ids[3]));
assert!(!result.dominates(entity_ids[3], entity_ids[0]));
}
#[test]
fn test_dominators_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 result = dominators(&graph, entry).expect("Failed to compute dominators");
for &node in &entity_ids {
assert!(
result.dominates(entry, node),
"Entry should dominate {}",
node
);
}
assert!(
result.dominates(entity_ids[1], entity_ids[1]),
"Node 2 should dominate itself"
);
assert!(
!result.dominates(entity_ids[1], entity_ids[3]),
"Node 2 should NOT dominate exit"
);
assert!(
!result.dominates(entity_ids[1], entity_ids[2]),
"Node 2 should NOT dominate node 3"
);
assert!(
result.dominates(entity_ids[2], entity_ids[2]),
"Node 3 should dominate itself"
);
assert!(
!result.dominates(entity_ids[2], entity_ids[3]),
"Node 3 should NOT dominate exit"
);
assert!(
!result.dominates(entity_ids[2], entity_ids[1]),
"Node 3 should NOT dominate node 2"
);
}
#[test]
fn test_dominators_loop() {
let graph = create_loop_cfg();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let result = dominators(&graph, entry).expect("Failed to compute dominators");
assert!(result.dominates(entity_ids[1], entity_ids[1]));
assert!(result.dominates(entity_ids[1], entity_ids[2]));
assert!(result.dominates(entity_ids[2], entity_ids[2]));
assert!(!result.dominates(entity_ids[2], entity_ids[1]));
}
#[test]
fn test_immediate_dominators_linear() {
let graph = create_linear_chain();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let result = dominators(&graph, entry).expect("Failed to compute dominators");
eprintln!("Entity IDs: {:?}", entity_ids);
eprintln!("Entry: {}", entry);
eprintln!("Dominators: {:?}", result.dom);
eprintln!("IDOM: {:?}", result.idom);
assert_eq!(
result.immediate_dominator(entry),
None,
"Entry should have no immediate dominator"
);
assert_eq!(
result.immediate_dominator(entity_ids[1]),
Some(entry),
"idom(1) should be 0"
);
assert_eq!(
result.immediate_dominator(entity_ids[2]),
Some(entity_ids[1]),
"idom(2) should be 1"
);
assert_eq!(
result.immediate_dominator(entity_ids[3]),
Some(entity_ids[2]),
"idom(3) should be 2"
);
}
#[test]
fn test_immediate_dominators_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 result = dominators(&graph, entry).expect("Failed to compute dominators");
assert_eq!(result.immediate_dominator(entry), None);
assert_eq!(
result.immediate_dominator(entity_ids[1]),
Some(entry),
"idom(1) should be entry"
);
assert_eq!(
result.immediate_dominator(entity_ids[2]),
Some(entry),
"idom(2) should be entry"
);
assert_eq!(
result.immediate_dominator(entity_ids[3]),
Some(entry),
"idom(3) should be entry (both paths converge)"
);
}
#[test]
fn test_dominators_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 result = dominators(&graph, entry).expect("Failed to compute dominators");
assert!(result.dominates(entry, entry));
assert_eq!(result.immediate_dominator(entry), None);
assert_eq!(result.dom.len(), 1, "Should have 1 node in dom sets");
}
#[test]
fn test_dominators_empty_graph() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let result = dominators(&graph, 999);
assert!(result.is_err(), "Should fail on empty graph");
}
#[test]
fn test_dominators_nonexistent_entry() {
let graph = create_linear_chain();
let result = dominators(&graph, 999);
assert!(result.is_err(), "Should fail for nonexistent entry");
if let Err(SqliteGraphError::NotFound(msg)) = result {
assert!(msg.contains("999"), "Error should mention node 999");
} else {
panic!("Expected NotFound error");
}
}
#[test]
fn test_dominators_self_dominance() {
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 result = dominators(&graph, entry).expect("Failed to compute dominators");
for &node in &entity_ids {
assert!(
result.dominates(node, node),
"Node {} should dominate itself",
node
);
}
}
#[test]
fn test_dominators_with_progress() {
use crate::progress::NoProgress;
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 progress = NoProgress;
let result_with = dominators_with_progress(&graph, entry, &progress).expect("Failed");
let result_without = dominators(&graph, entry).expect("Failed");
assert_eq!(
result_with.dom.len(),
result_without.dom.len(),
"Should have same number of nodes"
);
for (&node, dom_set) in &result_without.dom {
assert!(
result_with.dom.contains_key(&node),
"Progress result missing node {}",
node
);
assert_eq!(
result_with.dom.get(&node),
Some(dom_set),
"Dominance sets differ for node {}",
node
);
}
assert_eq!(result_with.idom, result_without.idom);
}
#[test]
fn test_dominators_is_entry() {
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 result = dominators(&graph, entry).expect("Failed to compute dominators");
assert!(
result.is_entry(entry),
"Entry should be identified as entry"
);
assert!(
!result.is_entry(entity_ids[1]),
"Non-entry should not be entry"
);
assert!(
!result.is_entry(entity_ids[2]),
"Non-entry should not be entry"
);
assert!(
!result.is_entry(entity_ids[3]),
"Non-entry should not be entry"
);
}
#[test]
fn test_dominators_nested_loops() {
let graph = create_nested_loops();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let result = dominators(&graph, entry).expect("Failed to compute dominators");
for &node in &entity_ids {
assert!(
result.dominates(entry, node),
"Entry should dominate {}",
node
);
}
assert!(result.dominates(entity_ids[1], entity_ids[1]));
assert!(result.dominates(entity_ids[1], entity_ids[2]));
assert!(result.dominates(entity_ids[1], entity_ids[3]));
assert!(result.dominates(entity_ids[2], entity_ids[2]));
assert!(result.dominates(entity_ids[2], entity_ids[3]));
}
#[test]
fn test_dominators_multiple_exits() {
let graph = create_multiple_exits();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let result = dominators(&graph, entry).expect("Failed to compute dominators");
for &node in &entity_ids {
assert!(
result.dominates(entry, node),
"Entry should dominate {}",
node
);
}
assert!(
result.dominates(entity_ids[1], entity_ids[1]),
"Node 2 should dominate itself"
);
assert!(
result.dominates(entity_ids[1], entity_ids[2]),
"Node 2 should dominate exit 3"
);
assert!(
result.dominates(entity_ids[1], entity_ids[3]),
"Node 2 should dominate exit 4"
);
assert!(
!result.dominates(entity_ids[2], entity_ids[3]),
"Exit 3 should NOT dominate exit 4"
);
assert!(
!result.dominates(entity_ids[3], entity_ids[2]),
"Exit 4 should NOT dominate exit 3"
);
}
}