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