use ahash::{AHashMap, AHashSet};
use crate::errors::SqliteGraphError;
use crate::graph::SqliteGraph;
use crate::progress::ProgressCallback;
use super::dominators::DominatorResult;
#[derive(Debug, Clone)]
pub struct DominanceFrontierResult {
pub frontiers: AHashMap<i64, AHashSet<i64>>,
}
impl DominanceFrontierResult {
pub fn frontier(&self, node: i64) -> Option<&AHashSet<i64>> {
self.frontiers.get(&node)
}
pub fn in_frontier(&self, n: i64, m: i64) -> bool {
self.frontier(n)
.map(|set| set.contains(&m))
.unwrap_or(false)
}
}
#[derive(Debug, Clone)]
pub struct IteratedDominanceFrontierResult {
pub phi_nodes: AHashSet<i64>,
pub iterations: usize,
}
pub fn dominance_frontiers(
graph: &SqliteGraph,
dom_result: &DominatorResult,
) -> Result<DominanceFrontierResult, SqliteGraphError> {
let all_nodes = graph.all_entity_ids()?;
let mut frontiers: AHashMap<i64, AHashSet<i64>> = AHashMap::new();
if all_nodes.is_empty() {
return Ok(DominanceFrontierResult { frontiers });
}
let idom = &dom_result.idom;
for &n in &all_nodes {
let predecessors = graph.fetch_incoming(n)?;
for &p in &predecessors {
let mut runner = p;
let idom_of_n = idom.get(&n).copied().flatten();
loop {
if Some(runner) == idom_of_n {
break;
}
let idom_of_runner = idom.get(&runner).copied().flatten();
if idom_of_runner.is_none() && idom_of_n.is_none() && runner != n {
if idom.get(&runner).is_some() || runner == n {
break;
}
}
frontiers.entry(runner).or_default().insert(n);
if let Some(next_runner) = idom_of_runner {
runner = next_runner;
} else {
break;
}
}
}
}
Ok(DominanceFrontierResult { frontiers })
}
pub fn dominance_frontiers_with_progress<F>(
graph: &SqliteGraph,
dom_result: &DominatorResult,
progress: &F,
) -> Result<DominanceFrontierResult, SqliteGraphError>
where
F: ProgressCallback,
{
let all_nodes = graph.all_entity_ids()?;
let total = all_nodes.len();
let mut frontiers: AHashMap<i64, AHashSet<i64>> = AHashMap::new();
if all_nodes.is_empty() {
progress.on_complete();
return Ok(DominanceFrontierResult { frontiers });
}
let idom = &dom_result.idom;
for (idx, &n) in all_nodes.iter().enumerate() {
let predecessors = graph.fetch_incoming(n)?;
progress.on_progress(
idx + 1,
Some(total),
&format!(
"Computing DF for node {}: {} predecessors",
n,
predecessors.len()
),
);
let idom_of_n = idom.get(&n).copied().flatten();
for &p in &predecessors {
let mut runner = p;
loop {
if Some(runner) == idom_of_n {
break;
}
let idom_of_runner = idom.get(&runner).copied().flatten();
if idom_of_runner.is_none() && idom_of_n.is_none() && runner != n {
break;
}
frontiers.entry(runner).or_default().insert(n);
if let Some(next_runner) = idom_of_runner {
runner = next_runner;
} else {
break;
}
}
}
}
progress.on_complete();
Ok(DominanceFrontierResult { frontiers })
}
pub fn iterated_dominance_frontiers(
graph: &SqliteGraph,
dom_result: &DominatorResult,
definition_nodes: &AHashSet<i64>,
) -> Result<IteratedDominanceFrontierResult, SqliteGraphError> {
let df_result = dominance_frontiers(graph, dom_result)?;
if definition_nodes.is_empty() {
return Ok(IteratedDominanceFrontierResult {
phi_nodes: AHashSet::new(),
iterations: 0,
});
}
let mut phi_nodes: AHashSet<i64> = definition_nodes.clone();
let mut current: AHashSet<i64> = definition_nodes.clone();
let max_iterations = 100;
let mut iterations = 0;
loop {
let mut df_current: AHashSet<i64> = AHashSet::new();
for &node in ¤t {
if let Some(frontier) = df_result.frontier(node) {
df_current.extend(frontier.iter().copied());
}
}
let new_nodes: AHashSet<i64> = df_current.difference(&phi_nodes).copied().collect();
if new_nodes.is_empty() {
break;
}
phi_nodes.extend(new_nodes.iter().copied());
current = new_nodes;
iterations += 1;
if iterations >= max_iterations {
return Err(SqliteGraphError::query(format!(
"Iterated dominance frontier failed to converge after {} iterations",
max_iterations
)));
}
}
Ok(IteratedDominanceFrontierResult {
phi_nodes,
iterations,
})
}
#[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_branches() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..6 {
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, 4), (1, 2), (1, 3), (2, 5), (3, 5), (4, 5)];
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_if_else_if_cfg() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..6 {
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, 4), (3, 5), (4, 5)];
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_dominance_frontiers_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 dom_result = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
for &node in &entity_ids {
let frontier = df_result.frontier(node);
if let Some(set) = frontier {
assert!(!set.is_empty(), "Frontier set should not be empty");
}
}
}
#[test]
fn test_dominance_frontiers_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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
assert!(
df_result.in_frontier(entity_ids[1], entity_ids[3]),
"Node 3 should be in DF(1)"
);
assert!(
df_result.in_frontier(entity_ids[2], entity_ids[3]),
"Node 3 should be in DF(2)"
);
assert!(
df_result.frontier(entity_ids[0]).is_none()
|| df_result.frontier(entity_ids[0]).unwrap().is_empty(),
"Node 0 should have empty DF"
);
}
#[test]
fn test_dominance_frontiers_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 dom_result = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
assert!(
df_result.in_frontier(entity_ids[2], entity_ids[1]),
"Node 1 should be in DF(2) - back-edge creates frontier"
);
let df_2 = df_result.frontier(entity_ids[2]);
assert!(df_2.is_some(), "Node 2 should have a DF set");
assert!(
df_2.map(|s| s.contains(&entity_ids[1])).unwrap_or(false),
"DF(2) should contain node 1"
);
}
#[test]
fn test_dominance_frontiers_nested_branches() {
let graph = create_nested_branches();
let entity_ids: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let entry = entity_ids[0];
let dom_result = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
for &node in &entity_ids {
let frontier = df_result.frontier(node);
if let Some(set) = frontier {
assert!(!set.is_empty(), "Frontier set should not be empty");
}
}
}
#[test]
fn test_dominance_frontiers_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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
assert_eq!(
df_result.frontier(entry),
None,
"Single node should have empty DF"
);
}
#[test]
fn test_dominance_frontiers_empty_graph() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let dom_result = DominatorResult {
dom: AHashMap::new(),
idom: AHashMap::new(),
};
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
assert_eq!(df_result.frontiers.len(), 0);
}
#[test]
fn test_iterated_dominance_frontier_simple() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let mut definitions = AHashSet::new();
definitions.insert(entity_ids[1]);
definitions.insert(entity_ids[2]);
let idf_result = iterated_dominance_frontiers(&graph, &dom_result, &definitions)
.expect("Failed to compute IDF");
for &node in &idf_result.phi_nodes {
assert!(
entity_ids.contains(&node),
"phi_nodes should contain valid nodes"
);
}
}
#[test]
fn test_iterated_dominance_frontier_single_definition() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let mut definitions = AHashSet::new();
definitions.insert(entity_ids[1]);
let idf_result = iterated_dominance_frontiers(&graph, &dom_result, &definitions)
.expect("Failed to compute IDF");
assert!(
idf_result.phi_nodes.contains(&entity_ids[1]),
"IDF should contain definition node"
);
}
#[test]
fn test_iterated_dominance_frontier_empty_definitions() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let definitions = AHashSet::new();
let idf_result = iterated_dominance_frontiers(&graph, &dom_result, &definitions)
.expect("Failed to compute IDF");
assert_eq!(idf_result.phi_nodes.len(), 0);
assert_eq!(idf_result.iterations, 0);
}
#[test]
fn test_dominance_frontiers_entry_node() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
let df_entry = df_result.frontier(entry);
assert!(
df_entry.is_some() || df_entry.is_none(), "Entry node DF should be computed"
);
}
#[test]
fn test_dominance_frontiers_self_loop() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..2 {
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, 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");
}
let entry = entity_ids[0];
let dom_result = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
assert!(
!df_result.frontiers.is_empty(),
"Should compute DF successfully"
);
}
#[test]
fn test_dominance_frontiers_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 dom_result = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let progress = NoProgress;
let result_with =
dominance_frontiers_with_progress(&graph, &dom_result, &progress).expect("Failed");
let result_without = dominance_frontiers(&graph, &dom_result).expect("Failed");
assert_eq!(
result_with.frontiers.len(),
result_without.frontiers.len(),
"Should have same number of nodes with DF"
);
for (&node, frontier_set) in &result_without.frontiers {
assert!(
result_with.frontiers.contains_key(&node),
"Progress result missing node {}",
node
);
assert_eq!(
result_with.frontiers.get(&node),
Some(frontier_set),
"DF sets differ for node {}",
node
);
}
}
#[test]
fn test_dominance_frontier_in_frontier() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
for &node in &entity_ids {
for &other in &entity_ids {
let in_frontier = df_result.in_frontier(node, other);
if in_frontier {
if let Some(frontier) = df_result.frontier(node) {
assert!(
frontier.contains(&other),
"in_frontier true but not in frontier set"
);
}
}
}
}
}
#[test]
fn test_dominance_frontier_frontier() {
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 = super::super::dominators::dominators(&graph, entry)
.expect("Failed to compute dominators");
let df_result = dominance_frontiers(&graph, &dom_result)
.expect("Failed to compute dominance frontiers");
for &node in &entity_ids {
let frontier = df_result.frontier(node);
if let Some(set) = frontier {
assert!(!set.is_empty(), "Frontier should not be empty if Some");
}
}
}
}