Skip to main content

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}