use alloc::{
collections::{BTreeMap, BTreeSet, VecDeque},
vec::Vec,
};
use crate::GlobalItemIndex;
#[derive(Debug)]
pub struct CycleError(BTreeSet<GlobalItemIndex>);
impl CycleError {
pub fn into_node_ids(self) -> impl ExactSizeIterator<Item = GlobalItemIndex> {
self.0.into_iter()
}
}
#[derive(Default, Clone)]
pub struct CallGraph {
nodes: BTreeMap<GlobalItemIndex, Vec<GlobalItemIndex>>,
}
impl CallGraph {
pub fn out_edges(&self, gid: GlobalItemIndex) -> &[GlobalItemIndex] {
self.nodes.get(&gid).map(|out_edges| out_edges.as_slice()).unwrap_or(&[])
}
pub fn get_or_insert_node(&mut self, id: GlobalItemIndex) -> &mut Vec<GlobalItemIndex> {
self.nodes.entry(id).or_default()
}
pub fn add_edge(&mut self, caller: GlobalItemIndex, callee: GlobalItemIndex) {
assert_ne!(caller, callee, "a procedure cannot call itself");
self.get_or_insert_node(callee);
let callees = self.get_or_insert_node(caller);
if callees.contains(&callee) {
return;
}
callees.push(callee);
}
pub fn remove_edge(&mut self, caller: GlobalItemIndex, callee: GlobalItemIndex) {
if let Some(out_edges) = self.nodes.get_mut(&caller) {
out_edges.retain(|n| *n != callee);
}
}
pub fn num_predecessors(&self, id: GlobalItemIndex) -> usize {
self.nodes.iter().filter(|(_, out_edges)| out_edges.contains(&id)).count()
}
pub fn toposort(&self) -> Result<Vec<GlobalItemIndex>, CycleError> {
if self.nodes.is_empty() {
return Ok(vec![]);
}
let mut output = Vec::with_capacity(self.nodes.len());
let mut graph = self.clone();
let mut has_preds = BTreeSet::default();
for (_node, out_edges) in graph.nodes.iter() {
for succ in out_edges.iter() {
has_preds.insert(*succ);
}
}
let mut roots =
VecDeque::from_iter(graph.nodes.keys().copied().filter(|n| !has_preds.contains(n)));
let mut expect_cycle = false;
if roots.is_empty() {
expect_cycle = true;
roots.extend(graph.nodes.keys().next());
}
let mut successors = Vec::with_capacity(4);
while let Some(id) = roots.pop_front() {
output.push(id);
successors.clear();
successors.extend(graph.nodes[&id].iter().copied());
for mid in successors.drain(..) {
graph.remove_edge(id, mid);
if graph.num_predecessors(mid) == 0 {
roots.push_back(mid);
}
}
}
let has_cycle = graph
.nodes
.iter()
.any(|(n, out_edges)| !output.contains(n) || !out_edges.is_empty());
if has_cycle {
let mut in_cycle = BTreeSet::default();
for (n, edges) in graph.nodes.iter() {
if edges.is_empty() {
continue;
}
in_cycle.insert(*n);
in_cycle.extend(edges.as_slice());
}
Err(CycleError(in_cycle))
} else {
assert!(!expect_cycle, "we expected a cycle to be found, but one was not identified");
Ok(output)
}
}
pub fn subgraph(&self, root: GlobalItemIndex) -> Self {
let mut worklist = VecDeque::from_iter([root]);
let mut graph = Self::default();
let mut visited = BTreeSet::default();
while let Some(gid) = worklist.pop_front() {
if !visited.insert(gid) {
continue;
}
let new_successors = graph.get_or_insert_node(gid);
let prev_successors = self.out_edges(gid);
worklist.extend(prev_successors.iter().cloned());
new_successors.extend_from_slice(prev_successors);
}
graph
}
pub fn toposort_caller(
&self,
caller: GlobalItemIndex,
) -> Result<Vec<GlobalItemIndex>, CycleError> {
let mut output = Vec::with_capacity(self.nodes.len());
let mut graph = self.subgraph(caller);
graph.nodes.iter_mut().for_each(|(_pred, out_edges)| {
out_edges.retain(|n| *n != caller);
});
let mut roots = VecDeque::from_iter([caller]);
let mut successors = Vec::with_capacity(4);
while let Some(id) = roots.pop_front() {
output.push(id);
successors.clear();
successors.extend(graph.nodes[&id].iter().copied());
for mid in successors.drain(..) {
graph.remove_edge(id, mid);
if graph.num_predecessors(mid) == 0 {
roots.push_back(mid);
}
}
}
let has_cycle = graph
.nodes
.iter()
.any(|(n, out_edges)| output.contains(n) && !out_edges.is_empty());
if has_cycle {
let mut in_cycle = BTreeSet::default();
for (n, edges) in graph.nodes.iter() {
if edges.is_empty() {
continue;
}
in_cycle.insert(*n);
in_cycle.extend(edges.as_slice());
}
Err(CycleError(in_cycle))
} else {
Ok(output)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GlobalItemIndex, ModuleIndex, ast::ItemIndex};
const A: ModuleIndex = ModuleIndex::const_new(1);
const B: ModuleIndex = ModuleIndex::const_new(2);
const P1: ItemIndex = ItemIndex::const_new(1);
const P2: ItemIndex = ItemIndex::const_new(2);
const P3: ItemIndex = ItemIndex::const_new(3);
const A1: GlobalItemIndex = GlobalItemIndex { module: A, index: P1 };
const A2: GlobalItemIndex = GlobalItemIndex { module: A, index: P2 };
const A3: GlobalItemIndex = GlobalItemIndex { module: A, index: P3 };
const B1: GlobalItemIndex = GlobalItemIndex { module: B, index: P1 };
const B2: GlobalItemIndex = GlobalItemIndex { module: B, index: P2 };
const B3: GlobalItemIndex = GlobalItemIndex { module: B, index: P3 };
#[test]
fn callgraph_add_edge() {
let graph = callgraph_simple();
assert_eq!(graph.num_predecessors(A1), 0);
assert_eq!(graph.num_predecessors(B1), 0);
assert_eq!(graph.num_predecessors(A2), 1);
assert_eq!(graph.num_predecessors(B2), 2);
assert_eq!(graph.num_predecessors(B3), 1);
assert_eq!(graph.num_predecessors(A3), 2);
assert_eq!(graph.out_edges(A1), &[A2]);
assert_eq!(graph.out_edges(B1), &[B2]);
assert_eq!(graph.out_edges(A2), &[B2, A3]);
assert_eq!(graph.out_edges(B2), &[B3]);
assert_eq!(graph.out_edges(A3), &[]);
assert_eq!(graph.out_edges(B3), &[A3]);
}
#[test]
fn callgraph_add_edge_with_cycle() {
let graph = callgraph_cycle();
assert_eq!(graph.num_predecessors(A1), 0);
assert_eq!(graph.num_predecessors(B1), 0);
assert_eq!(graph.num_predecessors(A2), 2);
assert_eq!(graph.num_predecessors(B2), 2);
assert_eq!(graph.num_predecessors(B3), 1);
assert_eq!(graph.num_predecessors(A3), 1);
assert_eq!(graph.out_edges(A1), &[A2]);
assert_eq!(graph.out_edges(B1), &[B2]);
assert_eq!(graph.out_edges(A2), &[B2]);
assert_eq!(graph.out_edges(B2), &[B3]);
assert_eq!(graph.out_edges(A3), &[A2]);
assert_eq!(graph.out_edges(B3), &[A3]);
}
#[test]
fn callgraph_subgraph() {
let graph = callgraph_simple();
let subgraph = graph.subgraph(A2);
assert_eq!(subgraph.nodes.keys().copied().collect::<Vec<_>>(), vec![A2, A3, B2, B3]);
}
#[test]
fn callgraph_with_cycle_subgraph() {
let graph = callgraph_cycle();
let subgraph = graph.subgraph(A2);
assert_eq!(subgraph.nodes.keys().copied().collect::<Vec<_>>(), vec![A2, A3, B2, B3]);
}
#[test]
fn callgraph_toposort() {
let graph = callgraph_simple();
let sorted = graph.toposort().expect("expected valid topological ordering");
assert_eq!(sorted.as_slice(), &[A1, B1, A2, B2, B3, A3]);
}
#[test]
fn callgraph_toposort_caller() {
let graph = callgraph_simple();
let sorted = graph.toposort_caller(A2).expect("expected valid topological ordering");
assert_eq!(sorted.as_slice(), &[A2, B2, B3, A3]);
}
#[test]
fn callgraph_with_cycle_toposort() {
let graph = callgraph_cycle();
let err = graph.toposort().expect_err("expected topological sort to fail with cycle");
assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A2, A3, B2, B3]);
}
fn callgraph_simple() -> CallGraph {
let mut graph = CallGraph::default();
graph.add_edge(A1, A2);
graph.add_edge(B1, B2);
graph.add_edge(A2, B2);
graph.add_edge(A2, A3);
graph.add_edge(B2, B3);
graph.add_edge(B3, A3);
graph
}
fn callgraph_cycle() -> CallGraph {
let mut graph = CallGraph::default();
graph.add_edge(A1, A2);
graph.add_edge(B1, B2);
graph.add_edge(A2, B2);
graph.add_edge(B2, B3);
graph.add_edge(B3, A3);
graph.add_edge(A3, A2);
graph
}
}