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}