zrx_graph/graph/traversal.rs
1// Copyright (c) 2025-2026 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// All contributions are certified under the 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::error::{Error, Result};
31use super::topology::Topology;
32
33mod into_iter;
34
35pub use into_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)?;
95 /// builder.add_edge(b, c)?;
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: AsRef<[usize]>,
109 {
110 let mut visitable: VecDeque<_> =
111 initial.as_ref().iter().copied().collect();
112
113 // Obtain incoming edges and distance matrix
114 let incoming = topology.incoming();
115 let distance = topology.distance();
116
117 // When doing a topological traversal, we only visit a node once all of
118 // its dependencies have been visited. This means that we need to track
119 // the number of dependencies for each node, which is the number of
120 // incoming edges for that node.
121 let mut dependencies = incoming.degrees().to_vec();
122 for node in incoming {
123 // We must adjust the dependency count for each node for all of its
124 // dependencies that are not reachable from the initial nodes
125 for &dependency in &incoming[node] {
126 let mut iter = visitable.iter();
127 if !iter.any(|&n| distance[n][dependency] != u8::MAX) {
128 dependencies[node] -= 1;
129 }
130 }
131 }
132
133 // Retain only the visitable nodes whose dependencies are satisfied,
134 // as we will discover the other initial nodes during traversal
135 visitable.retain(|&node| dependencies[node] == 0);
136 Self {
137 topology: topology.clone(),
138 dependencies,
139 visitable,
140 }
141 }
142
143 /// Returns the next visitable node.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// # use std::error::Error;
149 /// # fn main() -> Result<(), Box<dyn Error>> {
150 /// use zrx_graph::Graph;
151 ///
152 /// // Create graph builder and add nodes
153 /// let mut builder = Graph::builder();
154 /// let a = builder.add_node("a");
155 /// let b = builder.add_node("b");
156 /// let c = builder.add_node("c");
157 ///
158 /// // Create edges between nodes
159 /// builder.add_edge(a, b)?;
160 /// builder.add_edge(b, c)?;
161 ///
162 /// // Create graph from builder
163 /// let graph = builder.build();
164 ///
165 /// // Create topological traversal
166 /// let mut traversal = graph.traverse([a]);
167 /// while let Some(node) = traversal.take() {
168 /// println!("{node:?}");
169 /// traversal.complete(node)?;
170 /// }
171 /// # Ok(())
172 /// # }
173 /// ```
174 #[inline]
175 #[must_use]
176 pub fn take(&mut self) -> Option<usize> {
177 self.visitable.pop_front()
178 }
179
180 /// Marks the given node as visited.
181 ///
182 /// This method marks a node as visited as part of a traversal, which might
183 /// allow visiting dependent nodes when all of their dependencies have been
184 /// satisfied. After marking a node as visited, the next nodes that can be
185 /// visited can be obtained using the [`Traversal::take`] method.
186 ///
187 /// # Errors
188 ///
189 /// In case the node has already been marked as visited, [`Error::Found`]
190 /// is returned. This is likely an error in the traversal business logic.
191 ///
192 /// # Panics
193 ///
194 /// Panics if a node does not exist, as this indicates that there's a bug
195 /// in the code that creates or uses the traversal. While the [`Builder`][]
196 /// is designed to be fallible to ensure the structure is valid, methods
197 /// that operate on [`Graph`][] panic on violated invariants.
198 ///
199 /// [`Builder`]: crate::graph::Builder
200 /// [`Graph`]: crate::graph::Graph
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// # use std::error::Error;
206 /// # fn main() -> Result<(), Box<dyn Error>> {
207 /// use zrx_graph::Graph;
208 ///
209 /// // Create graph builder and add nodes
210 /// let mut builder = Graph::builder();
211 /// let a = builder.add_node("a");
212 /// let b = builder.add_node("b");
213 /// let c = builder.add_node("c");
214 ///
215 /// // Create edges between nodes
216 /// builder.add_edge(a, b)?;
217 /// builder.add_edge(b, c)?;
218 ///
219 /// // Create graph from builder
220 /// let graph = builder.build();
221 ///
222 /// // Create topological traversal
223 /// let mut traversal = graph.traverse([a]);
224 /// while let Some(node) = traversal.take() {
225 /// println!("{node:?}");
226 /// traversal.complete(node)?;
227 /// }
228 /// # Ok(())
229 /// # }
230 /// ```
231 pub fn complete(&mut self, node: usize) -> Result {
232 if self.dependencies[node] == u8::MAX {
233 return Err(Error::Found(node));
234 }
235
236 // Mark node as visited - we can just use the maximum value of `u8` as
237 // a marker, as we don't expect more than 255 dependencies for any node
238 self.dependencies[node] = u8::MAX;
239
240 // Obtain adjacency list of outgoing edges, and decrement the number
241 // of unresolved dependencies for each dependent by one. When the number
242 // of dependencies for a dependent reaches zero, it can be visited, so
243 // we add it to the queue of visitable nodes.
244 let outgoing = self.topology.outgoing();
245 for &dependent in &outgoing[node] {
246 self.dependencies[dependent] -= 1;
247
248 // We satisfied all dependencies, so the dependent can be visited
249 if self.dependencies[dependent] == 0 {
250 self.visitable.push_back(dependent);
251 }
252 }
253
254 // No errors occurred.
255 Ok(())
256 }
257}
258
259#[allow(clippy::must_use_candidate)]
260impl Traversal {
261 /// Returns the graph topology.
262 #[inline]
263 pub fn topology(&self) -> &Topology {
264 &self.topology
265 }
266
267 /// Returns the number of visitable nodes.
268 #[inline]
269 pub fn len(&self) -> usize {
270 self.visitable.len()
271 }
272
273 /// Returns whether there are any visitable nodes.
274 #[inline]
275 pub fn is_empty(&self) -> bool {
276 self.visitable.is_empty()
277 }
278}