Skip to main content

oxgraph_csr/
lib.rs

1//! Borrowed compressed-sparse-row graph views.
2//!
3//! `oxgraph-csr` provides the first concrete graph layout for the substrate. A
4//! [`CsrGraph`] borrows validated CSR offset and target slices and implements
5//! storage-agnostic graph traits from `oxgraph-graph`.
6//!
7//! CSR is optimized for outgoing traversal. Incoming traversal requires a CSC
8//! index or another reverse index and is intentionally not implemented here.
9//!
10//! # Optional builder
11//!
12//! The `build` feature enables append-only [`build::GraphBuilder`] and
13//! [`build::WeightedGraphBuilder`] types, plus snapshot export helpers, in
14//! the [`build`] submodule. The additional `build-property-arrow` feature
15//! enables property-snapshot export via `oxgraph-property`.
16//!
17//! # Snapshot section kinds
18//!
19//! | Family | Description |
20//! | ------ | ----------- |
21//! | `CSR_OFFSETS_*` | CSR offsets array (`node_count + 1` little-endian words) |
22//! | `CSR_TARGETS_*` | CSR targets array (one little-endian word per edge) |
23//!
24//! The `_U16` / `_U32` / `_U64` suffix selects the little-endian word width.
25//! All section-kind constants are `perf: unspecified` — compile-time `u32`
26//! tags.
27#![no_std]
28
29#[cfg(feature = "build")]
30extern crate alloc;
31
32#[cfg(kani)]
33extern crate kani;
34
35#[cfg(kani)]
36mod proofs;
37
38#[cfg(feature = "build")]
39pub mod build;
40
41use core::{fmt, hash::Hash, marker::PhantomData};
42
43use oxgraph_graph::{
44    ContainsElement, ContainsRelation, EdgeTargetGraph, ElementIndex, ElementSuccessors,
45    GraphCounts, OutgoingEdgeCount, OutgoingGraph, RelationIndex, TopologyBase, TopologyCounts,
46};
47use oxgraph_layout_util::{OffsetIntegrityIssue, check_offset_section, check_value_range};
48use oxgraph_snapshot::{SectionViewError, Snapshot};
49use zerocopy::{
50    FromBytes, Immutable, IntoBytes, KnownLayout,
51    byteorder::{LE, U16, U32, U64},
52};
53
54/// Section kind for a CSR `u16` offsets array.
55pub const SNAPSHOT_KIND_CSR_OFFSETS_U16: u32 = 0x0001;
56/// Section kind for a CSR `u32` offsets array.
57pub const SNAPSHOT_KIND_CSR_OFFSETS_U32: u32 = 0x0002;
58/// Section kind for a CSR `u64` offsets array.
59pub const SNAPSHOT_KIND_CSR_OFFSETS_U64: u32 = 0x0003;
60
61/// Section kind for a CSR `u16` targets array.
62pub const SNAPSHOT_KIND_CSR_TARGETS_U16: u32 = 0x0004;
63/// Section kind for a CSR `u32` targets array.
64pub const SNAPSHOT_KIND_CSR_TARGETS_U32: u32 = 0x0005;
65/// Section kind for a CSR `u64` targets array.
66pub const SNAPSHOT_KIND_CSR_TARGETS_U64: u32 = 0x0006;
67
68/// Private sealing traits for CSR-supported index and snapshot word types.
69mod sealed {
70    /// Seals [`super::CsrIndex`] to the built-in unsigned integer widths.
71    pub trait CsrIndex {}
72
73    /// Seals [`super::CsrSnapshotIndex`] to portable snapshot widths.
74    pub trait CsrSnapshotIndex {}
75
76    /// Seals [`super::CsrSnapshotWord`] to built-in little-endian storage words.
77    pub trait CsrSnapshotWord {}
78}
79
80/// Unsigned native index type usable as a CSR node, edge, offset, and target.
81///
82/// CSR uses one logical index type per view. Implementations are the supported
83/// unsigned widths (`u16`, `u32`, `u64`, and `usize`) and expose checked
84/// conversions to and from `usize`, because Rust slices are indexed by `usize`.
85///
86/// Raw indexes deliberately expose only checked conversions. Infallible
87/// conversion is private to CSR internals and requires a checked slot witness.
88///
89/// ```compile_fail
90/// use oxgraph_csr::CsrIndex;
91///
92/// let raw = 1_u32;
93/// let _ = raw.to_usize_validated();
94/// let _ = <u32 as CsrIndex>::from_usize_validated(0);
95/// ```
96///
97/// # Performance
98///
99/// Copying, comparing, hashing, formatting, and converting supported values are
100/// expected to be `O(1)`.
101pub trait CsrIndex:
102    sealed::CsrIndex + Copy + Eq + Ord + fmt::Debug + fmt::Display + Hash + Sized
103{
104    /// Zero value for this index type.
105    ///
106    /// # Performance
107    ///
108    /// Reading this constant is `O(1)`.
109    const ZERO: Self;
110
111    /// Converts this index value to `usize` when representable on this target.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`CsrError::UsizeOverflow`] when this index value does not fit
116    /// in `usize` on the current target.
117    ///
118    /// # Performance
119    ///
120    /// This method is `O(1)`.
121    fn to_usize(self) -> Option<usize>;
122
123    /// Converts a `usize` to this index type when representable.
124    ///
125    /// # Performance
126    ///
127    /// This method is `O(1)`.
128    #[must_use]
129    fn from_usize(value: usize) -> Option<Self>;
130}
131
132/// Implements [`CsrIndex`] for one native unsigned integer type.
133macro_rules! impl_csr_index {
134    ($index:ty) => {
135        impl sealed::CsrIndex for $index {}
136
137        impl CsrIndex for $index {
138            const ZERO: Self = 0;
139
140            fn to_usize(self) -> Option<usize> {
141                usize::try_from(self).ok()
142            }
143
144            fn from_usize(value: usize) -> Option<Self> {
145                Self::try_from(value).ok()
146            }
147        }
148    };
149}
150
151impl_csr_index!(u16);
152impl_csr_index!(u32);
153impl_csr_index!(u64);
154
155impl sealed::CsrIndex for usize {}
156
157impl CsrIndex for usize {
158    const ZERO: Self = 0;
159
160    fn to_usize(self) -> Option<usize> {
161        Some(self)
162    }
163
164    fn from_usize(value: usize) -> Option<Self> {
165        Some(value)
166    }
167}
168
169/// Portable index width usable for persisted CSR snapshot payloads.
170///
171/// `usize` deliberately does not implement this trait. Snapshot bytes encode
172/// their width through section kinds and use only fixed little-endian unsigned
173/// widths.
174///
175/// # Performance
176///
177/// `perf: unspecified`; implementations provide `O(1)` metadata access and
178/// little-endian word conversion.
179pub trait CsrSnapshotIndex: sealed::CsrSnapshotIndex + CsrIndex {
180    /// Little-endian zerocopy storage word for this logical index type.
181    ///
182    /// # Performance
183    ///
184    /// `perf: unspecified`; this associated type carries no runtime cost.
185    type LittleEndianWord: CsrSnapshotWord<Index = Self>;
186
187    /// Width-specific CSR offsets section kind.
188    ///
189    /// # Performance
190    ///
191    /// Reading this constant is `O(1)`.
192    const OFFSETS_KIND: u32;
193
194    /// Width-specific CSR targets section kind.
195    ///
196    /// # Performance
197    ///
198    /// Reading this constant is `O(1)`.
199    const TARGETS_KIND: u32;
200
201    /// Converts this value into its little-endian CSR snapshot word.
202    ///
203    /// # Performance
204    ///
205    /// This function is `O(1)`.
206    fn to_le_word(self) -> Self::LittleEndianWord;
207}
208
209/// Implements [`CsrSnapshotIndex`] for one portable snapshot width.
210macro_rules! impl_csr_snapshot_index {
211    ($index:ty, $little_endian:ty, $offsets_kind:expr, $targets_kind:expr) => {
212        impl sealed::CsrSnapshotIndex for $index {}
213
214        impl CsrSnapshotIndex for $index {
215            type LittleEndianWord = $little_endian;
216
217            const OFFSETS_KIND: u32 = $offsets_kind;
218            const TARGETS_KIND: u32 = $targets_kind;
219
220            fn to_le_word(self) -> Self::LittleEndianWord {
221                <$little_endian>::new(self)
222            }
223        }
224    };
225}
226
227impl_csr_snapshot_index!(
228    u16,
229    U16<LE>,
230    SNAPSHOT_KIND_CSR_OFFSETS_U16,
231    SNAPSHOT_KIND_CSR_TARGETS_U16
232);
233impl_csr_snapshot_index!(
234    u32,
235    U32<LE>,
236    SNAPSHOT_KIND_CSR_OFFSETS_U32,
237    SNAPSHOT_KIND_CSR_TARGETS_U32
238);
239impl_csr_snapshot_index!(
240    u64,
241    U64<LE>,
242    SNAPSHOT_KIND_CSR_OFFSETS_U64,
243    SNAPSHOT_KIND_CSR_TARGETS_U64
244);
245
246/// Integer word usable in borrowed CSR sections.
247///
248/// Native in-memory graph fixtures use the same type as their logical
249/// [`CsrIndex`]. Snapshot-backed views use unaligned little-endian zerocopy
250/// wrappers such as [`U32<LE>`] and still expose the same host-endian graph ID
251/// type through [`Self::Index`].
252///
253/// Implementors are also `oxgraph_layout_util::ZerocopyWord`, which exposes the
254/// shared `read_as_usize` predicate used by the layout-validation primitives
255/// in `oxgraph-csr-util`. The set of `ZerocopyWord` types is sealed in
256/// `oxgraph-csr-util`, so this supertrait does not widen the public surface.
257///
258/// # Performance
259///
260/// Reading a word is expected to be `O(1)`.
261pub trait CsrWord: Copy + oxgraph_layout_util::ZerocopyWord {
262    /// Host-endian logical index decoded from this word.
263    ///
264    /// # Performance
265    ///
266    /// `perf: unspecified`; this associated type carries no runtime cost.
267    type Index: CsrIndex;
268
269    /// Returns this CSR word as a host-endian index.
270    ///
271    /// # Performance
272    ///
273    /// This method is `O(1)`.
274    fn get(self) -> Self::Index;
275}
276
277/// Implements [`CsrWord`] for one native in-memory index word.
278macro_rules! impl_native_csr_word {
279    ($index:ty) => {
280        impl CsrWord for $index {
281            type Index = $index;
282
283            fn get(self) -> Self::Index {
284                self
285            }
286        }
287    };
288}
289
290impl_native_csr_word!(u16);
291impl_native_csr_word!(u32);
292impl_native_csr_word!(u64);
293impl_native_csr_word!(usize);
294
295/// Implements CSR word traits for one little-endian zerocopy storage word.
296macro_rules! impl_little_endian_csr_word {
297    ($word:ty, $index:ty) => {
298        impl CsrWord for $word {
299            type Index = $index;
300
301            fn get(self) -> Self::Index {
302                Self::get(self)
303            }
304        }
305
306        impl sealed::CsrSnapshotWord for $word {}
307
308        impl CsrSnapshotWord for $word {}
309    };
310}
311
312impl_little_endian_csr_word!(U16<LE>, u16);
313impl_little_endian_csr_word!(U32<LE>, u32);
314impl_little_endian_csr_word!(U64<LE>, u64);
315
316/// Little-endian zerocopy word usable when opening CSR data from a snapshot.
317///
318/// This marker keeps snapshot loading endian-safe: `from_snapshot` is available
319/// for unaligned little-endian storage words, not native-endian primitives.
320///
321/// # Performance
322///
323/// `perf: unspecified`; this marker trait has no methods.
324pub trait CsrSnapshotWord:
325    sealed::CsrSnapshotWord + CsrWord + FromBytes + Immutable + IntoBytes + KnownLayout
326{
327}
328
329/// Native borrowed CSR graph alias.
330///
331/// The node and edge index parameters are spelled explicitly. Target entries
332/// use `NodeIndex`, and offset entries use `EdgeIndex`.
333///
334/// # Performance
335///
336/// `perf: unspecified`; this alias carries no runtime cost.
337pub type CsrNativeGraph<'view, NodeIndex, EdgeIndex> =
338    CsrGraph<'view, NodeIndex, EdgeIndex, EdgeIndex, NodeIndex>;
339
340/// Snapshot-backed little-endian CSR graph alias.
341///
342/// `NodeIndex` selects the target-entry wire width, and `EdgeIndex` selects the
343/// offset-entry wire width. Both widths must be portable snapshot widths
344/// (`u16`, `u32`, or `u64`).
345///
346/// # Performance
347///
348/// `perf: unspecified`; this alias carries no runtime cost.
349pub type CsrSnapshotGraph<'view, NodeIndex, EdgeIndex> = CsrGraph<
350    'view,
351    NodeIndex,
352    EdgeIndex,
353    <EdgeIndex as CsrSnapshotIndex>::LittleEndianWord,
354    <NodeIndex as CsrSnapshotIndex>::LittleEndianWord,
355>;
356
357/// Local node ID for [`CsrGraph`].
358///
359/// Values are dense handles in `0..node_count` for one validated CSR view. They
360/// are topology-local IDs and are not stable across rebuilding or compaction
361/// unless a higher layer defines that contract.
362///
363/// # Performance
364///
365/// Copying, comparing, ordering, hashing, and debug-formatting are `O(1)` when
366/// the underlying index type provides those operations in `O(1)`.
367#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
368pub struct CsrNodeId<Index>(pub Index);
369
370impl<Index> fmt::Debug for CsrNodeId<Index>
371where
372    Index: fmt::Debug,
373{
374    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
375        formatter.debug_tuple("CsrNodeId").field(&self.0).finish()
376    }
377}
378
379/// Local edge ID for [`CsrGraph`].
380///
381/// Values are dense handles into the flat CSR target array. They are
382/// topology-local IDs and are not stable across sorting, rebuilding, or
383/// compaction unless a higher layer defines that contract.
384///
385/// # Performance
386///
387/// Copying, comparing, ordering, hashing, and debug-formatting are `O(1)` when
388/// the underlying index type provides those operations in `O(1)`.
389#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
390pub struct CsrEdgeId<Index>(pub Index);
391
392impl<Index> fmt::Debug for CsrEdgeId<Index>
393where
394    Index: fmt::Debug,
395{
396    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397        formatter.debug_tuple("CsrEdgeId").field(&self.0).finish()
398    }
399}
400
401/// Typestate marker for a slot that still carries an unchecked raw ID.
402#[derive(Clone, Copy, Debug)]
403struct Unchecked;
404
405/// Typestate marker for a slot checked against a validated CSR view.
406#[derive(Clone, Copy, Debug)]
407struct Checked;
408
409/// Node slot branded by checked/unchecked typestate.
410#[derive(Clone, Copy, Debug)]
411struct NodeSlot<State, Index> {
412    /// Raw node index value supplied by a public ID.
413    raw: Index,
414    /// Dense `usize` node slot; meaningful only in the [`Checked`] state.
415    slot: usize,
416    /// Marker carrying the slot typestate.
417    state: PhantomData<fn() -> State>,
418}
419
420impl<Index> NodeSlot<Unchecked, Index> {
421    /// Creates an unchecked node slot from a public node ID.
422    ///
423    /// # Performance
424    ///
425    /// This function is `O(1)`.
426    fn from_id(id: CsrNodeId<Index>) -> Self {
427        Self {
428            raw: id.0,
429            slot: 0,
430            state: PhantomData,
431        }
432    }
433}
434
435impl<Index> NodeSlot<Checked, Index> {
436    /// Creates a checked node slot after graph validation has succeeded.
437    ///
438    /// # Performance
439    ///
440    /// This function is `O(1)`.
441    fn from_raw_slot(raw: Index, slot: usize) -> Self {
442        Self {
443            raw,
444            slot,
445            state: PhantomData,
446        }
447    }
448
449    /// Returns the dense `usize` node slot.
450    ///
451    /// # Performance
452    ///
453    /// This function is `O(1)`.
454    const fn index(&self) -> usize {
455        self.slot
456    }
457}
458
459/// Edge slot branded by checked/unchecked typestate.
460#[derive(Clone, Copy, Debug)]
461struct EdgeSlot<State, Index> {
462    /// Raw edge index value supplied by a public ID or reconstructed from a slot.
463    raw: Index,
464    /// Dense `usize` edge slot; meaningful only in the [`Checked`] state.
465    slot: usize,
466    /// Marker carrying the slot typestate.
467    state: PhantomData<fn() -> State>,
468}
469
470impl<Index> EdgeSlot<Unchecked, Index> {
471    /// Creates an unchecked edge slot from a public edge ID.
472    ///
473    /// # Performance
474    ///
475    /// This function is `O(1)`.
476    fn from_id(id: CsrEdgeId<Index>) -> Self {
477        Self {
478            raw: id.0,
479            slot: 0,
480            state: PhantomData,
481        }
482    }
483}
484
485impl<Index> EdgeSlot<Checked, Index> {
486    /// Creates a checked edge slot after graph validation has succeeded.
487    ///
488    /// # Performance
489    ///
490    /// This function is `O(1)`.
491    fn from_raw_slot(raw: Index, slot: usize) -> Self {
492        Self {
493            raw,
494            slot,
495            state: PhantomData,
496        }
497    }
498
499    /// Reconstructs a checked edge slot from a validated CSR range position.
500    ///
501    /// # Panics
502    ///
503    /// Panics via `unreachable!()` only if CSR validation or range construction
504    /// has been bypassed inside this module. Public callers cannot construct a
505    /// checked edge range.
506    ///
507    /// # Performance
508    ///
509    /// This function is `O(1)`.
510    fn from_csr_range_slot(slot: usize) -> Option<Self>
511    where
512        Index: CsrIndex,
513    {
514        let raw = Index::from_usize(slot)?;
515        Some(Self::from_raw_slot(raw, slot))
516    }
517
518    /// Reconstructs a checked edge slot from a validated CSR range position.
519    ///
520    /// # Panics
521    ///
522    /// Panics via `unreachable!()` only if CSR validation or range construction
523    /// has been bypassed inside this module. Public callers cannot construct a
524    /// checked edge range.
525    ///
526    /// # Performance
527    ///
528    /// This function is `O(1)`.
529    fn from_csr_range_slot_unchecked(slot: usize) -> Self
530    where
531        Index: CsrIndex,
532    {
533        Self::from_csr_range_slot(slot)
534            .unwrap_or_else(|| unreachable!("checked CSR edge slot must fit index type"))
535    }
536
537    /// Returns the dense `usize` edge slot.
538    ///
539    /// # Performance
540    ///
541    /// This function is `O(1)`.
542    const fn index(&self) -> usize {
543        self.slot
544    }
545
546    /// Returns this checked edge slot as a public edge ID.
547    ///
548    /// # Performance
549    ///
550    /// This function is `O(1)`.
551    const fn id(&self) -> CsrEdgeId<Index>
552    where
553        Index: Copy,
554    {
555        CsrEdgeId(self.raw)
556    }
557}
558
559/// Edge-slot range branded by checked/unchecked typestate.
560#[derive(Clone, Copy, Debug)]
561struct EdgeRange<State, Index> {
562    /// Inclusive start slot.
563    start: usize,
564    /// Exclusive end slot.
565    end: usize,
566    /// Marker carrying the range typestate.
567    state: PhantomData<fn() -> State>,
568    /// Marker carrying the logical index type.
569    index: PhantomData<fn() -> Index>,
570}
571
572impl<Index> EdgeRange<Checked, Index>
573where
574    Index: CsrIndex,
575{
576    /// Creates a checked edge range after CSR row offsets have been validated.
577    ///
578    /// # Performance
579    ///
580    /// This function is `O(1)`.
581    fn from_bounds(start: usize, end: usize) -> Self {
582        Self {
583            start,
584            end,
585            state: PhantomData,
586            index: PhantomData,
587        }
588    }
589
590    /// Returns this range as a standard `usize` range.
591    ///
592    /// # Performance
593    ///
594    /// This function is `O(1)`.
595    const fn as_range(&self) -> core::ops::Range<usize> {
596        self.start..self.end
597    }
598
599    /// Returns the number of slots remaining in this range.
600    ///
601    /// # Performance
602    ///
603    /// This function is `O(1)`.
604    const fn len(&self) -> usize {
605        self.end - self.start
606    }
607
608    /// Advances the range and returns the next checked edge slot.
609    ///
610    /// # Performance
611    ///
612    /// This function is `O(1)`.
613    fn next_slot(&mut self) -> Option<EdgeSlot<Checked, Index>> {
614        if self.start == self.end {
615            return None;
616        }
617
618        let slot = EdgeSlot::from_csr_range_slot_unchecked(self.start);
619        self.start += 1;
620        Some(slot)
621    }
622}
623
624/// Borrowed compressed-sparse-row graph view.
625///
626/// The graph stores outgoing adjacency using `offsets[node]..offsets[node + 1]`
627/// ranges into the flat `targets` slice. The view borrows both slices and does
628/// not allocate. `NodeIndex` is the host-endian logical index type used for
629/// node IDs and target entries. `EdgeIndex` is the host-endian logical index
630/// type used for edge IDs and offset entries. The borrowed offset and target
631/// slices may use native words or matching little-endian zerocopy words.
632///
633/// # Performance
634///
635/// Creating a validated view is `O(n + m)` for `n` nodes and `m` edges because
636/// validation checks monotonic offsets and target bounds. Outgoing traversal for
637/// one node is `O(1)` to create and `O(k)` to yield `k` outgoing edges.
638#[derive(Clone, Copy, Debug)]
639pub struct CsrGraph<'view, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
640where
641    NodeIndex: CsrIndex,
642    EdgeIndex: CsrIndex,
643    OffsetWord: CsrWord<Index = EdgeIndex>,
644    TargetWord: CsrWord<Index = NodeIndex>,
645{
646    /// Number of nodes in the graph as the public logical index type.
647    node_count: NodeIndex,
648    /// Number of nodes cached as a validated `usize` slot bound.
649    node_bound: usize,
650    /// CSR offsets with length `node_count + 1`.
651    offsets: &'view [OffsetWord],
652    /// Flat outgoing target node IDs.
653    targets: &'view [TargetWord],
654}
655
656impl<'view, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
657    CsrGraph<'view, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
658where
659    NodeIndex: CsrIndex,
660    EdgeIndex: CsrIndex,
661    OffsetWord: CsrWord<Index = EdgeIndex>,
662    TargetWord: CsrWord<Index = NodeIndex>,
663{
664    /// Validates borrowed CSR sections and returns a graph view.
665    ///
666    /// # Errors
667    ///
668    /// Returns [`CsrError`] when offsets have the wrong length, offsets are not
669    /// monotonic, the final offset does not match `targets.len()`, a target is
670    /// out of range, or an index count cannot be represented as `usize` on the
671    /// current target.
672    ///
673    /// # Performance
674    ///
675    /// Validation is `O(n + m)` for `n` nodes and `m` edges.
676    pub fn validate(
677        node_count: NodeIndex,
678        offsets: &'view [OffsetWord],
679        targets: &'view [TargetWord],
680    ) -> Result<Self, CsrError<NodeIndex, EdgeIndex>> {
681        let node_bound = node_count
682            .to_usize()
683            .ok_or(CsrError::NodeUsizeOverflow { value: node_count })?;
684        if node_bound.checked_add(1).is_none() {
685            return Err(CsrError::OffsetLengthOverflow { node_count });
686        }
687
688        check_offset_section(offsets, node_bound, targets.len())
689            .map_err(|issue| map_offsets_issue::<NodeIndex, EdgeIndex, _>(offsets, issue))?;
690        check_value_range(targets, node_bound).map_err(|issue| {
691            map_targets_issue::<NodeIndex, EdgeIndex, _>(targets, node_count, issue)
692        })?;
693
694        Ok(Self {
695            node_count,
696            node_bound,
697            offsets,
698            targets,
699        })
700    }
701
702    /// Returns the borrowed CSR offset slice.
703    ///
704    /// # Performance
705    ///
706    /// This method is `O(1)`.
707    #[must_use]
708    pub const fn offsets(&self) -> &'view [OffsetWord] {
709        self.offsets
710    }
711
712    /// Returns the borrowed CSR target slice.
713    ///
714    /// # Performance
715    ///
716    /// This method is `O(1)`.
717    #[must_use]
718    pub const fn targets(&self) -> &'view [TargetWord] {
719        self.targets
720    }
721
722    /// Walks outgoing target node ids for `source` via the CSR target slice.
723    ///
724    /// Stops early when `visit` returns `true`. Returns `true` when stopped early.
725    ///
726    /// # Performance
727    ///
728    /// This method is `O(k)` for `k` outgoing edges with no iterator adapters.
729    pub fn for_each_out_target(
730        &self,
731        source: CsrNodeId<NodeIndex>,
732        mut visit: impl FnMut(CsrNodeId<NodeIndex>) -> bool,
733    ) -> bool {
734        let Some(node) = self.try_node_slot(source) else {
735            return false;
736        };
737        let Some(index) = node.raw.to_usize() else {
738            return false;
739        };
740        let Some(start) = self.offsets[index].get().to_usize() else {
741            return false;
742        };
743        let Some(end) = self.offsets[index + 1].get().to_usize() else {
744            return false;
745        };
746        for word in &self.targets[start..end] {
747            if visit(CsrNodeId(word.get())) {
748                return true;
749            }
750        }
751        false
752    }
753
754    /// Returns whether `node` is valid in this CSR view.
755    ///
756    /// # Performance
757    ///
758    /// This method is `O(1)`.
759    #[must_use]
760    pub fn contains_node(&self, node: CsrNodeId<NodeIndex>) -> bool {
761        self.try_node_slot(node).is_some()
762    }
763
764    /// Returns whether `edge` is valid in this CSR view.
765    ///
766    /// # Performance
767    ///
768    /// This method is `O(1)`.
769    #[must_use]
770    pub fn contains_edge(&self, edge: CsrEdgeId<EdgeIndex>) -> bool {
771        self.try_edge_slot(edge).is_some()
772    }
773
774    /// Returns the target node for `edge` when `edge` is valid in this view.
775    ///
776    /// # Performance
777    ///
778    /// This method is `O(1)`.
779    #[must_use]
780    pub fn try_target(&self, edge: CsrEdgeId<EdgeIndex>) -> Option<CsrNodeId<NodeIndex>> {
781        self.try_edge_slot(edge)
782            .map(|checked| self.target_node(checked))
783    }
784
785    /// Checks a public node ID and returns a checked node slot on success.
786    ///
787    /// # Performance
788    ///
789    /// This method is `O(1)`.
790    fn try_node_slot(&self, node: CsrNodeId<NodeIndex>) -> Option<NodeSlot<Checked, NodeIndex>> {
791        self.check_node_slot(NodeSlot::from_id(node))
792    }
793
794    /// Converts an unchecked node slot into a checked node slot.
795    ///
796    /// # Performance
797    ///
798    /// This method is `O(1)`.
799    fn check_node_slot(
800        &self,
801        node: NodeSlot<Unchecked, NodeIndex>,
802    ) -> Option<NodeSlot<Checked, NodeIndex>> {
803        let slot = node.raw.to_usize()?;
804        if node.raw < self.node_count && slot < self.node_bound {
805            Some(NodeSlot::from_raw_slot(node.raw, slot))
806        } else {
807            None
808        }
809    }
810
811    /// Returns a checked node slot or panics on a topology contract violation.
812    ///
813    /// # Panics
814    ///
815    /// Panics when `node` is not a valid node ID for this CSR view. Graph trait
816    /// methods that call this helper require valid IDs by contract.
817    ///
818    /// # Performance
819    ///
820    /// This method is `O(1)`.
821    fn checked_node_slot(&self, node: CsrNodeId<NodeIndex>) -> NodeSlot<Checked, NodeIndex> {
822        self.try_node_slot(node)
823            .unwrap_or_else(|| panic!("CSR node ID {node:?} is invalid for this graph"))
824    }
825
826    /// Checks a public edge ID and returns a checked edge slot on success.
827    ///
828    /// # Performance
829    ///
830    /// This method is `O(1)`.
831    fn try_edge_slot(&self, edge: CsrEdgeId<EdgeIndex>) -> Option<EdgeSlot<Checked, EdgeIndex>> {
832        self.check_edge_slot(EdgeSlot::from_id(edge))
833    }
834
835    /// Converts an unchecked edge slot into a checked edge slot.
836    ///
837    /// # Performance
838    ///
839    /// This method is `O(1)`.
840    fn check_edge_slot(
841        &self,
842        edge: EdgeSlot<Unchecked, EdgeIndex>,
843    ) -> Option<EdgeSlot<Checked, EdgeIndex>> {
844        let slot = edge.raw.to_usize()?;
845        if slot < self.targets.len() {
846            Some(EdgeSlot::from_raw_slot(edge.raw, slot))
847        } else {
848            None
849        }
850    }
851
852    /// Returns a checked edge slot or panics on a topology contract violation.
853    ///
854    /// # Panics
855    ///
856    /// Panics when `edge` is not a valid edge ID for this CSR view. Graph trait
857    /// methods that call this helper require valid IDs by contract.
858    ///
859    /// # Performance
860    ///
861    /// This method is `O(1)`.
862    fn checked_edge_slot(&self, edge: CsrEdgeId<EdgeIndex>) -> EdgeSlot<Checked, EdgeIndex> {
863        self.try_edge_slot(edge)
864            .unwrap_or_else(|| panic!("CSR edge ID {edge:?} is invalid for this graph"))
865    }
866
867    /// Converts a CSR offset value from a validated row into a `usize` slot.
868    ///
869    /// # Panics
870    ///
871    /// Panics via `unreachable!()` only if CSR validation has been bypassed
872    /// inside this module. Validation checks that the final offset fits in
873    /// `usize`, and monotonicity ensures every row offset is at most that final
874    /// offset.
875    ///
876    /// # Performance
877    ///
878    /// This method is `O(1)`.
879    fn checked_offset_slot(offset: EdgeIndex) -> usize {
880        offset
881            .to_usize()
882            .unwrap_or_else(|| unreachable!("checked CSR offset must fit usize"))
883    }
884
885    /// Returns the target node for a checked CSR edge slot.
886    ///
887    /// # Performance
888    ///
889    /// This method is `O(1)` for valid edge IDs from this view.
890    fn target_node(&self, edge: EdgeSlot<Checked, EdgeIndex>) -> CsrNodeId<NodeIndex> {
891        CsrNodeId(self.targets[edge.index()].get())
892    }
893
894    /// Returns the start and end edge slots for a checked node.
895    ///
896    /// # Performance
897    ///
898    /// This method is `O(1)` for valid node IDs from this view.
899    fn outgoing_range(&self, node: NodeSlot<Checked, NodeIndex>) -> EdgeRange<Checked, EdgeIndex> {
900        let index = node.index();
901        EdgeRange::from_bounds(
902            Self::checked_offset_slot(self.offsets[index].get()),
903            Self::checked_offset_slot(self.offsets[index + 1].get()),
904        )
905    }
906}
907
908impl<'view, NodeIndex, EdgeIndex>
909    CsrGraph<
910        'view,
911        NodeIndex,
912        EdgeIndex,
913        <EdgeIndex as CsrSnapshotIndex>::LittleEndianWord,
914        <NodeIndex as CsrSnapshotIndex>::LittleEndianWord,
915    >
916where
917    NodeIndex: CsrSnapshotIndex,
918    EdgeIndex: CsrSnapshotIndex,
919{
920    /// Builds a snapshot-backed CSR view from a validated [`Snapshot`].
921    ///
922    /// Reads the width-specific CSR offsets and targets sections, borrows them
923    /// as little-endian index words, derives `node_count` from
924    /// `offsets.len() - 1`, and runs CSR-shape validation. The returned view
925    /// borrows directly from the snapshot's byte slice and does not copy. Use
926    /// [`CsrSnapshotGraph`] to select node and edge snapshot widths, for example
927    /// `CsrSnapshotGraph<'_, u32, u64>`.
928    ///
929    /// # Errors
930    ///
931    /// Returns [`CsrSnapshotError`] when either section is missing, cannot be
932    /// viewed as the selected word width, is empty, has too many offsets for
933    /// the selected index type, or fails CSR validation.
934    ///
935    /// # Performance
936    ///
937    /// This function is `O(s + n + m)` for `s` snapshot sections, `n` graph
938    /// nodes, and `m` graph edges.
939    pub fn from_snapshot(
940        snapshot: &Snapshot<'view>,
941    ) -> Result<Self, CsrSnapshotError<NodeIndex, EdgeIndex>> {
942        let offsets_section = snapshot
943            .section(EdgeIndex::OFFSETS_KIND)
944            .ok_or(CsrSnapshotError::MissingOffsets)?;
945        let targets_section = snapshot
946            .section(NodeIndex::TARGETS_KIND)
947            .ok_or(CsrSnapshotError::MissingTargets)?;
948
949        let offsets: &'view [<EdgeIndex as CsrSnapshotIndex>::LittleEndianWord] = offsets_section
950            .try_as_slice()
951            .map_err(CsrSnapshotError::OffsetsView)?;
952        let targets: &'view [<NodeIndex as CsrSnapshotIndex>::LittleEndianWord] = targets_section
953            .try_as_slice()
954            .map_err(CsrSnapshotError::TargetsView)?;
955
956        if offsets.is_empty() {
957            return Err(CsrSnapshotError::OffsetsEmpty);
958        }
959
960        let node_count_usize = offsets.len() - 1;
961        let node_count =
962            NodeIndex::from_usize(node_count_usize).ok_or(CsrSnapshotError::NodeCountOverflow {
963                offsets_len: offsets.len(),
964            })?;
965
966        Ok(Self::validate(node_count, offsets, targets)?)
967    }
968}
969
970impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> TopologyBase
971    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
972where
973    NodeIndex: CsrIndex,
974    EdgeIndex: CsrIndex,
975    OffsetWord: CsrWord<Index = EdgeIndex>,
976    TargetWord: CsrWord<Index = NodeIndex>,
977{
978    type ElementId = CsrNodeId<NodeIndex>;
979    type RelationId = CsrEdgeId<EdgeIndex>;
980}
981
982impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> TopologyCounts
983    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
984where
985    NodeIndex: CsrIndex,
986    EdgeIndex: CsrIndex,
987    OffsetWord: CsrWord<Index = EdgeIndex>,
988    TargetWord: CsrWord<Index = NodeIndex>,
989{
990    fn element_count(&self) -> usize {
991        self.node_bound
992    }
993
994    fn relation_count(&self) -> usize {
995        self.targets.len()
996    }
997}
998
999impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> GraphCounts
1000    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1001where
1002    NodeIndex: CsrIndex,
1003    EdgeIndex: CsrIndex,
1004    OffsetWord: CsrWord<Index = EdgeIndex>,
1005    TargetWord: CsrWord<Index = NodeIndex>,
1006{
1007}
1008
1009impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> ElementIndex
1010    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1011where
1012    NodeIndex: CsrIndex,
1013    EdgeIndex: CsrIndex,
1014    OffsetWord: CsrWord<Index = EdgeIndex>,
1015    TargetWord: CsrWord<Index = NodeIndex>,
1016{
1017    fn element_bound(&self) -> usize {
1018        self.node_bound
1019    }
1020
1021    fn element_index(&self, element: CsrNodeId<NodeIndex>) -> usize {
1022        self.checked_node_slot(element).index()
1023    }
1024}
1025
1026impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> RelationIndex
1027    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1028where
1029    NodeIndex: CsrIndex,
1030    EdgeIndex: CsrIndex,
1031    OffsetWord: CsrWord<Index = EdgeIndex>,
1032    TargetWord: CsrWord<Index = NodeIndex>,
1033{
1034    fn relation_bound(&self) -> usize {
1035        self.targets.len()
1036    }
1037
1038    fn relation_index(&self, relation: CsrEdgeId<EdgeIndex>) -> usize {
1039        self.checked_edge_slot(relation).index()
1040    }
1041}
1042
1043impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> ContainsElement
1044    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1045where
1046    NodeIndex: CsrIndex,
1047    EdgeIndex: CsrIndex,
1048    OffsetWord: CsrWord<Index = EdgeIndex>,
1049    TargetWord: CsrWord<Index = NodeIndex>,
1050{
1051    fn contains_element(&self, element: CsrNodeId<NodeIndex>) -> bool {
1052        self.contains_node(element)
1053    }
1054}
1055
1056impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> ContainsRelation
1057    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1058where
1059    NodeIndex: CsrIndex,
1060    EdgeIndex: CsrIndex,
1061    OffsetWord: CsrWord<Index = EdgeIndex>,
1062    TargetWord: CsrWord<Index = NodeIndex>,
1063{
1064    fn contains_relation(&self, relation: CsrEdgeId<EdgeIndex>) -> bool {
1065        self.contains_edge(relation)
1066    }
1067}
1068
1069impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> EdgeTargetGraph
1070    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1071where
1072    NodeIndex: CsrIndex,
1073    EdgeIndex: CsrIndex,
1074    OffsetWord: CsrWord<Index = EdgeIndex>,
1075    TargetWord: CsrWord<Index = NodeIndex>,
1076{
1077    fn target(&self, edge: CsrEdgeId<EdgeIndex>) -> CsrNodeId<NodeIndex> {
1078        self.target_node(self.checked_edge_slot(edge))
1079    }
1080}
1081
1082impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> OutgoingGraph
1083    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1084where
1085    NodeIndex: CsrIndex,
1086    EdgeIndex: CsrIndex,
1087    OffsetWord: CsrWord<Index = EdgeIndex>,
1088    TargetWord: CsrWord<Index = NodeIndex>,
1089{
1090    type OutEdges<'view>
1091        = CsrOutEdges<EdgeIndex>
1092    where
1093        Self: 'view;
1094
1095    fn outgoing_edges(&self, node: CsrNodeId<NodeIndex>) -> Self::OutEdges<'_> {
1096        CsrOutEdges {
1097            range: self.outgoing_range(self.checked_node_slot(node)),
1098        }
1099    }
1100}
1101
1102impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> OutgoingEdgeCount
1103    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1104where
1105    NodeIndex: CsrIndex,
1106    EdgeIndex: CsrIndex,
1107    OffsetWord: CsrWord<Index = EdgeIndex>,
1108    TargetWord: CsrWord<Index = NodeIndex>,
1109{
1110    fn out_degree(&self, node: CsrNodeId<NodeIndex>) -> usize {
1111        self.outgoing_range(self.checked_node_slot(node)).len()
1112    }
1113}
1114
1115impl<NodeIndex, EdgeIndex, OffsetWord, TargetWord> ElementSuccessors
1116    for CsrGraph<'_, NodeIndex, EdgeIndex, OffsetWord, TargetWord>
1117where
1118    NodeIndex: CsrIndex,
1119    EdgeIndex: CsrIndex,
1120    OffsetWord: CsrWord<Index = EdgeIndex>,
1121    TargetWord: CsrWord<Index = NodeIndex>,
1122{
1123    type Successors<'view>
1124        = CsrOutNeighbors<'view, TargetWord>
1125    where
1126        Self: 'view;
1127
1128    fn element_successors(&self, node: CsrNodeId<NodeIndex>) -> Self::Successors<'_> {
1129        let range = self.outgoing_range(self.checked_node_slot(node));
1130        CsrOutNeighbors {
1131            targets: self.targets[range.as_range()].iter(),
1132        }
1133    }
1134}
1135
1136/// Iterator over outgoing CSR edge slots.
1137///
1138/// # Performance
1139///
1140/// Advancing the iterator is `O(1)`.
1141#[derive(Clone, Debug)]
1142pub struct CsrOutEdges<Index> {
1143    /// Checked outgoing edge range remaining to yield.
1144    range: EdgeRange<Checked, Index>,
1145}
1146
1147impl<Index> Iterator for CsrOutEdges<Index>
1148where
1149    Index: CsrIndex,
1150{
1151    type Item = CsrEdgeId<Index>;
1152
1153    fn next(&mut self) -> Option<Self::Item> {
1154        self.range.next_slot().map(|slot| slot.id())
1155    }
1156}
1157
1158impl<Index> ExactSizeIterator for CsrOutEdges<Index>
1159where
1160    Index: CsrIndex,
1161{
1162    fn len(&self) -> usize {
1163        self.range.len()
1164    }
1165}
1166
1167/// Iterator over outgoing CSR target nodes.
1168///
1169/// This iterator borrows the validated target slice for one node's outgoing
1170/// range and yields target node IDs directly. Parallel edges and self-loops are
1171/// preserved because each target entry is yielded once in CSR order.
1172///
1173/// # Performance
1174///
1175/// Advancing the iterator is `O(1)` and performs no allocation.
1176#[derive(Clone, Debug)]
1177pub struct CsrOutNeighbors<'view, StorageWord> {
1178    /// Remaining target words in the outgoing range.
1179    targets: core::slice::Iter<'view, StorageWord>,
1180}
1181
1182impl<StorageWord> Iterator for CsrOutNeighbors<'_, StorageWord>
1183where
1184    StorageWord: CsrWord,
1185{
1186    type Item = CsrNodeId<StorageWord::Index>;
1187
1188    fn next(&mut self) -> Option<Self::Item> {
1189        self.targets.next().map(|word| CsrNodeId(word.get()))
1190    }
1191}
1192
1193impl<StorageWord> ExactSizeIterator for CsrOutNeighbors<'_, StorageWord>
1194where
1195    StorageWord: CsrWord,
1196{
1197    fn len(&self) -> usize {
1198        self.targets.len()
1199    }
1200}
1201
1202/// Maps an offset-side [`OffsetIntegrityIssue`] into a typed [`CsrError`],
1203/// reading the offending word out of `offsets` to populate typed fields.
1204///
1205/// `from_usize` fallbacks would zero-out diagnostic state, so we recover the
1206/// typed offset by indexing back into the original word slice — that read is
1207/// what produced the issue in the first place and is guaranteed in-bounds.
1208fn map_offsets_issue<NodeIndex, EdgeIndex, OffsetWord>(
1209    offsets: &[OffsetWord],
1210    issue: OffsetIntegrityIssue,
1211) -> CsrError<NodeIndex, EdgeIndex>
1212where
1213    NodeIndex: CsrIndex,
1214    EdgeIndex: CsrIndex,
1215    OffsetWord: CsrWord<Index = EdgeIndex>,
1216{
1217    match issue {
1218        OffsetIntegrityIssue::Length { expected, actual } => {
1219            CsrError::OffsetLength { expected, actual }
1220        }
1221        OffsetIntegrityIssue::FirstNonZero { .. } => CsrError::FirstOffset {
1222            actual: offsets[0].get(),
1223        },
1224        OffsetIntegrityIssue::NonMonotonic { index, .. } => CsrError::NonMonotonicOffset {
1225            index,
1226            previous: offsets[index - 1].get(),
1227            actual: offsets[index].get(),
1228        },
1229        OffsetIntegrityIssue::FinalMismatch { value_len, .. } => CsrError::FinalOffset {
1230            final_offset: offsets[offsets.len() - 1].get(),
1231            target_len: value_len,
1232        },
1233        OffsetIntegrityIssue::UsizeOverflow { index } => CsrError::EdgeUsizeOverflow {
1234            value: offsets[index].get(),
1235        },
1236        OffsetIntegrityIssue::ValueOutOfRange { .. } => {
1237            // Offset section never produces ValueOutOfRange; treat as overflow.
1238            CsrError::EdgeUsizeOverflow {
1239                value: EdgeIndex::ZERO,
1240            }
1241        }
1242        _ => CsrError::EdgeUsizeOverflow {
1243            value: EdgeIndex::ZERO,
1244        },
1245    }
1246}
1247
1248/// Maps a target-side [`OffsetIntegrityIssue`] into a typed [`CsrError`],
1249/// reading the offending word out of `targets` and preserving the typed
1250/// `node_count` bound the caller supplied to `check_value_range`.
1251fn map_targets_issue<NodeIndex, EdgeIndex, TargetWord>(
1252    targets: &[TargetWord],
1253    node_count: NodeIndex,
1254    issue: OffsetIntegrityIssue,
1255) -> CsrError<NodeIndex, EdgeIndex>
1256where
1257    NodeIndex: CsrIndex,
1258    EdgeIndex: CsrIndex,
1259    TargetWord: CsrWord<Index = NodeIndex>,
1260{
1261    match issue {
1262        OffsetIntegrityIssue::ValueOutOfRange { index, .. }
1263        | OffsetIntegrityIssue::UsizeOverflow { index } => CsrError::TargetOutOfRange {
1264            index,
1265            target: targets[index].get(),
1266            node_count,
1267        },
1268        _ => CsrError::TargetOutOfRange {
1269            index: 0,
1270            target: NodeIndex::ZERO,
1271            node_count,
1272        },
1273    }
1274}
1275
1276/// CSR validation error.
1277///
1278/// # Performance
1279///
1280/// `perf: unspecified`; errors are returned only from validation paths.
1281#[derive(Clone, Debug, Eq, PartialEq)]
1282pub enum CsrError<NodeIndex, EdgeIndex> {
1283    /// `node_count + 1` overflowed `usize`.
1284    OffsetLengthOverflow {
1285        /// Node count that could not be converted to an offset length.
1286        node_count: NodeIndex,
1287    },
1288    /// Offset slice length does not equal `node_count + 1`.
1289    OffsetLength {
1290        /// Expected offset length.
1291        expected: usize,
1292        /// Actual offset length.
1293        actual: usize,
1294    },
1295    /// The first CSR offset was not zero.
1296    FirstOffset {
1297        /// Actual first offset.
1298        actual: EdgeIndex,
1299    },
1300    /// Offsets were not monotonically increasing.
1301    NonMonotonicOffset {
1302        /// Offset index where monotonicity failed.
1303        index: usize,
1304        /// Previous offset value.
1305        previous: EdgeIndex,
1306        /// Actual offset value at `index`.
1307        actual: EdgeIndex,
1308    },
1309    /// Final offset does not match target slice length.
1310    FinalOffset {
1311        /// Final offset value.
1312        final_offset: EdgeIndex,
1313        /// Target slice length.
1314        target_len: usize,
1315    },
1316    /// Target node ID is outside `0..node_count`.
1317    TargetOutOfRange {
1318        /// Target slice index containing the bad value.
1319        index: usize,
1320        /// Bad target node ID.
1321        target: NodeIndex,
1322        /// Number of nodes in the graph.
1323        node_count: NodeIndex,
1324    },
1325    /// A node index value could not be represented as `usize` on this target.
1326    NodeUsizeOverflow {
1327        /// Node value that could not be represented as `usize`.
1328        value: NodeIndex,
1329    },
1330    /// An edge index value could not be represented as `usize` on this target.
1331    EdgeUsizeOverflow {
1332        /// Edge value that could not be represented as `usize`.
1333        value: EdgeIndex,
1334    },
1335}
1336
1337impl<NodeIndex, EdgeIndex> fmt::Display for CsrError<NodeIndex, EdgeIndex>
1338where
1339    NodeIndex: fmt::Display,
1340    EdgeIndex: fmt::Display,
1341{
1342    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1343        match self {
1344            Self::OffsetLengthOverflow { node_count } => {
1345                write!(
1346                    formatter,
1347                    "offset length overflow for node count {node_count}"
1348                )
1349            }
1350            Self::OffsetLength { expected, actual } => write!(
1351                formatter,
1352                "invalid CSR offset length: expected {expected}, got {actual}"
1353            ),
1354            Self::FirstOffset { actual } => {
1355                write!(formatter, "first CSR offset must be 0, got {actual}")
1356            }
1357            Self::NonMonotonicOffset {
1358                index,
1359                previous,
1360                actual,
1361            } => write!(
1362                formatter,
1363                "CSR offset at index {index} is not monotonic: previous {previous}, got {actual}"
1364            ),
1365            Self::FinalOffset {
1366                final_offset,
1367                target_len,
1368            } => write!(
1369                formatter,
1370                "final CSR offset {final_offset} does not match target length {target_len}"
1371            ),
1372            Self::TargetOutOfRange {
1373                index,
1374                target,
1375                node_count,
1376            } => write!(
1377                formatter,
1378                "CSR target at index {index} is out of range: target {target}, node count {node_count}"
1379            ),
1380            Self::NodeUsizeOverflow { value } => {
1381                write!(formatter, "CSR node index value {value} does not fit usize")
1382            }
1383            Self::EdgeUsizeOverflow { value } => {
1384                write!(formatter, "CSR edge index value {value} does not fit usize")
1385            }
1386        }
1387    }
1388}
1389
1390impl<NodeIndex, EdgeIndex> core::error::Error for CsrError<NodeIndex, EdgeIndex>
1391where
1392    NodeIndex: fmt::Debug + fmt::Display,
1393    EdgeIndex: fmt::Debug + fmt::Display,
1394{
1395}
1396
1397/// Error returned when a snapshot cannot be opened as a CSR graph.
1398///
1399/// # Performance
1400///
1401/// `perf: unspecified`; errors are returned only from snapshot-bound paths.
1402#[derive(Clone, Debug, Eq, PartialEq)]
1403pub enum CsrSnapshotError<NodeIndex, EdgeIndex> {
1404    /// The snapshot has no CSR offsets section for the requested edge width.
1405    MissingOffsets,
1406    /// The snapshot has no CSR targets section for the requested node width.
1407    MissingTargets,
1408    /// The CSR offsets section payload could not be borrowed as the selected
1409    /// little-endian index word slice.
1410    OffsetsView(SectionViewError),
1411    /// The CSR targets section payload could not be borrowed as the selected
1412    /// little-endian index word slice.
1413    TargetsView(SectionViewError),
1414    /// The CSR offsets section is empty; CSR requires at least one entry for
1415    /// the n-plus-one layout.
1416    OffsetsEmpty,
1417    /// The derived node count would not fit in the selected index type.
1418    NodeCountOverflow {
1419        /// Length of the offsets section.
1420        offsets_len: usize,
1421    },
1422    /// CSR-shape validation failed on the borrowed sections.
1423    Csr(CsrError<NodeIndex, EdgeIndex>),
1424}
1425
1426impl<NodeIndex, EdgeIndex> fmt::Display for CsrSnapshotError<NodeIndex, EdgeIndex>
1427where
1428    NodeIndex: fmt::Display,
1429    EdgeIndex: fmt::Display,
1430{
1431    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1432        match self {
1433            Self::MissingOffsets => formatter.write_str("snapshot has no CSR offsets section"),
1434            Self::MissingTargets => formatter.write_str("snapshot has no CSR targets section"),
1435            Self::OffsetsView(error) => write!(
1436                formatter,
1437                "CSR offsets section cannot be borrowed as selected little-endian index words: {error}"
1438            ),
1439            Self::TargetsView(error) => write!(
1440                formatter,
1441                "CSR targets section cannot be borrowed as selected little-endian index words: {error}"
1442            ),
1443            Self::OffsetsEmpty => formatter.write_str("CSR offsets section is empty"),
1444            Self::NodeCountOverflow { offsets_len } => write!(
1445                formatter,
1446                "derived node count from offsets length {offsets_len} does not fit selected CSR index type"
1447            ),
1448            Self::Csr(error) => write!(formatter, "CSR validation failed: {error}"),
1449        }
1450    }
1451}
1452
1453impl<NodeIndex, EdgeIndex> core::error::Error for CsrSnapshotError<NodeIndex, EdgeIndex>
1454where
1455    NodeIndex: fmt::Debug + fmt::Display,
1456    EdgeIndex: fmt::Debug + fmt::Display,
1457{
1458}
1459
1460impl<NodeIndex, EdgeIndex> From<CsrError<NodeIndex, EdgeIndex>>
1461    for CsrSnapshotError<NodeIndex, EdgeIndex>
1462{
1463    fn from(error: CsrError<NodeIndex, EdgeIndex>) -> Self {
1464        Self::Csr(error)
1465    }
1466}