zrx_graph/graph/traversal.rs
1// Copyright (c) 2025 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// Third-party contributions licensed under DCO
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22// IN THE SOFTWARE.
23
24// ----------------------------------------------------------------------------
25
26//! Topological traversal.
27
28use std::collections::VecDeque;
29
30use super::topology::Topology;
31use super::{Error, Result};
32
33mod iter;
34
35pub use iter::IntoIter;
36
37// ----------------------------------------------------------------------------
38// Structs
39// ----------------------------------------------------------------------------
40
41/// Topological traversal.
42///
43/// This data type manages a topological traversal of a directed acyclic graph
44/// (DAG). It allows visiting nodes in a way that respects their dependencies,
45/// meaning that a node can only be visited after all of its dependencies have
46/// been visited. Visitable nodes can be obtained with [`Traversal::take`].
47///
48/// Note that the traversal itself doesn't know whether it's complete or not,
49/// as it only tracks visitable nodes depending on what has been reported back
50/// to [`Traversal::complete`]. This is because we also need to support partial
51/// traversals that can be resumed, which must be managed by the caller. In case
52/// a traversal starts at an intermediate node, only the nodes and dependencies
53/// reachable from this node are considered, which is necessary for implementing
54/// subgraph traversals that are self-contained, allowing for the creation of
55/// frontiers at any point in the graph.
56#[derive(Clone, Debug)]
57pub struct Traversal {
58 /// Graph topology.
59 topology: Topology,
60 /// Dependency counts.
61 dependencies: Vec<u8>,
62 /// Visitable nodes.
63 visitable: VecDeque<usize>,
64}
65
66// ----------------------------------------------------------------------------
67// Implementations
68// ----------------------------------------------------------------------------
69
70impl Traversal {
71 /// Creates a topological traversal.
72 ///
73 /// The given initial nodes are immediately marked as visitable, and thus
74 /// returned by [`Traversal::take`], so the caller must make sure they can
75 /// be processed. Note that the canonical way to create a [`Traversal`] is
76 /// to invoke the [`Graph::traverse`][] method.
77 ///
78 /// [`Graph::traverse`]: crate::graph::Graph::traverse
79 ///
80 /// # Examples
81 ///
82 /// ```
83 /// # use std::error::Error;
84 /// # fn main() -> Result<(), Box<dyn Error>> {
85 /// use zrx_graph::{Graph, Traversal};
86 ///
87 /// // Create graph builder and add nodes
88 /// let mut builder = Graph::builder();
89 /// let a = builder.add_node("a");
90 /// let b = builder.add_node("b");
91 /// let c = builder.add_node("c");
92 ///
93 /// // Create edges between nodes
94 /// builder.add_edge(a, b, 0)?;
95 /// builder.add_edge(b, c, 0)?;
96 ///
97 /// // Create graph from builder
98 /// let graph = builder.build();
99 ///
100 /// // Create topological traversal
101 /// let traversal = Traversal::new(graph.topology(), [a]);
102 /// # Ok(())
103 /// # }
104 /// ```
105 #[must_use]
106 pub fn new<I>(topology: &Topology, initial: I) -> Self
107 where
108 I: IntoIterator<Item = usize>,
109 {
110 let incoming = topology.incoming();
111 let distance = topology.distance();
112
113 // When doing a topological traversal, we only visit a node once all of
114 // its dependencies have been visited. This means that we need to track
115 // the number of dependencies for each node, which is the number of
116 // incoming edges for that node.
117 let mut visitable = VecDeque::from_iter(initial);
118 let mut dependencies = incoming.degrees().to_vec();
119 for node in incoming {
120 // We must adjust the dependency count for each node for all of its
121 // dependencies that are not reachable from the initial nodes
122 for &dependency in &incoming[node] {
123 let mut iter = visitable.iter();
124 if !iter.any(|&n| distance[n][dependency] != u8::MAX) {
125 dependencies[node] -= 1;
126 }
127 }
128 }
129
130 // Retain only the visitable nodes whose dependencies are satisfied,
131 // as we will discover the other initial nodes during traversal
132 visitable.retain(|&node| dependencies[node] == 0);
133 Self {
134 topology: topology.clone(),
135 dependencies,
136 visitable,
137 }
138 }
139
140 /// Returns the next visitable node.
141 ///
142 /// # Examples
143 ///
144 /// ```
145 /// # use std::error::Error;
146 /// # fn main() -> Result<(), Box<dyn Error>> {
147 /// use zrx_graph::Graph;
148 ///
149 /// // Create graph builder and add nodes
150 /// let mut builder = Graph::builder();
151 /// let a = builder.add_node("a");
152 /// let b = builder.add_node("b");
153 /// let c = builder.add_node("c");
154 ///
155 /// // Create edges between nodes
156 /// builder.add_edge(a, b, 0)?;
157 /// builder.add_edge(b, c, 0)?;
158 ///
159 /// // Create graph from builder
160 /// let graph = builder.build();
161 ///
162 /// // Create topological traversal
163 /// let mut traversal = graph.traverse([a]);
164 /// while let Some(node) = traversal.take() {
165 /// println!("{node:?}");
166 /// traversal.complete(node)?;
167 /// }
168 /// # Ok(())
169 /// # }
170 /// ```
171 #[inline]
172 #[must_use]
173 pub fn take(&mut self) -> Option<usize> {
174 self.visitable.pop_front()
175 }
176
177 /// Marks the given node as visited.
178 ///
179 /// This method marks a node as visited as part of a traversal, which might
180 /// allow visiting dependent nodes when all of their dependencies have been
181 /// satisfied. After marking a node as visited, the next nodes that can be
182 /// visited can be obtained using the [`Traversal::take`] method.
183 ///
184 /// # Errors
185 ///
186 /// In case the node has already been marked as visited, [`Error::Found`]
187 /// is returned. This is likely an error in the traversal business logic.
188 ///
189 /// # Panics
190 ///
191 /// Panics if a node does not exist, as this indicates that there's a bug
192 /// in the code that creates or uses the traversal. While the [`Builder`][]
193 /// is designed to be fallible to ensure the structure is valid, methods
194 /// that operate on [`Graph`][] panic on violated invariants.
195 ///
196 /// [`Builder`]: crate::graph::Builder
197 /// [`Graph`]: crate::graph::Graph
198 ///
199 /// # Examples
200 ///
201 /// ```
202 /// # use std::error::Error;
203 /// # fn main() -> Result<(), Box<dyn Error>> {
204 /// use zrx_graph::Graph;
205 ///
206 /// // Create graph builder and add nodes
207 /// let mut builder = Graph::builder();
208 /// let a = builder.add_node("a");
209 /// let b = builder.add_node("b");
210 /// let c = builder.add_node("c");
211 ///
212 /// // Create edges between nodes
213 /// builder.add_edge(a, b, 0)?;
214 /// builder.add_edge(b, c, 0)?;
215 ///
216 /// // Create graph from builder
217 /// let graph = builder.build();
218 ///
219 /// // Create topological traversal
220 /// let mut traversal = graph.traverse([a]);
221 /// while let Some(node) = traversal.take() {
222 /// println!("{node:?}");
223 /// traversal.complete(node)?;
224 /// }
225 /// # Ok(())
226 /// # }
227 /// ```
228 pub fn complete(&mut self, node: usize) -> Result {
229 if self.dependencies[node] == u8::MAX {
230 return Err(Error::Found(node));
231 }
232
233 // Mark node as visited - we can just use the maximum value of `u8` as
234 // a marker, as we don't expect more than 255 dependencies for any node
235 self.dependencies[node] = u8::MAX;
236
237 // Obtain adjacency list of outgoing edges, and decrement the number
238 // of unresolved dependencies for each dependent by one. When the number
239 // of dependencies for a dependent reaches zero, it can be visited, so
240 // we add it to the queue of visitable nodes.
241 let outgoing = self.topology.outgoing();
242 for &dependent in &outgoing[node] {
243 self.dependencies[dependent] -= 1;
244
245 // We satisfied all dependencies, so the dependent can be visited
246 if self.dependencies[dependent] == 0 {
247 self.visitable.push_back(dependent);
248 }
249 }
250
251 // No errors occurred.
252 Ok(())
253 }
254}
255
256#[allow(clippy::must_use_candidate)]
257impl Traversal {
258 /// Returns the graph topology.
259 #[inline]
260 pub fn topology(&self) -> &Topology {
261 &self.topology
262 }
263
264 /// Returns the number of visitable nodes.
265 #[inline]
266 pub fn len(&self) -> usize {
267 self.visitable.len()
268 }
269
270 /// Returns whether there are any visitable nodes.
271 #[inline]
272 pub fn is_empty(&self) -> bool {
273 self.visitable.is_empty()
274 }
275}