oxgraph_algo/bfs/scratch.rs
1//! Scratch-backed no-allocation indexed BFS (forward and reverse).
2
3use core::iter::FusedIterator;
4
5use oxgraph_topology::{
6 ContainsElement, ElementId, ElementIndex, ElementPredecessors, ElementSuccessors,
7};
8
9use crate::bfs::{
10 BfsError,
11 core::{Bfs, Forward, Reverse, SeededByteFlagFrontier, ValidatedScratch},
12};
13
14/// Forward BFS iterator using caller-provided byte-flag scratch.
15///
16/// Constructed only via [`breadth_first_search_with_scratch`]. The internal
17/// storage policy ([`SeededByteFlagFrontier`]) is intentionally not part of
18/// the public API.
19///
20/// # Performance
21///
22/// For a reachable subgraph with `n` elements and `m` successor entries
23/// inspected, traversal is `O(n + m)` and uses `O(b)` caller-provided scratch
24/// memory for `b = graph.element_bound()`.
25#[must_use = "iterators are lazy and do nothing unless consumed"]
26pub struct BreadthFirstSearchScratch<'graph, 'scratch, G>
27where
28 G: ContainsElement + ElementSuccessors + ElementIndex,
29{
30 /// Underlying generic BFS driver carrying the seeded frontier.
31 inner: Bfs<'graph, G, SeededByteFlagFrontier<'scratch, G>, Forward>,
32}
33
34impl<G> Iterator for BreadthFirstSearchScratch<'_, '_, G>
35where
36 G: ContainsElement + ElementSuccessors + ElementIndex,
37{
38 type Item = ElementId<G>;
39
40 fn next(&mut self) -> Option<Self::Item> {
41 self.inner.next()
42 }
43}
44
45impl<G> FusedIterator for BreadthFirstSearchScratch<'_, '_, G> where
46 G: ContainsElement + ElementSuccessors + ElementIndex
47{
48}
49
50/// Creates a forward breadth-first traversal using caller-provided scratch.
51///
52/// Traversal follows outgoing connections and yields each reachable element at
53/// most once. The start element is yielded first. `visited` and `queue` must
54/// each have at least `graph.element_bound()` entries. The scratch slices are
55/// cleared and reused by the traversal; their previous contents are ignored.
56///
57/// `start` and all IDs returned by `graph.element_successors` must be valid
58/// for `graph` and must map to indexes below `graph.element_bound()`. An
59/// expansion-target index at or past the bound observed at construction time
60/// will panic with [`BfsError::NeighborIndexOutOfBounds`]; this is a topology
61/// contract violation, not a recoverable failure.
62///
63/// # Errors
64///
65/// Returns [`BfsError::VisitedTooSmall`] or [`BfsError::QueueTooSmall`] when
66/// the corresponding scratch slice is smaller than `graph.element_bound()`,
67/// [`BfsError::StartElementNotContained`] when `start` is not contained in
68/// `graph`, or [`BfsError::StartIndexOutOfBounds`] when `start` maps outside
69/// `graph.element_bound()`. Errors are returned in that order: scratch sizes
70/// are checked before start-element validation.
71///
72/// # Performance
73///
74/// Construction clears `O(b)` visited entries for `b = graph.element_bound()`
75/// and performs no heap allocation. Traversal over a reachable subgraph with
76/// `n` elements and `m` successor entries inspected is `O(n + m)` and performs
77/// no heap allocation. Reusing scratch across traversals reuses the same
78/// memory.
79pub fn breadth_first_search_with_scratch<'graph, 'scratch, G>(
80 graph: &'graph G,
81 start: ElementId<G>,
82 visited: &'scratch mut [u8],
83 queue: &'scratch mut [ElementId<G>],
84) -> Result<BreadthFirstSearchScratch<'graph, 'scratch, G>, BfsError>
85where
86 G: ContainsElement + ElementSuccessors + ElementIndex,
87{
88 let witness = ValidatedScratch::new(graph, start, visited.len(), queue.len())?;
89 let frontier = SeededByteFlagFrontier::new(visited, queue, start, witness);
90 Ok(BreadthFirstSearchScratch {
91 inner: Bfs::new(graph, frontier),
92 })
93}
94
95/// Reverse BFS iterator using caller-provided byte-flag scratch.
96///
97/// Constructed only via [`reverse_breadth_first_search_with_scratch`]. The
98/// internal storage policy ([`SeededByteFlagFrontier`]) is intentionally not
99/// part of the public API.
100///
101/// # Performance
102///
103/// For a reverse-reachable subgraph with `n` elements and `m` predecessor
104/// entries inspected, traversal is `O(n + m)` and uses `O(b)` caller-provided
105/// scratch memory for `b = graph.element_bound()`.
106#[must_use = "iterators are lazy and do nothing unless consumed"]
107pub struct ReverseBreadthFirstSearchScratch<'graph, 'scratch, G>
108where
109 G: ContainsElement + ElementPredecessors + ElementIndex,
110{
111 /// Underlying generic BFS driver carrying the seeded frontier and reverse
112 /// direction selector.
113 inner: Bfs<'graph, G, SeededByteFlagFrontier<'scratch, G>, Reverse>,
114}
115
116impl<G> Iterator for ReverseBreadthFirstSearchScratch<'_, '_, G>
117where
118 G: ContainsElement + ElementPredecessors + ElementIndex,
119{
120 type Item = ElementId<G>;
121
122 fn next(&mut self) -> Option<Self::Item> {
123 self.inner.next()
124 }
125}
126
127impl<G> FusedIterator for ReverseBreadthFirstSearchScratch<'_, '_, G> where
128 G: ContainsElement + ElementPredecessors + ElementIndex
129{
130}
131
132/// Creates a reverse breadth-first traversal using caller-provided scratch.
133///
134/// Traversal follows incoming connections and yields each reverse-reachable
135/// element at most once. The start element is yielded first. `visited` and
136/// `queue` must each have at least `graph.element_bound()` entries. The
137/// scratch slices are cleared and reused by the traversal; their previous
138/// contents are ignored.
139///
140/// `start` and all IDs returned by `graph.element_predecessors` must be valid
141/// for `graph` and must map to indexes below `graph.element_bound()`. An
142/// expansion-target index at or past the bound observed at construction time
143/// will panic with [`BfsError::NeighborIndexOutOfBounds`]; this is a topology
144/// contract violation, not a recoverable failure.
145///
146/// # Errors
147///
148/// Returns [`BfsError::VisitedTooSmall`] or [`BfsError::QueueTooSmall`] when
149/// the corresponding scratch slice is smaller than `graph.element_bound()`,
150/// [`BfsError::StartElementNotContained`] when `start` is not contained in
151/// `graph`, or [`BfsError::StartIndexOutOfBounds`] when `start` maps outside
152/// `graph.element_bound()`. Errors are returned in that order.
153///
154/// # Performance
155///
156/// Same shape as [`breadth_first_search_with_scratch`] with predecessor
157/// expansion in place of successor expansion.
158pub fn reverse_breadth_first_search_with_scratch<'graph, 'scratch, G>(
159 graph: &'graph G,
160 start: ElementId<G>,
161 visited: &'scratch mut [u8],
162 queue: &'scratch mut [ElementId<G>],
163) -> Result<ReverseBreadthFirstSearchScratch<'graph, 'scratch, G>, BfsError>
164where
165 G: ContainsElement + ElementPredecessors + ElementIndex,
166{
167 let witness = ValidatedScratch::new(graph, start, visited.len(), queue.len())?;
168 let frontier = SeededByteFlagFrontier::new(visited, queue, start, witness);
169 Ok(ReverseBreadthFirstSearchScratch {
170 inner: Bfs::new(graph, frontier),
171 })
172}