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}