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}