Skip to main content

igraph/
visitor.rs

1//! Breadth-first and depth-first traversals, with Rust closures as visitors
2//! (`igraph_visitor.h`).
3//!
4//! This module binds the three graph traversal functions of igraph's
5//! [Visitors](https://igraph.org/c/html/latest/igraph-Visitors.html) chapter:
6//!
7//! | Rust method | C function | What you get |
8//! |-------------|------------|--------------|
9//! | [`Graph::bfs`] | [`igraph_bfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfs) | [`BfsResult`]: order, rank, parents, pred, succ, dist |
10//! | [`Graph::bfs_with`] | `igraph_bfs` + `igraph_bfshandler_t` | same, calling a closure on every visited vertex ([`BfsVisit`]) |
11//! | [`Graph::bfs_simple`] | [`igraph_bfs_simple`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfs_simple) | [`BfsSimpleResult`]: order, distance layers, parents |
12//! | [`Graph::dfs`] | [`igraph_dfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_dfs) | [`DfsResult`]: discovery and finishing orders, parents, dist |
13//! | [`Graph::dfs_with`] | `igraph_dfs` + `igraph_dfshandler_t` (in and out) | same, calling a closure on every [`DfsEvent`] |
14//!
15//! Traversal parameters are grouped in [`BfsOptions`] and [`DfsOptions`]
16//! (edge direction to follow, whether to restart from unreachable vertices,
17//! and, for BFS, an optional restricted vertex set).
18//!
19//! # Visitors are closures
20//!
21//! The callback variants accept any `FnMut` closure returning
22//! [`ControlFlow<()>`](std::ops::ControlFlow): return
23//! [`ControlFlow::Continue(())`](std::ops::ControlFlow::Continue) to go on,
24//! or [`ControlFlow::Break(())`](std::ops::ControlFlow::Break) to stop the
25//! traversal early. Stopping is *not* an error (it maps to igraph's
26//! `IGRAPH_STOP`): the wrapper returns `Ok` with the partial results computed
27//! so far and sets the `stopped` flag of the result. If the closure panics,
28//! the traversal is aborted inside igraph (no unwinding crosses the FFI
29//! boundary) and the panic is then resumed in the calling Rust code.
30//!
31//! # Rusty results
32//!
33//! Where igraph stores negative sentinels (`-1` for "root", `-2` for "not
34//! visited"), the result structs use [`Option`]s instead, and the visiting
35//! orders only contain the vertices that were actually reached (igraph pads
36//! them with `-1`).
37//!
38//! # Differences from the raw C functions
39//!
40//! The bindings smooth over a few rough edges of `src/graph/visitors.c`,
41//! which are present in igraph 1.0.0 and 1.0.1 (the file is unchanged
42//! between the two releases):
43//!
44//! - `igraph_dfs` reports a wrong depth to its out-callback (one less than
45//!   the depth given at discovery) and, after a restart with `unreachable`,
46//!   its depth counter is not reset: the roots of later trees still get
47//!   depth 0, but the other vertices of the second tree get one less than
48//!   their true depth, those of the third tree two less, and so on (the raw
49//!   `dist` output can even become `-1`, the "not visited" sentinel).
50//!   [`DfsEvent`] and [`DfsResult::dist`] carry the true depths, tracked on
51//!   the Rust side.
52//! - `igraph_bfs` with `unreachable = true` reads out of bounds on the null
53//!   graph: the bindings answer that case without calling igraph.
54//! - `igraph_bfs_simple` does not validate its root, and `igraph_dfs`
55//!   reports an invalid root as `IGRAPH_EINVAL`: both methods check the
56//!   root first and report [`ErrorKind::InvalidVertexId`], like
57//!   [`Graph::bfs`].
58//! - After an early stop, `igraph_bfs` has already assigned a parent to the
59//!   vertices waiting in its queue; [`BfsResult::parents`] only describes
60//!   the vertices that were actually visited.
61//!
62//! # See also
63//!
64//! Many questions that can be answered with a hand-written traversal have a
65//! dedicated (and usually faster) function elsewhere in the crate:
66//!
67//! - distances and shortest paths: [`Graph::distances`],
68//!   [`Graph::get_shortest_path`], [`Graph::get_shortest_paths`],
69//!   [`Graph::eccentricity`] (all in [`crate::paths`]);
70//! - reachability and components: [`Graph::subcomponent`] (the vertices a
71//!   BFS from one root reaches), [`Graph::connected_components`],
72//!   [`Graph::neighborhood`] (vertices within a given number of hops);
73//! - DAGs and cycles: [`Graph::topological_sorting`], [`Graph::is_dag`],
74//!   [`Graph::find_cycle`] (in [`crate::cycles`]);
75//! - trees: [`Graph::unfold_tree`] (unrolls a graph into a BFS tree),
76//!   [`Graph::kary_tree`] and [`Graph::famous`] to build test inputs;
77//! - low-level neighbor access for your own traversals: [`crate::adjlist`].
78//!
79//! # Example
80//!
81//! ```
82//! use igraph::prelude::*;
83//! use igraph::visitor::{BfsOptions, DfsOptions};
84//! use std::ops::ControlFlow;
85//!
86//! // A small binary tree:      0
87//! //                         /   \
88//! //                        1     2
89//! //                       / \   /
90//! //                      3   4 5
91//! let tree = Graph::kary_tree(6, 2, TreeMode::Undirected)?;
92//!
93//! let bfs = tree.bfs(&[0], &BfsOptions::default())?;
94//! assert_eq!(bfs.order, [0, 1, 2, 3, 4, 5]);
95//! assert_eq!(bfs.dist, [Some(0), Some(1), Some(1), Some(2), Some(2), Some(2)]);
96//! assert_eq!(bfs.path_to(5), Some(vec![0, 2, 5]));
97//!
98//! let dfs = tree.dfs(0, &DfsOptions::default())?;
99//! assert_eq!(dfs.order, [0, 1, 3, 4, 2, 5]);     // pre-order
100//! assert_eq!(dfs.order_out, [3, 4, 1, 5, 2, 0]); // post-order
101//!
102//! // Stop as soon as a vertex at distance 2 is found.
103//! let mut first_deep = None;
104//! let partial = tree.bfs_with(&[0], &BfsOptions::default(), |visit| {
105//!     if visit.dist == 2 {
106//!         first_deep = Some(visit.vid);
107//!         return ControlFlow::Break(());
108//!     }
109//!     ControlFlow::Continue(())
110//! })?;
111//! assert_eq!(first_deep, Some(3));
112//! assert!(partial.stopped);
113//!
114//! // In an unweighted graph, BFS distances are shortest-path lengths.
115//! let d = tree.distances(0, .., None, NeighborMode::All)?;
116//! assert_eq!(d.to_rows()[0], [0.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
117//! # Ok::<(), igraph::Error>(())
118//! ```
119
120use crate::{
121    constants::NeighborMode,
122    error::{Error, ErrorKind, Result, catch_panic},
123    ffi::*,
124    graph::{Graph, VertexId},
125    igraph_call,
126    vector::VectorInt,
127};
128use std::{ffi::c_void, ops::ControlFlow, ptr};
129
130// ---------------------------------------------------------------------------
131// Options
132// ---------------------------------------------------------------------------
133
134/// Parameters of a breadth-first search ([`Graph::bfs`], [`Graph::bfs_with`]).
135///
136/// The defaults are: follow out-edges ([`NeighborMode::Out`]), do **not**
137/// visit vertices unreachable from the roots, no restriction.
138///
139/// ```
140/// use igraph::prelude::*;
141/// use igraph::visitor::BfsOptions;
142///
143/// let restricted = [0, 1, 2];
144/// let opts = BfsOptions::default()
145///     .with_mode(NeighborMode::All)
146///     .with_unreachable(true)
147///     .with_restricted(&restricted);
148/// assert_eq!(opts.mode, NeighborMode::All);
149/// assert!(opts.unreachable);
150/// assert_eq!(opts.restricted, Some(&restricted[..]));
151/// ```
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct BfsOptions<'a> {
154    /// Which edges to follow in directed graphs: [`NeighborMode::Out`] follows
155    /// the edge directions, [`NeighborMode::In`] goes against them and
156    /// [`NeighborMode::All`] ignores them. Ignored for undirected graphs.
157    pub mode: NeighborMode,
158    /// If `true`, once the roots are exhausted, further searches are started
159    /// from the not yet visited vertices, in increasing id order, until every
160    /// (allowed) vertex has been visited.
161    pub unreachable: bool,
162    /// If set, the search only walks on these vertices: every other vertex
163    /// is treated as already visited (even when it is given as a root, in
164    /// which case it is silently skipped).
165    pub restricted: Option<&'a [VertexId]>,
166}
167
168impl Default for BfsOptions<'_> {
169    fn default() -> Self {
170        Self {
171            mode: NeighborMode::Out,
172            unreachable: false,
173            restricted: None,
174        }
175    }
176}
177
178impl<'a> BfsOptions<'a> {
179    /// Sets [`mode`](Self::mode).
180    pub fn with_mode(mut self, mode: NeighborMode) -> Self {
181        self.mode = mode;
182        self
183    }
184
185    /// Sets [`unreachable`](Self::unreachable).
186    pub fn with_unreachable(mut self, unreachable: bool) -> Self {
187        self.unreachable = unreachable;
188        self
189    }
190
191    /// Sets [`restricted`](Self::restricted) to `Some(vertices)`.
192    pub fn with_restricted(mut self, vertices: &'a [VertexId]) -> Self {
193        self.restricted = Some(vertices);
194        self
195    }
196}
197
198/// Parameters of a depth-first search ([`Graph::dfs`], [`Graph::dfs_with`]).
199///
200/// The defaults are: follow out-edges ([`NeighborMode::Out`]) and do **not**
201/// visit vertices unreachable from the root.
202///
203/// ```
204/// use igraph::prelude::*;
205/// use igraph::visitor::DfsOptions;
206///
207/// // 0 -> 1, 2 -> 1: walking against the edges from 1 finds 0 and 2.
208/// let g = Graph::from_edges(&[(0, 1), (2, 1)], 3, true)?;
209/// let opts = DfsOptions::default().with_mode(NeighborMode::In);
210/// assert_eq!(g.dfs(1, &opts)?.order, [1, 0, 2]);
211/// assert_eq!(g.dfs(1, &DfsOptions::default())?.order, [1]);
212/// # Ok::<(), igraph::Error>(())
213/// ```
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct DfsOptions {
216    /// Which edges to follow in directed graphs (see [`BfsOptions::mode`]).
217    /// Ignored for undirected graphs.
218    pub mode: NeighborMode,
219    /// If `true`, once the tree of the root is complete, further searches are
220    /// started from the not yet visited vertices, in increasing id order.
221    pub unreachable: bool,
222}
223
224impl Default for DfsOptions {
225    fn default() -> Self {
226        Self {
227            mode: NeighborMode::Out,
228            unreachable: false,
229        }
230    }
231}
232
233impl DfsOptions {
234    /// Sets [`mode`](Self::mode).
235    pub fn with_mode(mut self, mode: NeighborMode) -> Self {
236        self.mode = mode;
237        self
238    }
239
240    /// Sets [`unreachable`](Self::unreachable).
241    pub fn with_unreachable(mut self, unreachable: bool) -> Self {
242        self.unreachable = unreachable;
243        self
244    }
245}
246
247// ---------------------------------------------------------------------------
248// Results and events
249// ---------------------------------------------------------------------------
250
251/// What a breadth-first search visitor sees each time a vertex is visited
252/// (the arguments of igraph's `igraph_bfshandler_t`).
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
254pub struct BfsVisit {
255    /// The vertex being visited.
256    pub vid: VertexId,
257    /// The vertex visited just before, or `None` if `vid` is the root of a
258    /// search tree.
259    pub pred: Option<VertexId>,
260    /// The vertex that will be visited next, or `None` if `vid` is the last
261    /// vertex of its search tree.
262    pub succ: Option<VertexId>,
263    /// The rank of `vid`, i.e. its position in the visiting order (from 0).
264    pub rank: usize,
265    /// The distance (number of hops) of `vid` from the root of its search
266    /// tree.
267    pub dist: usize,
268}
269
270/// Results of a breadth-first search ([`Graph::bfs`], [`Graph::bfs_with`]).
271///
272/// The per-vertex vectors (`rank`, `parents`, `pred`, `succ`, `dist`) have
273/// one entry per vertex of the graph, `None` for vertices that were not
274/// visited (e.g. unreachable, outside the restricted set, or not reached
275/// because the visitor stopped the search).
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct BfsResult {
278    /// The visited vertices, in visiting order.
279    pub order: Vec<VertexId>,
280    /// The rank (position in [`order`](Self::order)) of each vertex.
281    pub rank: Vec<Option<usize>>,
282    /// The parent of each vertex in the BFS forest: `None` for the roots of
283    /// the search trees and for unvisited vertices (igraph itself already
284    /// assigns a parent to vertices that were queued but not yet visited when
285    /// the visitor stopped the search; the bindings report them as `None`
286    /// too, so that `parents` always describes the visited forest).
287    pub parents: Vec<Option<VertexId>>,
288    /// The vertex visited just before each vertex: `None` for the roots of
289    /// the search trees and for unvisited vertices.
290    pub pred: Vec<Option<VertexId>>,
291    /// The vertex visited just after each vertex: `None` for the last vertex
292    /// of each search tree and for unvisited vertices. When the visitor stops
293    /// the search, the successor of the vertex it stopped at is `None` too.
294    pub succ: Vec<Option<VertexId>>,
295    /// The distance of each vertex from the root of its search tree.
296    pub dist: Vec<Option<usize>>,
297    /// `true` if the visitor closure returned
298    /// [`ControlFlow::Break`], so that the search ended early.
299    pub stopped: bool,
300}
301
302impl BfsResult {
303    /// Whether vertex `v` was visited by the search.
304    pub fn is_visited(&self, v: VertexId) -> bool {
305        usize::try_from(v)
306            .ok()
307            .and_then(|i| self.rank.get(i))
308            .is_some_and(Option::is_some)
309    }
310
311    /// The roots of the search trees, in the order they were used.
312    ///
313    /// In an undirected graph searched with [`BfsOptions::unreachable`] set
314    /// (and no restriction), there is exactly one root per connected
315    /// component, see [`Graph::connected_components`].
316    pub fn roots(&self) -> Vec<VertexId> {
317        self.order
318            .iter()
319            .copied()
320            .filter(|&v| self.parents[v as usize].is_none())
321            .collect()
322    }
323
324    /// The path of the BFS forest from the root of `v`'s search tree down to
325    /// `v` (a shortest path in unweighted graphs), or `None` if `v` was not
326    /// visited. [`Graph::get_shortest_path`] computes a single such path
327    /// directly (also with weights).
328    pub fn path_to(&self, v: VertexId) -> Option<Vec<VertexId>> {
329        tree_path(&self.parents, &self.rank, v)
330    }
331}
332
333/// Results of [`Graph::bfs_simple`].
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct BfsSimpleResult {
336    /// The visited vertices, in visiting order (only the ones reachable from
337    /// the root).
338    pub order: Vec<VertexId>,
339    /// Layer boundaries: the vertices at distance `i` from the root are
340    /// `order[layers[i]..layers[i + 1]]`. It has one more element than the
341    /// number of layers; the last one is `order.len()`.
342    pub layers: Vec<usize>,
343    /// The parent of each vertex in the BFS tree: `None` for the root and for
344    /// vertices that were not reached.
345    pub parents: Vec<Option<VertexId>>,
346}
347
348impl BfsSimpleResult {
349    /// Number of distance layers (the eccentricity of the root plus one).
350    pub fn num_layers(&self) -> usize {
351        self.layers.len().saturating_sub(1)
352    }
353
354    /// The vertices at distance `i` from the root (empty if `i` is too large).
355    pub fn layer(&self, i: usize) -> &[VertexId] {
356        match (self.layers.get(i), self.layers.get(i + 1)) {
357            (Some(&a), Some(&b)) => &self.order[a..b],
358            _ => &[],
359        }
360    }
361
362    /// Iterates over the distance layers, from the root outwards.
363    pub fn iter_layers(&self) -> impl Iterator<Item = &[VertexId]> + '_ {
364        self.layers.windows(2).map(|w| &self.order[w[0]..w[1]])
365    }
366}
367
368/// An event reported to a depth-first search visitor ([`Graph::dfs_with`]).
369///
370/// igraph has two DFS callbacks: one when a vertex is *discovered*
371/// (`in_callback`) and one when its subtree is *finished* (`out_callback`).
372/// A single Rust closure receives both, as the two variants of this enum.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
374pub enum DfsEvent {
375    /// A vertex has just been discovered (pre-order).
376    Discover {
377        /// The discovered vertex.
378        vid: VertexId,
379        /// Its distance (depth) from the root of its search tree.
380        dist: usize,
381    },
382    /// The whole subtree of a vertex has been explored (post-order).
383    Finish {
384        /// The finished vertex.
385        vid: VertexId,
386        /// Its distance (depth) from the root of its search tree: the same
387        /// value as in the matching [`Discover`](DfsEvent::Discover) event
388        /// (the raw C callback receives one less).
389        dist: usize,
390    },
391}
392
393impl DfsEvent {
394    /// The vertex the event is about.
395    pub fn vid(&self) -> VertexId {
396        match *self {
397            Self::Discover { vid, .. } | Self::Finish { vid, .. } => vid,
398        }
399    }
400
401    /// The depth of the vertex in its search tree.
402    pub fn dist(&self) -> usize {
403        match *self {
404            Self::Discover { dist, .. } | Self::Finish { dist, .. } => dist,
405        }
406    }
407}
408
409/// Results of a depth-first search ([`Graph::dfs`], [`Graph::dfs_with`]).
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct DfsResult {
412    /// The vertices in the order they were discovered (pre-order).
413    pub order: Vec<VertexId>,
414    /// The vertices in the order their subtrees were completed (post-order).
415    /// If the search was stopped early, it may be shorter than
416    /// [`order`](Self::order).
417    pub order_out: Vec<VertexId>,
418    /// The parent of each vertex in the DFS forest: `None` for the roots of
419    /// the search trees and for unvisited vertices.
420    pub parents: Vec<Option<VertexId>>,
421    /// The depth of each vertex in its DFS tree (the number of tree edges
422    /// between it and the root of its tree), `None` if not visited. Unlike
423    /// the raw output of `igraph_dfs`, it stays exact after a restart with
424    /// [`DfsOptions::unreachable`] (see the [module docs](self)).
425    pub dist: Vec<Option<usize>>,
426    /// `true` if the visitor closure returned [`ControlFlow::Break`].
427    pub stopped: bool,
428}
429
430impl DfsResult {
431    /// Whether vertex `v` was discovered by the search.
432    pub fn is_visited(&self, v: VertexId) -> bool {
433        usize::try_from(v)
434            .ok()
435            .and_then(|i| self.dist.get(i))
436            .is_some_and(Option::is_some)
437    }
438
439    /// The path of the DFS forest from the root of `v`'s tree down to `v`,
440    /// or `None` if `v` was not visited.
441    pub fn path_to(&self, v: VertexId) -> Option<Vec<VertexId>> {
442        tree_path(&self.parents, &self.dist, v)
443    }
444}
445
446// ---------------------------------------------------------------------------
447// Helpers
448// ---------------------------------------------------------------------------
449
450/// Follows `parents` from `v` up to a root; `visited[v]` tells whether `v`
451/// belongs to the forest at all.
452fn tree_path<T>(
453    parents: &[Option<VertexId>],
454    visited: &[Option<T>],
455    v: VertexId,
456) -> Option<Vec<VertexId>> {
457    let i = usize::try_from(v).ok()?;
458    visited.get(i)?.as_ref()?;
459    let mut path = vec![v];
460    let mut cur = v;
461    while let Some(p) = parents[cur as usize] {
462        path.push(p);
463        cur = p;
464        if path.len() > parents.len() {
465            return None; // defensive: never loop on a malformed forest
466        }
467    }
468    path.reverse();
469    Some(path)
470}
471
472/// Converts an igraph id vector with negative sentinels into `Option`s.
473fn opt_ids(v: VectorInt) -> Vec<Option<VertexId>> {
474    v.iter().map(|&x| (x >= 0).then_some(x)).collect()
475}
476
477/// Converts an igraph count vector with negative sentinels into `Option`s.
478fn opt_counts(v: VectorInt) -> Vec<Option<usize>> {
479    v.iter().map(|&x| usize::try_from(x).ok()).collect()
480}
481
482/// Keeps the visited prefix of an order vector padded with `-1`.
483fn visited_prefix(v: VectorInt) -> Vec<VertexId> {
484    v.iter().copied().take_while(|&x| x >= 0).collect()
485}
486
487fn check_vertex(graph: &Graph, v: VertexId, what: &str) -> Result<()> {
488    if v < 0 || v as usize >= graph.vcount() {
489        return Err(Error::new(
490            ErrorKind::InvalidVertexId,
491            format!(
492                "invalid {what} vertex id {v} (the graph has {} vertices)",
493                graph.vcount()
494            ),
495        ));
496    }
497    Ok(())
498}
499
500fn flow_to_code(flow: ControlFlow<()>, stopped: &mut bool) -> igraph_error_t {
501    match flow {
502        ControlFlow::Continue(()) => igraph_error_type_t_IGRAPH_SUCCESS,
503        ControlFlow::Break(()) => {
504            *stopped = true;
505            igraph_error_type_t_IGRAPH_STOP
506        }
507    }
508}
509
510/// Runs the Rust side of a BFS/DFS callback in a new level of igraph's
511/// "finally" stack (`IGRAPH_FINALLY_ENTER` / `IGRAPH_FINALLY_EXIT`), catching
512/// panics.
513///
514/// The running search keeps its temporaries (queue, stack, bitsets) on that
515/// stack, and igraph's error handler frees the objects of the current level
516/// when a call fails. Without a new level, an igraph call made by the user
517/// closure that fails (returning an `Err` the closure may well ignore) would
518/// free the temporaries of the search that is still running, a
519/// use-after-free in C.
520fn in_finally_level(f: impl FnOnce() -> igraph_error_t) -> igraph_error_t {
521    // SAFETY: plain bookkeeping on igraph's thread-local finally stack; the
522    // matching EXIT runs below, as `catch_panic` never unwinds.
523    unsafe { IGRAPH_FINALLY_ENTER() };
524    let code = catch_panic(f);
525    // SAFETY: closes the level opened above. Every igraph call made by the
526    // closure has returned, and a failed one has already freed its objects.
527    unsafe { IGRAPH_FINALLY_EXIT() };
528    code
529}
530
531struct BfsState<F> {
532    f: F,
533    stopped: bool,
534}
535
536unsafe extern "C" fn bfs_trampoline<F>(
537    _graph: *const igraph_t,
538    vid: igraph_int_t,
539    pred: igraph_int_t,
540    succ: igraph_int_t,
541    rank: igraph_int_t,
542    dist: igraph_int_t,
543    extra: *mut c_void,
544) -> igraph_error_t
545where
546    F: FnMut(BfsVisit) -> ControlFlow<()>,
547{
548    in_finally_level(|| {
549        // SAFETY: `extra` is the `&mut BfsState<F>` passed by `bfs_impl`,
550        // alive and exclusively borrowed for the whole C call.
551        let state = unsafe { &mut *(extra as *mut BfsState<F>) };
552        let visit = BfsVisit {
553            vid,
554            pred: (pred >= 0).then_some(pred),
555            succ: (succ >= 0).then_some(succ),
556            rank: rank as usize,
557            dist: dist as usize,
558        };
559        let flow = (state.f)(visit);
560        flow_to_code(flow, &mut state.stopped)
561    })
562}
563
564/// State shared by the two DFS trampolines. igraph's own `dist` argument to
565/// the out-callback is off by one, and both callbacks' `dist` drift after a
566/// restart with
567/// `unreachable` (igraph 1.0.0 and 1.0.1: `act_dist` is never reset for a new
568/// root), so the depth is tracked here: `depth` is the size of the DFS
569/// stack, incremented on discovery and decremented on completion.
570struct DfsState<F> {
571    f: F,
572    depth: usize,
573    stopped: bool,
574}
575
576unsafe extern "C" fn dfs_in_trampoline<F>(
577    _graph: *const igraph_t,
578    vid: igraph_int_t,
579    _dist: igraph_int_t,
580    extra: *mut c_void,
581) -> igraph_error_t
582where
583    F: FnMut(DfsEvent) -> ControlFlow<()>,
584{
585    in_finally_level(|| {
586        // SAFETY: see `bfs_trampoline`.
587        let state = unsafe { &mut *(extra as *mut DfsState<F>) };
588        let dist = state.depth;
589        state.depth += 1;
590        let flow = (state.f)(DfsEvent::Discover { vid, dist });
591        flow_to_code(flow, &mut state.stopped)
592    })
593}
594
595unsafe extern "C" fn dfs_out_trampoline<F>(
596    _graph: *const igraph_t,
597    vid: igraph_int_t,
598    _dist: igraph_int_t,
599    extra: *mut c_void,
600) -> igraph_error_t
601where
602    F: FnMut(DfsEvent) -> ControlFlow<()>,
603{
604    in_finally_level(|| {
605        // SAFETY: see `bfs_trampoline`.
606        let state = unsafe { &mut *(extra as *mut DfsState<F>) };
607        state.depth = state.depth.saturating_sub(1);
608        let dist = state.depth;
609        let flow = (state.f)(DfsEvent::Finish { vid, dist });
610        flow_to_code(flow, &mut state.stopped)
611    })
612}
613
614// ---------------------------------------------------------------------------
615// Graph methods
616// ---------------------------------------------------------------------------
617
618impl igraph_t {
619    fn bfs_impl(
620        &self,
621        roots: &[VertexId],
622        options: &BfsOptions<'_>,
623        callback: igraph_bfshandler_t,
624        extra: *mut c_void,
625    ) -> Result<BfsResult> {
626        if self.vcount() == 0 {
627            // igraph 1.0.0 and 1.0.1 read out of bounds when `unreachable` is
628            // set on the null graph (`IGRAPH_BIT_TEST(added, 0)` on an empty
629            // bitset): handle it here (any given id is invalid).
630            if let Some(&v) = roots.iter().chain(options.restricted.unwrap_or(&[])).next() {
631                check_vertex(self, v, "root or restricted")?;
632            }
633            return Ok(BfsResult {
634                order: vec![],
635                rank: vec![],
636                parents: vec![],
637                pred: vec![],
638                succ: vec![],
639                dist: vec![],
640                stopped: false,
641            });
642        }
643        let roots_v = VectorInt::view(roots);
644        let restricted_v = options.restricted.map(VectorInt::view);
645        let restricted_p = restricted_v.as_ref().map_or(ptr::null(), |v| v.as_ptr());
646        let mut order = VectorInt::new();
647        let mut rank = VectorInt::new();
648        let mut parents = VectorInt::new();
649        let mut pred = VectorInt::new();
650        let mut succ = VectorInt::new();
651        let mut dist = VectorInt::new();
652        igraph_call!(igraph_bfs(
653            self,
654            0,
655            roots_v.as_ptr(),
656            options.mode.into(),
657            options.unreachable,
658            restricted_p,
659            &mut order,
660            &mut rank,
661            &mut parents,
662            &mut pred,
663            &mut succ,
664            &mut dist,
665            callback,
666            extra,
667        ))?;
668        let rank = opt_counts(rank);
669        // igraph assigns `parents` when a vertex is *enqueued*, not when it is
670        // visited: after an early stop, vertices still waiting in the queue
671        // would have a parent but no rank/dist. Keep the documented invariant
672        // "parent is `Some` only for visited, non-root vertices".
673        let parents = opt_ids(parents)
674            .into_iter()
675            .zip(&rank)
676            .map(|(p, r)| r.and(p))
677            .collect();
678        Ok(BfsResult {
679            order: visited_prefix(order),
680            rank,
681            parents,
682            pred: opt_ids(pred),
683            succ: opt_ids(succ),
684            dist: opt_counts(dist),
685            stopped: false,
686        })
687    }
688
689    /// Breadth-first search from one or more root vertices.
690    ///
691    /// The search starts from `roots[0]`; when its tree is exhausted, it
692    /// continues from the next root in `roots` that was not visited yet, and
693    /// so on (roots already reached from a previous one are skipped). If
694    /// [`options.unreachable`](BfsOptions::unreachable) is `true`, the
695    /// remaining vertices are then used as roots too, in increasing id order,
696    /// so that the whole graph is traversed. Neighbors are enqueued in the
697    /// order of the graph's adjacency lists (increasing neighbor id). An
698    /// empty `roots` slice is allowed: nothing is visited unless
699    /// `unreachable` is set.
700    ///
701    /// The result gathers every output of the C function: the visiting
702    /// order, and, for each vertex, its rank, BFS-tree parent, predecessor
703    /// and successor in the visiting order, and distance from its root. See
704    /// [`BfsResult`]. Use [`Graph::bfs_with`] to run a visitor closure during
705    /// the search, and [`Graph::bfs_simple`] for a lighter single-root
706    /// variant that also reports distance layers.
707    ///
708    /// Time complexity: O(|V| + |E|).
709    ///
710    /// Binds [`igraph_bfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfs).
711    ///
712    /// See also [`Graph::subcomponent`] (just the set of reachable
713    /// vertices), [`Graph::distances`] and [`Graph::get_shortest_paths`]
714    /// (weighted distances and paths), [`Graph::connected_components`], and
715    /// [`Graph::unfold_tree`] (turns the graph into its BFS tree).
716    ///
717    /// # Errors
718    ///
719    /// [`ErrorKind::InvalidVertexId`] if a root or a restricted vertex does
720    /// not exist.
721    ///
722    /// # Examples
723    ///
724    /// Two disjoint 4-cycles `0-1-2-3` and `4-5-6-7`:
725    ///
726    /// ```
727    /// use igraph::prelude::*;
728    /// use igraph::visitor::BfsOptions;
729    ///
730    /// let square = Graph::ring(4, false, false, true)?;
731    /// let g = square.disjoint_union(&square)?;
732    /// let r = g.bfs(&[0], &BfsOptions::default())?;
733    /// assert_eq!(r.order, [0, 1, 3, 2]);
734    /// assert_eq!(r.dist[2], Some(2));
735    /// assert!(!r.is_visited(5));
736    ///
737    /// let all = g.bfs(&[0], &BfsOptions::default().with_unreachable(true))?;
738    /// assert_eq!(all.order, [0, 1, 3, 2, 4, 5, 7, 6]);
739    /// // One search tree per connected component.
740    /// assert_eq!(all.roots(), [0, 4]);
741    /// assert_eq!(g.connected_components(Connectedness::Weak)?.count, 2);
742    /// # Ok::<(), igraph::Error>(())
743    /// ```
744    pub fn bfs(&self, roots: &[VertexId], options: &BfsOptions<'_>) -> Result<BfsResult> {
745        self.bfs_impl(roots, options, None, ptr::null_mut())
746    }
747
748    /// Breadth-first search calling a visitor closure on every visited vertex.
749    ///
750    /// Same traversal as [`Graph::bfs`]; in addition, `visitor` is called
751    /// each time a vertex is *visited* (dequeued), after all its unvisited
752    /// neighbors have been enqueued, with a [`BfsVisit`] describing the
753    /// vertex, its predecessor and successor in the visiting order, its rank
754    /// and its distance from the root.
755    ///
756    /// Return [`ControlFlow::Continue`] to go on or [`ControlFlow::Break`] to
757    /// stop: the search then ends normally, the returned [`BfsResult`] has
758    /// [`stopped`](BfsResult::stopped) set, and contains the values computed
759    /// so far (the vertices not yet visited are `None`). Note that, when
760    /// stopping, the vertex the visitor stopped at has already been visited
761    /// (it is in `order`), but its `succ` entry is `None`. If `visitor`
762    /// panics, the search is aborted and the panic resumes in the caller.
763    ///
764    /// Binds [`igraph_bfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfs)
765    /// with an [`igraph_bfshandler_t`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfshandler_t)
766    /// callback.
767    ///
768    /// See also [`Graph::dfs_with`] for the depth-first counterpart.
769    ///
770    /// # Errors
771    ///
772    /// Same as [`Graph::bfs`].
773    ///
774    /// # Examples
775    ///
776    /// Find the first vertex of a path that is at least three hops away,
777    /// without visiting the rest of the graph:
778    ///
779    /// ```
780    /// use igraph::prelude::*;
781    /// use igraph::visitor::BfsOptions;
782    /// use std::ops::ControlFlow;
783    ///
784    /// let path = Graph::ring(6, false, false, false)?; // 0 - 1 - 2 - 3 - 4 - 5
785    /// let mut seen = vec![];
786    /// let r = path.bfs_with(&[0], &BfsOptions::default(), |v| {
787    ///     seen.push(v.vid);
788    ///     if v.dist >= 3 { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
789    /// })?;
790    /// assert_eq!(seen, [0, 1, 2, 3]);
791    /// assert!(r.stopped);
792    /// assert_eq!(r.order, [0, 1, 2, 3]);
793    /// assert_eq!(r.rank[5], None);
794    /// # Ok::<(), igraph::Error>(())
795    /// ```
796    pub fn bfs_with<F>(
797        &self,
798        roots: &[VertexId],
799        options: &BfsOptions<'_>,
800        visitor: F,
801    ) -> Result<BfsResult>
802    where
803        F: FnMut(BfsVisit) -> ControlFlow<()>,
804    {
805        let mut state = BfsState {
806            f: visitor,
807            stopped: false,
808        };
809        let extra = &mut state as *mut BfsState<F> as *mut c_void;
810        let mut result = self.bfs_impl(roots, options, Some(bfs_trampoline::<F>), extra)?;
811        result.stopped = state.stopped;
812        Ok(result)
813    }
814
815    /// Simple single-source breadth-first search, reporting distance layers.
816    ///
817    /// Visits the vertices reachable from `root` (following edges according
818    /// to `mode` in directed graphs; `mode` is ignored for undirected graphs)
819    /// and returns the visiting order, the *layers* (vertices grouped by
820    /// their distance from `root`) and the BFS-tree parents. This is the
821    /// lighter alternative to [`Graph::bfs`] when only these outputs matter.
822    ///
823    /// Time complexity: O(|V| + |E|).
824    ///
825    /// Binds [`igraph_bfs_simple`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_bfs_simple).
826    ///
827    /// See also [`Graph::neighborhood`] (the vertices within a number of
828    /// hops, possibly from several roots) and [`Graph::eccentricity`] (which,
829    /// computed with the same `mode`, is `num_layers() - 1`: igraph ignores
830    /// unreachable vertices there too).
831    ///
832    /// # Errors
833    ///
834    /// [`ErrorKind::InvalidVertexId`] if `root` does not exist (checked on
835    /// the Rust side: igraph 1.0.0 and 1.0.1 do not validate it).
836    ///
837    /// # Examples
838    ///
839    /// The layers of a complete binary tree with 7 vertices:
840    ///
841    /// ```
842    /// use igraph::prelude::*;
843    ///
844    /// let t = Graph::kary_tree(7, 2, TreeMode::Undirected)?;
845    /// let r = t.bfs_simple(0, NeighborMode::All)?;
846    /// assert_eq!(r.num_layers(), 3);
847    /// assert_eq!(r.layer(0), [0]);
848    /// assert_eq!(r.layer(1), [1, 2]);
849    /// assert_eq!(r.layer(2), [3, 4, 5, 6]);
850    /// assert_eq!(r.parents[4], Some(1));
851    /// // The eccentricity of the root is the index of the last layer.
852    /// assert_eq!(t.eccentricity(0, None, NeighborMode::All)?, [2.0]);
853    /// # Ok::<(), igraph::Error>(())
854    /// ```
855    pub fn bfs_simple(&self, root: VertexId, mode: NeighborMode) -> Result<BfsSimpleResult> {
856        check_vertex(self, root, "root")?;
857        let mut order = VectorInt::new();
858        let mut layers = VectorInt::new();
859        let mut parents = VectorInt::new();
860        igraph_call!(igraph_bfs_simple(
861            self,
862            root,
863            mode.into(),
864            &mut order,
865            &mut layers,
866            &mut parents,
867        ))?;
868        Ok(BfsSimpleResult {
869            order: order.into(),
870            layers: layers.iter().map(|&x| x as usize).collect(),
871            parents: opt_ids(parents),
872        })
873    }
874
875    fn dfs_impl(
876        &self,
877        root: VertexId,
878        options: &DfsOptions,
879        callbacks: (igraph_dfshandler_t, igraph_dfshandler_t),
880        extra: *mut c_void,
881    ) -> Result<DfsResult> {
882        check_vertex(self, root, "root")?;
883        let mut order = VectorInt::new();
884        let mut order_out = VectorInt::new();
885        let mut parents = VectorInt::new();
886        igraph_call!(igraph_dfs(
887            self,
888            root,
889            options.mode.into(),
890            options.unreachable,
891            &mut order,
892            &mut order_out,
893            &mut parents,
894            ptr::null_mut(),
895            callbacks.0,
896            callbacks.1,
897            extra,
898        ))?;
899        let order = visited_prefix(order);
900        let parents = opt_ids(parents);
901        // igraph's `dist` output drifts after a restart with `unreachable`
902        // (igraph 1.0.0 and 1.0.1); recompute it from the parents, which are
903        // always exact. Parents are
904        // discovered before their children, so one pass in `order` suffices.
905        let mut dist: Vec<Option<usize>> = vec![None; self.vcount()];
906        for &v in &order {
907            dist[v as usize] = Some(match parents[v as usize] {
908                Some(p) => dist[p as usize].map_or(0, |d| d + 1),
909                None => 0,
910            });
911        }
912        Ok(DfsResult {
913            order,
914            order_out: visited_prefix(order_out),
915            parents,
916            dist,
917            stopped: false,
918        })
919    }
920
921    /// Depth-first search from a root vertex.
922    ///
923    /// Explores the graph depth first from `root`, following edges according
924    /// to [`options.mode`](DfsOptions::mode) in directed graphs; neighbors
925    /// are tried in adjacency list order (increasing neighbor id). If
926    /// [`options.unreachable`](DfsOptions::unreachable) is set, further
927    /// searches are started from the unvisited vertices, in
928    /// increasing id order, until all vertices are visited.
929    ///
930    /// Returns the discovery order (pre-order), the completion order
931    /// (post-order), the DFS-forest parents and the depth of each vertex,
932    /// see [`DfsResult`]. Use [`Graph::dfs_with`] to react to discovery and
933    /// completion events while the search runs.
934    ///
935    /// Time complexity: O(|V| + |E|).
936    ///
937    /// Binds [`igraph_dfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_dfs).
938    ///
939    /// See also [`Graph::topological_sorting`], [`Graph::is_dag`] and
940    /// [`Graph::find_cycle`] (in [`crate::cycles`]), which answer the most
941    /// common DFS questions directly, and [`Graph::biconnected_components`].
942    ///
943    /// # Errors
944    ///
945    /// [`ErrorKind::InvalidVertexId`] if `root` does not exist (checked on
946    /// the Rust side; igraph 1.0.0 and 1.0.1 would report
947    /// [`ErrorKind::InvalidValue`]). In particular, the graph must have at
948    /// least one vertex.
949    ///
950    /// # Examples
951    ///
952    /// A reverse post-order of a DAG is a topological order:
953    ///
954    /// ```
955    /// use igraph::prelude::*;
956    /// use igraph::visitor::DfsOptions;
957    ///
958    /// // 0 → 1 → 3, 0 → 2 → 3
959    /// let dag = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (2, 3)], 4, true)?;
960    /// let r = dag.dfs(0, &DfsOptions::default())?;
961    /// assert_eq!(r.order, [0, 1, 3, 2]);
962    /// assert_eq!(r.order_out, [3, 1, 2, 0]);
963    /// let topo: Vec<_> = r.order_out.iter().rev().copied().collect();
964    /// assert_eq!(topo, [0, 2, 1, 3]);
965    /// assert_eq!(r.dist[3], Some(2));
966    /// // `topological_sorting` finds another valid order (Kahn's algorithm).
967    /// assert_eq!(dag.topological_sorting(NeighborMode::Out)?, [0, 1, 2, 3]);
968    /// # Ok::<(), igraph::Error>(())
969    /// ```
970    pub fn dfs(&self, root: VertexId, options: &DfsOptions) -> Result<DfsResult> {
971        self.dfs_impl(root, options, (None, None), ptr::null_mut())
972    }
973
974    /// Depth-first search calling a visitor closure on discovery and
975    /// completion of every vertex.
976    ///
977    /// Same traversal as [`Graph::dfs`]; in addition `visitor` receives a
978    /// [`DfsEvent::Discover`] when a vertex is first reached and a
979    /// [`DfsEvent::Finish`] when its whole subtree has been explored (the
980    /// two C callbacks `in_callback` and `out_callback`, merged in a single
981    /// closure so that they can share mutable state). Both events carry the
982    /// depth of the vertex in its DFS tree.
983    ///
984    /// Return [`ControlFlow::Break`] to stop the search: the call still
985    /// returns `Ok` with the partial results and
986    /// [`stopped`](DfsResult::stopped) set. If `visitor` panics, the search
987    /// is aborted and the panic resumes in the caller.
988    ///
989    /// Binds [`igraph_dfs`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_dfs)
990    /// with two [`igraph_dfshandler_t`](https://igraph.org/c/html/latest/igraph-Visitors.html#igraph_dfshandler_t)
991    /// callbacks.
992    ///
993    /// See also [`Graph::bfs_with`] for the breadth-first counterpart.
994    ///
995    /// # Errors
996    ///
997    /// Same as [`Graph::dfs`].
998    ///
999    /// # Examples
1000    ///
1001    /// Print a tree as an indented outline, using the discovery events:
1002    ///
1003    /// ```
1004    /// use igraph::prelude::*;
1005    /// use igraph::visitor::{DfsEvent, DfsOptions};
1006    /// use std::ops::ControlFlow;
1007    ///
1008    /// let t = Graph::from_edges(&[(0, 1), (0, 2), (1, 3)], 4, false)?;
1009    /// let mut outline = String::new();
1010    /// t.dfs_with(0, &DfsOptions::default(), |e| {
1011    ///     if let DfsEvent::Discover { vid, dist } = e {
1012    ///         outline += &format!("{}{vid}\n", "  ".repeat(dist));
1013    ///     }
1014    ///     ControlFlow::Continue(())
1015    /// })?;
1016    /// assert_eq!(outline, "0\n  1\n    3\n  2\n");
1017    /// # Ok::<(), igraph::Error>(())
1018    /// ```
1019    pub fn dfs_with<F>(&self, root: VertexId, options: &DfsOptions, visitor: F) -> Result<DfsResult>
1020    where
1021        F: FnMut(DfsEvent) -> ControlFlow<()>,
1022    {
1023        let mut state = DfsState {
1024            f: visitor,
1025            depth: 0,
1026            stopped: false,
1027        };
1028        let extra = &mut state as *mut DfsState<F> as *mut c_void;
1029        let callbacks = (
1030            Some(dfs_in_trampoline::<F> as _),
1031            Some(dfs_out_trampoline::<F> as _),
1032        );
1033        let mut result = self.dfs_impl(root, options, callbacks, extra)?;
1034        result.stopped = state.stopped;
1035        Ok(result)
1036    }
1037}