Skip to main content

oxgraph_algo/bfs/
epoch.rs

1//! Epoch-marked no-allocation indexed BFS (forward and reverse).
2
3use core::{iter::FusedIterator, marker::PhantomData};
4
5use oxgraph_topology::{
6    ContainsElement, ElementId, ElementIndex, ElementPredecessors, ElementSuccessors,
7};
8
9use crate::bfs::{
10    BfsError,
11    core::{Bfs, EpochCounter, Forward, Reverse, SeededEpochFrontier, ValidatedScratch},
12};
13
14/// Reusable caller-provided scratch for epoch-marked indexed BFS, branded to
15/// a specific topology view type.
16///
17/// The mark slice stores traversal epochs instead of byte flags. Reusing this
18/// value across traversals avoids clearing all `graph.element_bound()` entries
19/// each time; only epoch overflow performs a full mark clear.
20///
21/// The `G` type parameter brands the scratch at compile time: a
22/// `BfsEpochScratch<'_, ViewA>` cannot be passed to a traversal of `ViewB`.
23/// `PhantomData<fn() -> G>` is covariant in `G` (function-return position is
24/// covariant) and crucially keeps `Send` / `Sync` independent of `G`'s
25/// thread-safety, so the scratch remains usable from threads that `G` itself
26/// cannot cross.
27///
28/// The same scratch is usable for both forward and reverse traversal because
29/// epoch advancement is direction-agnostic and the visited semantics of
30/// "already discovered along the current expansion" hold regardless of
31/// whether expansion follows successors or predecessors.
32///
33/// # Performance
34///
35/// Construction clears `O(m)` mark entries for `m = marks.len()`. Each traversal
36/// advances the epoch in `O(1)` except on `u32` overflow, where it clears `O(m)`
37/// marks before continuing. Memory usage is caller-provided `O(m + q)` for mark
38/// and queue lengths `m` and `q`.
39pub struct BfsEpochScratch<'scratch, G>
40where
41    G: ContainsElement + ElementIndex,
42{
43    /// Dense visited epoch marks indexed by `ElementIndex::element_index`.
44    marks: &'scratch mut [u32],
45    /// Queue storage for discovered elements.
46    queue: &'scratch mut [ElementId<G>],
47    /// Epoch counter that owns the per-traversal advance / overflow logic.
48    epoch: EpochCounter,
49    /// Brands the scratch to a specific topology view type without coupling
50    /// `Send`/`Sync` to `G`.
51    _graph: PhantomData<fn() -> G>,
52}
53
54impl<'scratch, G> BfsEpochScratch<'scratch, G>
55where
56    G: ContainsElement + ElementIndex,
57{
58    /// Creates reusable epoch scratch over caller-provided mark and queue
59    /// slices.
60    ///
61    /// The view type `G` is bound by the queue slice's element type
62    /// `ElementId<G>`; type inference picks it up from the queue argument
63    /// when `ElementId<G>` is unique. When inference cannot resolve `G`
64    /// (because multiple view types share the same `ElementId`), pair this
65    /// with [`BfsEpochScratch::for_graph`] or use turbofish.
66    ///
67    /// Existing mark contents are cleared so the first traversal starts from
68    /// a known epoch state. Queue contents are ignored.
69    ///
70    /// # Performance
71    ///
72    /// Clears `O(m)` entries for `m = marks.len()` and performs no heap
73    /// allocation.
74    pub fn new(marks: &'scratch mut [u32], queue: &'scratch mut [ElementId<G>]) -> Self {
75        marks.fill(0);
76        Self {
77            marks,
78            queue,
79            epoch: EpochCounter::default(),
80            _graph: PhantomData,
81        }
82    }
83
84    /// Creates reusable epoch scratch with the view type bound from a
85    /// borrowed reference.
86    ///
87    /// Equivalent to [`BfsEpochScratch::new`] but takes `&G` purely as a
88    /// type witness so callers do not need turbofish when multiple view
89    /// types share `ElementId`. The view reference is consumed only for type
90    /// inference; its runtime value is not retained.
91    ///
92    /// # Performance
93    ///
94    /// Clears `O(m)` entries for `m = marks.len()` and performs no heap
95    /// allocation.
96    pub fn for_graph(
97        _graph: &G,
98        marks: &'scratch mut [u32],
99        queue: &'scratch mut [ElementId<G>],
100    ) -> Self {
101        Self::new(marks, queue)
102    }
103
104    /// Returns the number of mark entries available to traversals.
105    ///
106    /// # Performance
107    ///
108    /// Runs in `O(1)`.
109    #[must_use]
110    pub const fn mark_capacity(&self) -> usize {
111        self.marks.len()
112    }
113
114    /// Returns the number of queue slots available to traversals.
115    ///
116    /// # Performance
117    ///
118    /// Runs in `O(1)`.
119    #[must_use]
120    pub const fn queue_capacity(&self) -> usize {
121        self.queue.len()
122    }
123
124    /// Advances to the next epoch and borrows the mark/queue slices for the
125    /// level-synchronous bounded driver in [`crate::bfs::bounded`].
126    ///
127    /// Returns the freshly advanced epoch alongside disjoint mutable borrows of
128    /// the mark and queue storage, so the bounded BFS reuses this scratch with
129    /// the same epoch-overflow discipline as the per-element iterators.
130    ///
131    /// # Performance
132    ///
133    /// `O(1)` except on epoch overflow, which clears `O(marks.len())` marks.
134    pub(in crate::bfs) fn bounded_parts(&mut self) -> (&mut [u32], &mut [ElementId<G>], u32) {
135        let epoch = self.epoch.advance(self.marks);
136        (&mut *self.marks, &mut *self.queue, epoch)
137    }
138}
139
140/// Forward BFS iterator using caller-provided epoch-marked scratch.
141///
142/// Constructed only via [`breadth_first_search_with_epoch_scratch`]. The
143/// internal storage policy ([`SeededEpochFrontier`]) is intentionally not
144/// part of the public API.
145///
146/// # Performance
147///
148/// For a reachable subgraph with `n` elements and `m` successor entries
149/// inspected, traversal is `O(n + m)` and uses `O(b)` caller-provided scratch
150/// memory for `b = graph.element_bound()`.
151#[must_use = "iterators are lazy and do nothing unless consumed"]
152pub struct BreadthFirstSearchEpochScratch<'graph, 'borrow, G>
153where
154    G: ContainsElement + ElementSuccessors + ElementIndex,
155{
156    /// Underlying generic BFS driver carrying the seeded frontier.
157    inner: Bfs<'graph, G, SeededEpochFrontier<'borrow, G>, Forward>,
158}
159
160impl<G> Iterator for BreadthFirstSearchEpochScratch<'_, '_, G>
161where
162    G: ContainsElement + ElementSuccessors + ElementIndex,
163{
164    type Item = ElementId<G>;
165
166    fn next(&mut self) -> Option<Self::Item> {
167        self.inner.next()
168    }
169}
170
171impl<G> FusedIterator for BreadthFirstSearchEpochScratch<'_, '_, G> where
172    G: ContainsElement + ElementSuccessors + ElementIndex
173{
174}
175
176/// Creates a forward breadth-first traversal using reusable epoch scratch.
177///
178/// Traversal follows outgoing connections and yields each reachable element
179/// at most once. `scratch` must provide at least `graph.element_bound()` mark
180/// entries and queue slots. Unlike
181/// [`crate::breadth_first_search_with_scratch`], construction does not clear
182/// all visited entries on every traversal except when the epoch wraps.
183///
184/// `start` and all IDs returned by `graph.element_successors` must be valid
185/// for `graph` and must map to indexes below `graph.element_bound()`. An
186/// expansion-target index at or past the bound observed at construction time
187/// will panic with [`BfsError::NeighborIndexOutOfBounds`]; this is a topology
188/// contract violation, not a recoverable failure.
189///
190/// # Errors
191///
192/// Returns [`BfsError::VisitedTooSmall`] or [`BfsError::QueueTooSmall`] when
193/// the corresponding scratch storage is smaller than
194/// `graph.element_bound()`, [`BfsError::StartElementNotContained`] when
195/// `start` is not contained in `graph`, or [`BfsError::StartIndexOutOfBounds`]
196/// when `start` maps outside `graph.element_bound()`. Errors are returned in
197/// that order: scratch sizes are checked before start-element validation.
198///
199/// # Performance
200///
201/// Construction is `O(1)` after scratch capacity validation except on epoch
202/// overflow, where it clears `O(m)` mark entries for `m = scratch.mark_capacity()`.
203/// Traversal over a reachable subgraph with `n` elements and `m` successor
204/// entries inspected is `O(n + m)` and performs no heap allocation.
205pub fn breadth_first_search_with_epoch_scratch<'graph, 'borrow, 'scratch, G>(
206    graph: &'graph G,
207    start: ElementId<G>,
208    scratch: &'borrow mut BfsEpochScratch<'scratch, G>,
209) -> Result<BreadthFirstSearchEpochScratch<'graph, 'borrow, G>, BfsError>
210where
211    'scratch: 'borrow,
212    G: ContainsElement + ElementSuccessors + ElementIndex,
213{
214    let witness = ValidatedScratch::new(graph, start, scratch.marks.len(), scratch.queue.len())?;
215    let epoch = scratch.epoch.advance(scratch.marks);
216    let frontier = SeededEpochFrontier::new(scratch.marks, scratch.queue, epoch, start, witness);
217    Ok(BreadthFirstSearchEpochScratch {
218        inner: Bfs::new(graph, frontier),
219    })
220}
221
222/// Reverse BFS iterator using caller-provided epoch-marked scratch.
223///
224/// Constructed only via [`reverse_breadth_first_search_with_epoch_scratch`].
225/// The internal storage policy ([`SeededEpochFrontier`]) is intentionally not
226/// part of the public API.
227///
228/// # Performance
229///
230/// For a reverse-reachable subgraph with `n` elements and `m` predecessor
231/// entries inspected, traversal is `O(n + m)` and uses `O(b)` caller-provided
232/// scratch memory for `b = graph.element_bound()`.
233#[must_use = "iterators are lazy and do nothing unless consumed"]
234pub struct ReverseBreadthFirstSearchEpochScratch<'graph, 'borrow, G>
235where
236    G: ContainsElement + ElementPredecessors + ElementIndex,
237{
238    /// Underlying generic BFS driver carrying the seeded frontier and reverse
239    /// direction selector.
240    inner: Bfs<'graph, G, SeededEpochFrontier<'borrow, G>, Reverse>,
241}
242
243impl<G> Iterator for ReverseBreadthFirstSearchEpochScratch<'_, '_, G>
244where
245    G: ContainsElement + ElementPredecessors + ElementIndex,
246{
247    type Item = ElementId<G>;
248
249    fn next(&mut self) -> Option<Self::Item> {
250        self.inner.next()
251    }
252}
253
254impl<G> FusedIterator for ReverseBreadthFirstSearchEpochScratch<'_, '_, G> where
255    G: ContainsElement + ElementPredecessors + ElementIndex
256{
257}
258
259/// Creates a reverse breadth-first traversal using reusable epoch scratch.
260///
261/// Traversal follows incoming connections and yields each reverse-reachable
262/// element at most once. Same scratch / contract / error shape as
263/// [`breadth_first_search_with_epoch_scratch`]; the only difference is that
264/// expansion uses
265/// [`ElementPredecessors`](oxgraph_topology::ElementPredecessors) instead of
266/// [`ElementSuccessors`](oxgraph_topology::ElementSuccessors).
267///
268/// # Errors
269///
270/// Same as [`breadth_first_search_with_epoch_scratch`].
271///
272/// # Performance
273///
274/// Same as [`breadth_first_search_with_epoch_scratch`] with predecessor
275/// expansion in place of successor expansion.
276pub fn reverse_breadth_first_search_with_epoch_scratch<'graph, 'borrow, 'scratch, G>(
277    graph: &'graph G,
278    start: ElementId<G>,
279    scratch: &'borrow mut BfsEpochScratch<'scratch, G>,
280) -> Result<ReverseBreadthFirstSearchEpochScratch<'graph, 'borrow, G>, BfsError>
281where
282    'scratch: 'borrow,
283    G: ContainsElement + ElementPredecessors + ElementIndex,
284{
285    let witness = ValidatedScratch::new(graph, start, scratch.marks.len(), scratch.queue.len())?;
286    let epoch = scratch.epoch.advance(scratch.marks);
287    let frontier = SeededEpochFrontier::new(scratch.marks, scratch.queue, epoch, start, witness);
288    Ok(ReverseBreadthFirstSearchEpochScratch {
289        inner: Bfs::new(graph, frontier),
290    })
291}