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