blob: 7171469622f335d479597947a719861ff1f08285 [file] [log] [blame]
//! Support for cycle detection in DFS graph traversals.
use core::ops::{Deref, DerefMut};
#[derive(Copy, Clone, Debug)]
pub(crate) enum DecyclerError {
DepthLimitExceeded,
CycleDetected,
}
/// Cycle detector for DFS traversal of a graph.
///
/// The graph is expected to have unique node identifiers of type `T`
/// and traversal depth is limited to the constant `D`.
///
/// This is based on `hb_decycler_t` in HarfBuzz (<https://github.com/harfbuzz/harfbuzz/blob/a2ea5d28cb5387f4de2049802474b817be15ad5b/src/hb-decycler.hh>)
/// which is an extension of Floyd's tortoise and hare algorithm (<https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare>)
/// to DFS traversals.
///
/// Unlike the implementation in HB which supports traversals of arbitrary
/// depth, this is limited to a constant value. Instead of building a
/// forward-linked list of nodes (on the stack) to track the traversal chain,
/// we simply use a fixed size array, indexed by depth, which imposes the
/// limit.
///
/// It _might_ be possible to implement the HB algorithm in safe Rust but
/// satisfying the borrow checker would be a challenge and we require a depth
/// limit to prevent stack overflows anyway. Improvements welcome!
pub(crate) struct Decycler<T, const D: usize> {
node_ids: [T; D],
depth: usize,
}
impl<T, const D: usize> Decycler<T, D>
where
T: Copy + PartialEq + Default,
{
pub fn new() -> Self {
Self {
node_ids: [T::default(); D],
depth: 0,
}
}
/// Enters a new graph node with the given value that uniquely
/// identifies the current node.
///
/// Returns an error when a cycle is detected or the max depth of the
/// traversal is exceeded. Otherwise, increases the current depth and
/// returns a guard object that will decrease the depth when dropped.
///
/// The guard object derefs to the decycler, so it can be passed to
/// a recursive traversal function to check for cycles in descendent
/// nodes in a graph.
pub fn enter(&mut self, node_id: T) -> Result<DecyclerGuard<T, D>, DecyclerError> {
if self.depth < D {
if self.depth == 0 || self.node_ids[self.depth / 2] != node_id {
self.node_ids[self.depth] = node_id;
self.depth += 1;
Ok(DecyclerGuard { decycler: self })
} else {
Err(DecyclerError::CycleDetected)
}
} else {
Err(DecyclerError::DepthLimitExceeded)
}
}
}
impl<T, const D: usize> Default for Decycler<T, D>
where
T: Copy + PartialEq + Default,
{
fn default() -> Self {
Self::new()
}
}
pub(crate) struct DecyclerGuard<'a, T, const D: usize> {
decycler: &'a mut Decycler<T, D>,
}
impl<T, const D: usize> Deref for DecyclerGuard<'_, T, D> {
type Target = Decycler<T, D>;
fn deref(&self) -> &Self::Target {
self.decycler
}
}
impl<T, const D: usize> DerefMut for DecyclerGuard<'_, T, D> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.decycler
}
}
impl<T, const D: usize> Drop for DecyclerGuard<'_, T, D> {
fn drop(&mut self) {
self.decycler.depth -= 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn graph_with_cycles() {
let tree = Tree {
nodes: vec![
Node::new(vec![1, 2]),
Node::new(vec![2, 3]),
Node::new(vec![]),
Node::new(vec![0, 1]),
],
};
let result = tree.traverse(&mut TestDecycler::new());
assert!(matches!(result, Err(DecyclerError::CycleDetected)));
}
#[test]
fn exceeds_max_depth() {
let mut nodes = (0..MAX_DEPTH)
.map(|ix| Node::new(vec![ix + 1]))
.collect::<Vec<_>>();
nodes.push(Node::new(vec![]));
let tree = Tree { nodes };
let result = tree.traverse(&mut TestDecycler::new());
assert!(matches!(result, Err(DecyclerError::DepthLimitExceeded)));
}
#[test]
fn well_formed_tree() {
let mut nodes = (0..MAX_DEPTH - 1)
.map(|ix| Node::new(vec![ix + 1]))
.collect::<Vec<_>>();
nodes.push(Node::new(vec![]));
let tree = Tree { nodes };
let result = tree.traverse(&mut TestDecycler::new());
assert!(result.is_ok());
}
const MAX_DEPTH: usize = 64;
type TestDecycler = Decycler<usize, MAX_DEPTH>;
struct Node {
child_ids: Vec<usize>,
}
impl Node {
fn new(child_ids: Vec<usize>) -> Self {
Self { child_ids }
}
}
struct Tree {
nodes: Vec<Node>,
}
impl Tree {
fn traverse(&self, decycler: &mut TestDecycler) -> Result<(), DecyclerError> {
self.traverse_impl(decycler, 0)
}
fn traverse_impl(
&self,
decycler: &mut TestDecycler,
node_id: usize,
) -> Result<(), DecyclerError> {
let mut cycle_guard = decycler.enter(node_id)?;
let node = &self.nodes[node_id];
for child_id in &node.child_ids {
self.traverse_impl(&mut cycle_guard, *child_id)?;
}
Ok(())
}
}
}