Skip to main content

jstd/
graph.rs

1//! Graph data structure with typed node and edge identifiers.
2//!
3//! `Graph` stores node and edge payloads in separate maps and exposes typed
4//! reference wrappers (`NodeRef`, `NodeMutRef`, `EdgeRef`, `EdgeMutRef`) to
5//! navigate and mutate the structure.
6
7use std::collections::HashSet;
8use std::fmt::Debug;
9use std::hash::{BuildHasher, Hash};
10
11pub mod analysis;
12pub mod edge;
13pub mod node;
14pub mod owning;
15pub mod parse;
16
17pub use edge::{Edge, EdgeMut};
18pub use node::{Node, NodeMut};
19pub use parse::TestGraph;
20
21/// Recommended fixed-seed hasher for `Graph::Hasher`.
22///
23/// The graph traits are generic over the hasher (see [`Graph::Hasher`]); this
24/// re-export lets consumers select a deterministic, fast hasher for their
25/// incident-edge sets without taking a direct `rustc-hash` dependency.
26pub use rustc_hash::FxBuildHasher;
27
28/// A typed graph container.
29///
30/// The graph is parameterized by:
31/// - `NodeId`: strongly typed node identifier
32/// - `EdgeId`: strongly typed edge identifier
33pub trait Graph {
34    // Node/edge IDs need only be cheap, hashable, totally-ordered handles — not
35    // `Registry` `Identifier`s. This lets a graph use *composite* IDs (e.g.
36    // qcode's `BlockId { func, local }`) that index no global arena directly,
37    // while `OwningGraph` still pins `Identifier` on its own type parameters.
38    type NodeId: Copy + Eq + Hash + Ord + Debug;
39    type EdgeId: Copy + Eq + Hash + Ord + Debug;
40
41    /// Hasher backing the graph's incident-edge sets and DFS bookkeeping.
42    ///
43    /// The trait is generic over the hasher rather than pinning a concrete one:
44    /// picking a fixed-seed hasher (e.g. `rustc_hash::FxBuildHasher`) makes
45    /// `predecessors()`/`successors()` iteration order deterministic across
46    /// runs, whereas the std default (`RandomState`) reseeds per process.
47    type Hasher: BuildHasher + Default;
48
49    type Node<'graph>: Node<'graph, Graph = Self>
50    where
51        Self: 'graph;
52
53    type Edge<'graph>: Edge<'graph, Graph = Self>
54    where
55        Self: 'graph;
56
57    /// Gets a node in the graph by its identifier, if it exists.
58    fn get_node(&self, id: Self::NodeId) -> Option<Self::Node<'_>>;
59
60    fn get_edge(&self, id: Self::EdgeId) -> Option<Self::Edge<'_>>;
61
62    /// Iterates over all nodes in insertion identifier order.
63    fn nodes(&self) -> impl Iterator<Item = Self::Node<'_>> + '_;
64
65    /// Iterates over all edges in insertion identifier order.
66    fn edges(&self) -> impl Iterator<Item = Self::Edge<'_>> + '_;
67
68    /// A dfs iterator over the graph starting from the root, if present.
69    fn dfs(&self, root: Self::NodeId) -> DfsIter<'_, Directed, Self>
70    where
71        Self: Sized,
72    {
73        DfsIter {
74            graph: self,
75            visited: HashSet::default(),
76            stack: vec![(None, root)],
77            _marker: std::marker::PhantomData,
78        }
79    }
80
81    /// A dfs iterator over the graph treating edges as undirected.
82    fn undirected_dfs(&self, root: Self::NodeId) -> DfsIter<'_, Undirected, Self>
83    where
84        Self: Sized,
85    {
86        DfsIter {
87            graph: self,
88            visited: HashSet::default(),
89            stack: vec![(None, root)],
90            _marker: std::marker::PhantomData,
91        }
92    }
93}
94
95/// The mutable half of [`Graph`]: node/edge handles that can rewrite the graph
96/// structure, plus structural edits.
97///
98/// Read-only algorithms (dominators, DFS, loop analysis) bound only [`Graph`], so
99/// a read-only view (e.g. a checked-out function's CFG behind an immutable
100/// reference) can implement `Graph` without providing a mutable surface it does
101/// not have. Graphs that own their storage (e.g. [`owning::OwningGraph`]) also
102/// implement `GraphMut`.
103pub trait GraphMut: Graph {
104    type NodeMut<'graph>: NodeMut<'graph, Graph = Self>
105    where
106        Self: 'graph;
107
108    type EdgeMut<'graph>: EdgeMut<'graph, Graph = Self>
109    where
110        Self: 'graph;
111
112    fn get_node_mut(&mut self, id: Self::NodeId) -> Option<Self::NodeMut<'_>>;
113
114    fn get_edge_mut(&mut self, id: Self::EdgeId) -> Option<Self::EdgeMut<'_>>;
115
116    /// Merges `remove` into `keep` at the graph-structural level.
117    ///
118    /// - Removes `direct_edge` (the edge from `keep` to `remove`) from both
119    ///   nodes' incident edge sets.
120    /// - Rehomes every outgoing edge of `remove` so that it originates from
121    ///   `keep` instead, updating both the edge record and the nodes' edge sets.
122    ///
123    /// The caller is responsible for any payload-level cleanup (e.g. removing
124    /// `remove` from a parent list, transferring instruction lists, etc.).
125    fn merge_nodes(&mut self, keep: Self::NodeId, remove: Self::NodeId, direct_edge: Self::EdgeId)
126    where
127        Self: Sized,
128    {
129        // 1. Remove the direct edge from both nodes' incident sets.
130        self.get_node_mut(keep).unwrap().remove_edge_id(direct_edge);
131        self.get_node_mut(remove)
132            .unwrap()
133            .remove_edge_id(direct_edge);
134
135        // 2. Collect outgoing edges of `remove` (releasing the immutable borrow).
136        let outgoing: Vec<Self::EdgeId> = self
137            .get_node(remove)
138            .unwrap()
139            .children()
140            .map(|item| item.edge_id())
141            .collect();
142
143        // 3. Rehome each outgoing edge from `remove` to `keep`.
144        for eid in outgoing {
145            self.get_edge_mut(eid).unwrap().set_from(keep);
146            self.get_node_mut(keep).unwrap().add_edge_id(eid);
147            self.get_node_mut(remove).unwrap().remove_edge_id(eid);
148        }
149    }
150}
151
152/// A minimal control-flow-graph view: the successor relation on node ids.
153///
154/// Dominator and post-dominator analysis need only two things from a graph: the
155/// successors of a node id, and a deterministic [`Hasher`](Cfg::Hasher) for their
156/// internal node-keyed maps. Bounding those algorithms on `Cfg` instead of the
157/// full [`Graph`] lets a caller expose just a successor relation — e.g. a
158/// checked-out function's CFG read through a `Copy` host handle, which cannot
159/// hand out the `&'graph Self::Graph` that [`Node`] requires.
160///
161/// Every [`Graph`] is a `Cfg` via the blanket impl below, so existing graph
162/// consumers keep working unchanged.
163pub trait Cfg {
164    type NodeId: Copy + Eq + Hash + Ord + Debug;
165
166    /// Hasher backing the analyses' node-keyed maps. Picking a fixed-seed hasher
167    /// (e.g. [`FxBuildHasher`]) keeps [`successors`](Cfg::successors) iteration —
168    /// and thus the derived dominator structures — deterministic across runs,
169    /// whereas the std default (`RandomState`) reseeds per process.
170    type Hasher: BuildHasher + Default;
171
172    /// The successors of `n` (the targets of its outgoing edges).
173    fn successors(&self, n: Self::NodeId) -> impl Iterator<Item = Self::NodeId> + '_;
174}
175
176impl<G: Graph> Cfg for G {
177    type NodeId = G::NodeId;
178    type Hasher = G::Hasher;
179
180    fn successors(&self, n: Self::NodeId) -> impl Iterator<Item = Self::NodeId> + '_ {
181        let succs: Vec<Self::NodeId> = self
182            .get_node(n)
183            .map(|nref| nref.children().map(|e| e.node_id()).collect())
184            .unwrap_or_default();
185        succs.into_iter()
186    }
187}
188
189pub struct Directed;
190
191pub struct Undirected;
192
193/// Depth-first traversal iterator over graph nodes.
194///
195/// The iterator yields tuples of:
196/// - the incoming tree edge used to discover the node (`None` for root),
197/// - the discovered node reference.
198///
199/// `Mode` controls how neighbors are explored:
200/// - [`Directed`]: follows only outgoing edges
201/// - [`Undirected`]: treats all incident edges as traversable
202pub struct DfsIter<'graph, Mode, G: Graph + Sized> {
203    graph: &'graph G,
204    visited: HashSet<G::NodeId, G::Hasher>,
205    stack: Vec<(Option<G::EdgeId>, G::NodeId)>,
206    _marker: std::marker::PhantomData<Mode>,
207}
208
209impl<'graph, G: Graph + Sized> Iterator for DfsIter<'graph, Directed, G> {
210    type Item = (Option<G::Edge<'graph>>, G::Node<'graph>);
211
212    fn next(&mut self) -> Option<Self::Item> {
213        while let Some((edge_id, node_id)) = self.stack.pop() {
214            if !self.visited.insert(node_id) {
215                continue;
216            }
217
218            let node_ref = self.graph.get_node(node_id).unwrap();
219
220            for edge in node_ref.children() {
221                let child_id = edge.node_id();
222                if !self.visited.contains(&child_id) {
223                    self.stack.push((Some(edge.edge_id()), child_id));
224                }
225            }
226
227            return Some((edge_id.and_then(|id| self.graph.get_edge(id)), node_ref));
228        }
229        None
230    }
231}
232
233impl<'graph, G: Graph + Sized> Iterator for DfsIter<'graph, Undirected, G> {
234    type Item = (Option<G::Edge<'graph>>, G::Node<'graph>);
235
236    fn next(&mut self) -> Option<Self::Item> {
237        while let Some((edge_id, node_id)) = self.stack.pop() {
238            if !self.visited.insert(node_id) {
239                continue;
240            }
241
242            let node_ref = self.graph.get_node(node_id).unwrap();
243
244            for edge in node_ref.edges() {
245                let child_id = edge.node_id();
246                if !self.visited.contains(&child_id) {
247                    self.stack.push((Some(edge.edge_id()), child_id));
248                }
249            }
250
251            return Some((edge_id.and_then(|id| self.graph.get_edge(id)), node_ref));
252        }
253        None
254    }
255}