Skip to main content

oxgraph_layout_util/
lib.rs

1//! Shared layout primitives for `OxGraph` graph and hypergraph crates.
2//!
3//! Three responsibilities live here:
4//!
5//! - **Index/word vocabulary** ([`LayoutIndex`], [`LayoutWord`], [`LayoutSnapshotWord`],
6//!   [`SnapshotWidth`]): the single sealed set of dense index widths, native/little-endian storage
7//!   words, and the width-to-LE-word bijection shared by every layout and snapshot crate. CSR and
8//!   BCSR add only thin section-kind-bearing sub-traits on top.
9//! - **Build-time** ([`id_to_slot`], [`slot_or_max`], [`index_from_usize`], [`build_offset_index`],
10//!   [`slice_to_le`], [`map_offset_overflow`]): validate dense IDs against a known count, convert
11//!   `usize` slots back into a typed index width, flatten per-bucket payloads into CSR-style
12//!   `(offsets, items)` pairs, and lower native index slices into explicit little-endian words.
13//! - **Read-time** ([`OffsetIntegrityIssue`], [`check_offsets_monotonic`], [`check_value_range`],
14//!   [`check_offset_section`], [`index_to_usize_validated`], [`usize_to_index_validated`]): walk
15//!   borrowed offset arrays at view-open time and convert already-validated indexes infallibly.
16//!
17//! Helpers return small typed data enums ([`IdOutOfBounds`], [`OffsetOverflow`],
18//! [`OffsetIntegrityIssue`]) instead of crate-specific error types. Callers map
19//! the issue to their own typed error at the boundary.
20//!
21//! [`LocalId`] is the one generic local-handle newtype every layout crate
22//! aliases for its node/edge/vertex/hyperedge/incidence identities, and
23//! [`IdSlice`] is the one slice-to-handle iterator they all reuse.
24//!
25//! `no_std + alloc` (build-time primitives need `Vec`). No public domain
26//! semantics. No dependency on any other `oxgraph` crate.
27// kani-skip: helpers loop over arbitrary slice lengths and allocate
28// variable-sized buffers; proofs exercise the algebraic contract on bounded
29// fixtures.
30#![no_std]
31
32#[cfg(any(feature = "alloc", kani))]
33extern crate alloc;
34
35#[cfg(kani)]
36extern crate kani;
37
38#[cfg(any(feature = "alloc", kani))]
39use alloc::vec::Vec;
40use core::{error::Error, fmt, hash::Hash, iter::FusedIterator, marker::PhantomData};
41
42use zerocopy::{
43    FromBytes, Immutable, IntoBytes, KnownLayout,
44    byteorder::{LE, U16, U32, U64},
45};
46
47/// Sealed module preventing external types from satisfying the in-crate
48/// index/word traits.
49mod sealed {
50    /// Seals [`super::LayoutIndex`] to the supported unsigned index widths.
51    pub trait LayoutIndex {}
52
53    /// Seals [`super::ZerocopyWord`] to in-tree native and little-endian words.
54    pub trait ZerocopyWord {}
55
56    /// Seals [`super::LayoutSnapshotWord`] to little-endian storage words.
57    pub trait LayoutSnapshotWord {}
58
59    /// Seals [`super::SnapshotWidth`] to the persisted unsigned widths.
60    pub trait SnapshotWidth {}
61
62    /// Seals [`super::Axis`] to the in-tree local-handle axis markers.
63    pub trait Axis {}
64}
65
66// ---------------------------------------------------------------------------
67// Index / word vocabulary
68// ---------------------------------------------------------------------------
69
70/// Unsigned dense ID width usable by graph and hypergraph layouts and builders.
71///
72/// This is the single index-width contract for the whole substrate: it merges
73/// what `oxgraph-csr` and `oxgraph-hyper-bcsr` previously duplicated as
74/// `CsrIndex`/`BcsrIndex`. `usize` is included for native build-time indices;
75/// persisted widths additionally implement [`SnapshotWidth`].
76///
77/// # Performance
78///
79/// Implementations perform checked conversions in `O(1)`.
80pub trait LayoutIndex:
81    sealed::LayoutIndex + Copy + Eq + Ord + fmt::Debug + fmt::Display + Hash + Sized
82{
83    /// The additive identity for this width (the zero offset / first slot).
84    const ZERO: Self;
85
86    /// Converts this ID to `usize` when representable on the current target.
87    ///
88    /// # Performance
89    ///
90    /// This function is `O(1)`.
91    fn to_usize(self) -> Option<usize>;
92
93    /// Converts a `usize` into this ID width when representable.
94    ///
95    /// # Performance
96    ///
97    /// This function is `O(1)`.
98    fn from_usize(value: usize) -> Option<Self>;
99}
100
101/// Implements [`LayoutIndex`] for one unsigned width.
102macro_rules! impl_layout_index {
103    ($index:ty) => {
104        impl sealed::LayoutIndex for $index {}
105
106        impl LayoutIndex for $index {
107            const ZERO: Self = 0;
108
109            fn to_usize(self) -> Option<usize> {
110                usize::try_from(self).ok()
111            }
112
113            fn from_usize(value: usize) -> Option<Self> {
114                Self::try_from(value).ok()
115            }
116        }
117    };
118}
119
120impl_layout_index!(u16);
121impl_layout_index!(u32);
122impl_layout_index!(u64);
123
124impl sealed::LayoutIndex for usize {}
125
126impl LayoutIndex for usize {
127    const ZERO: Self = 0;
128
129    fn to_usize(self) -> Option<usize> {
130        Some(self)
131    }
132
133    fn from_usize(value: usize) -> Option<Self> {
134        Some(value)
135    }
136}
137
138/// Borrowed offset or value word usable by offset-integrity primitives.
139///
140/// Sealed: native unsigned integers and little-endian storage words opt in via
141/// the in-tree macros below. External crates cannot satisfy this trait.
142///
143/// # Performance
144///
145/// Reading a word is expected to be `O(1)`.
146pub trait ZerocopyWord: sealed::ZerocopyWord + Copy {
147    /// Reads this word's value as `usize`, or returns `None` when the value
148    /// does not fit in `usize` on the current target.
149    ///
150    /// # Performance
151    ///
152    /// This method is `O(1)`.
153    fn read_as_usize(self) -> Option<usize>;
154}
155
156/// Implements [`ZerocopyWord`] for one native unsigned integer type.
157macro_rules! impl_native_zerocopy_word {
158    ($word:ty) => {
159        impl sealed::ZerocopyWord for $word {}
160
161        impl ZerocopyWord for $word {
162            fn read_as_usize(self) -> Option<usize> {
163                usize::try_from(self).ok()
164            }
165        }
166    };
167}
168
169impl_native_zerocopy_word!(u16);
170impl_native_zerocopy_word!(u32);
171impl_native_zerocopy_word!(u64);
172impl_native_zerocopy_word!(usize);
173
174/// Implements [`ZerocopyWord`] for one little-endian zerocopy storage word.
175macro_rules! impl_le_zerocopy_word {
176    ($word:ty) => {
177        impl sealed::ZerocopyWord for $word {}
178
179        impl ZerocopyWord for $word {
180            fn read_as_usize(self) -> Option<usize> {
181                usize::try_from(<$word>::get(self)).ok()
182            }
183        }
184    };
185}
186
187impl_le_zerocopy_word!(U16<LE>);
188impl_le_zerocopy_word!(U32<LE>);
189impl_le_zerocopy_word!(U64<LE>);
190
191/// A native-host or little-endian word carrying a typed dense [`LayoutIndex`].
192///
193/// This merges what `oxgraph-csr` and `oxgraph-hyper-bcsr` previously
194/// duplicated as `CsrWord`/`BcsrWord`. The associated [`LayoutWord::Index`]
195/// recovers the logical index value from either a native word (identity) or a
196/// little-endian storage word (byte-order conversion), so a single [`IdSlice`]
197/// drives both build-path and view-path iteration.
198///
199/// # Performance
200///
201/// [`LayoutWord::get`] is expected to be `O(1)`.
202pub trait LayoutWord: Copy + ZerocopyWord {
203    /// Logical dense index recovered from this word.
204    type Index: LayoutIndex;
205
206    /// Reads the logical index value out of this word.
207    ///
208    /// # Performance
209    ///
210    /// This method is `O(1)`.
211    fn get(self) -> Self::Index;
212}
213
214/// Implements [`LayoutWord`] for one native unsigned integer (identity index).
215macro_rules! impl_native_layout_word {
216    ($word:ty) => {
217        impl LayoutWord for $word {
218            type Index = $word;
219
220            fn get(self) -> Self::Index {
221                self
222            }
223        }
224    };
225}
226
227impl_native_layout_word!(u16);
228impl_native_layout_word!(u32);
229impl_native_layout_word!(u64);
230impl_native_layout_word!(usize);
231
232/// Implements [`LayoutWord`] for one little-endian word over a native index.
233macro_rules! impl_le_layout_word {
234    ($word:ty, $index:ty) => {
235        impl LayoutWord for $word {
236            type Index = $index;
237
238            fn get(self) -> Self::Index {
239                <$word>::get(self)
240            }
241        }
242    };
243}
244
245impl_le_layout_word!(U16<LE>, u16);
246impl_le_layout_word!(U32<LE>, u32);
247impl_le_layout_word!(U64<LE>, u64);
248
249/// A little-endian storage word usable in persisted snapshot payloads.
250///
251/// Sealed marker over [`LayoutWord`] plus the zerocopy byte-view bounds. Only
252/// the explicit little-endian words implement it — native integers are
253/// excluded so persisted payloads always carry a defined byte order.
254///
255/// # Performance
256///
257/// `perf: unspecified`; this is a marker trait.
258pub trait LayoutSnapshotWord:
259    sealed::LayoutSnapshotWord + LayoutWord + FromBytes + Immutable + IntoBytes + KnownLayout
260{
261}
262
263/// Implements [`LayoutSnapshotWord`] for one little-endian storage word.
264macro_rules! impl_layout_snapshot_word {
265    ($word:ty) => {
266        impl sealed::LayoutSnapshotWord for $word {}
267
268        impl LayoutSnapshotWord for $word {}
269    };
270}
271
272impl_layout_snapshot_word!(U16<LE>);
273impl_layout_snapshot_word!(U32<LE>);
274impl_layout_snapshot_word!(U64<LE>);
275
276/// A persisted unsigned width with its little-endian storage word.
277///
278/// This is the single width-to-LE-word bijection shared by `oxgraph-csr`,
279/// `oxgraph-hyper-bcsr`, and any future layout crate. `usize` deliberately does
280/// not implement it: persisted snapshots are fixed-width.
281///
282/// # Performance
283///
284/// [`SnapshotWidth::to_le_word`] and [`SnapshotWidth::from_le_word`] are `O(1)`.
285pub trait SnapshotWidth: sealed::SnapshotWidth + LayoutIndex {
286    /// Little-endian storage word for this width.
287    type LittleEndianWord: LayoutSnapshotWord<Index = Self>;
288
289    /// Lowers this logical index into its little-endian storage word.
290    ///
291    /// # Performance
292    ///
293    /// This function is `O(1)`.
294    fn to_le_word(self) -> Self::LittleEndianWord;
295
296    /// Recovers this logical index from a little-endian storage word.
297    ///
298    /// # Performance
299    ///
300    /// This function is `O(1)`.
301    fn from_le_word(word: Self::LittleEndianWord) -> Self;
302}
303
304/// Implements [`SnapshotWidth`] for one width and its little-endian word.
305macro_rules! impl_snapshot_width {
306    ($index:ty, $word:ty) => {
307        impl sealed::SnapshotWidth for $index {}
308
309        impl SnapshotWidth for $index {
310            type LittleEndianWord = $word;
311
312            fn to_le_word(self) -> Self::LittleEndianWord {
313                <$word>::new(self)
314            }
315
316            fn from_le_word(word: Self::LittleEndianWord) -> Self {
317                <$word>::get(word)
318            }
319        }
320    };
321}
322
323impl_snapshot_width!(u16, U16<LE>);
324impl_snapshot_width!(u32, U32<LE>);
325impl_snapshot_width!(u64, U64<LE>);
326
327// ---------------------------------------------------------------------------
328// Local-handle newtype + slice iterator
329// ---------------------------------------------------------------------------
330
331/// Marker for a local-handle axis (node, edge, vertex, hyperedge, incidence).
332///
333/// Sealed: only the in-tree axis markers below implement it, so [`LocalId`]
334/// cannot be branded with an arbitrary external type.
335///
336/// # Performance
337///
338/// `perf: unspecified`; this is a marker trait.
339pub trait Axis: sealed::Axis {}
340
341/// Defines one zero-sized local-handle axis marker.
342macro_rules! define_axis {
343    ($(#[$doc:meta])* $name:ident) => {
344        $(#[$doc])*
345        #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
346        pub struct $name;
347
348        impl sealed::Axis for $name {}
349
350        impl Axis for $name {}
351    };
352}
353
354define_axis!(
355    /// Element axis for binary graphs (a node).
356    NodeAxis
357);
358define_axis!(
359    /// Relation axis for binary graphs (an edge).
360    EdgeAxis
361);
362define_axis!(
363    /// Element axis for hypergraphs (a vertex).
364    VertexAxis
365);
366define_axis!(
367    /// Relation axis for hypergraphs (a hyperedge).
368    HyperedgeAxis
369);
370define_axis!(
371    /// Incidence axis (a participant / endpoint).
372    IncidenceAxis
373);
374
375/// A dense local handle: an axis-branded index value.
376///
377/// Every layout crate aliases this for its node/edge/vertex/hyperedge/incidence
378/// identities so a built graph and its borrowed snapshot view yield the same
379/// handle type. The `Axis` brand is a zero-sized `PhantomData<fn() -> A>` so the
380/// handle stays `Copy`, `Send`, and `Sync` regardless of the marker, and
381/// satisfies the `TopologyId` blanket bound when `Width` does.
382///
383/// # Performance
384///
385/// Copy, compare, order, hash, and debug-format are `O(1)`.
386pub struct LocalId<A, Width> {
387    /// The raw dense index value.
388    value: Width,
389    /// Zero-sized axis brand; `fn() -> A` keeps the handle `Send`/`Sync`.
390    axis: PhantomData<fn() -> A>,
391}
392
393impl<A, Width> LocalId<A, Width> {
394    /// Wraps a raw index value as an axis-branded handle.
395    ///
396    /// # Performance
397    ///
398    /// This function is `O(1)`.
399    #[inline]
400    #[must_use]
401    pub const fn new(value: Width) -> Self {
402        Self {
403            value,
404            axis: PhantomData,
405        }
406    }
407}
408
409impl<A, Width: Copy> LocalId<A, Width> {
410    /// Returns the raw index value of this handle.
411    ///
412    /// # Performance
413    ///
414    /// This function is `O(1)`.
415    #[inline]
416    #[must_use]
417    pub const fn get(self) -> Width {
418        self.value
419    }
420}
421
422impl<A, Width> From<Width> for LocalId<A, Width> {
423    #[inline]
424    fn from(value: Width) -> Self {
425        Self::new(value)
426    }
427}
428
429impl<A, Width: Clone> Clone for LocalId<A, Width> {
430    #[inline]
431    fn clone(&self) -> Self {
432        Self {
433            value: self.value.clone(),
434            axis: PhantomData,
435        }
436    }
437}
438
439impl<A, Width: Copy> Copy for LocalId<A, Width> {}
440
441impl<A, Width: PartialEq> PartialEq for LocalId<A, Width> {
442    #[inline]
443    fn eq(&self, other: &Self) -> bool {
444        self.value == other.value
445    }
446}
447
448impl<A, Width: Eq> Eq for LocalId<A, Width> {}
449
450impl<A, Width: PartialOrd> PartialOrd for LocalId<A, Width> {
451    #[inline]
452    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
453        self.value.partial_cmp(&other.value)
454    }
455}
456
457impl<A, Width: Ord> Ord for LocalId<A, Width> {
458    #[inline]
459    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
460        self.value.cmp(&other.value)
461    }
462}
463
464impl<A, Width: Hash> Hash for LocalId<A, Width> {
465    #[inline]
466    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
467        self.value.hash(state);
468    }
469}
470
471impl<A, Width: fmt::Debug> fmt::Debug for LocalId<A, Width> {
472    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
473        formatter.debug_tuple("LocalId").field(&self.value).finish()
474    }
475}
476
477impl<A, Width: fmt::Display> fmt::Display for LocalId<A, Width> {
478    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
479        self.value.fmt(formatter)
480    }
481}
482
483/// Iterator that maps a borrowed slice of [`LayoutWord`]s into axis-branded
484/// handles, recovering each logical index and wrapping it with `Id::from`.
485///
486/// This is the single slice-to-handle iterator shared by every layout crate's
487/// build-path (native words) and view-path (little-endian words).
488///
489/// # Performance
490///
491/// Advancing is `O(1)`; the iterator borrows and never allocates.
492#[derive(Clone, Debug)]
493pub struct IdSlice<'view, W: LayoutWord, Id> {
494    /// Borrowed iterator over the backing word slice.
495    inner: core::slice::Iter<'view, W>,
496    /// Zero-sized brand for the produced handle type.
497    id: PhantomData<fn() -> Id>,
498}
499
500impl<'view, W: LayoutWord, Id> IdSlice<'view, W, Id> {
501    /// Creates an [`IdSlice`] over a borrowed word slice.
502    ///
503    /// # Performance
504    ///
505    /// This function is `O(1)`.
506    #[inline]
507    #[must_use]
508    pub fn new(slice: &'view [W]) -> Self {
509        Self {
510            inner: slice.iter(),
511            id: PhantomData,
512        }
513    }
514}
515
516impl<W: LayoutWord, Id> Iterator for IdSlice<'_, W, Id>
517where
518    Id: From<W::Index>,
519{
520    type Item = Id;
521
522    #[inline]
523    fn next(&mut self) -> Option<Self::Item> {
524        self.inner.next().map(|word| Id::from(word.get()))
525    }
526
527    #[inline]
528    fn size_hint(&self) -> (usize, Option<usize>) {
529        self.inner.size_hint()
530    }
531}
532
533impl<W: LayoutWord, Id> ExactSizeIterator for IdSlice<'_, W, Id>
534where
535    Id: From<W::Index>,
536{
537    #[inline]
538    fn len(&self) -> usize {
539        self.inner.len()
540    }
541}
542
543impl<W: LayoutWord, Id> DoubleEndedIterator for IdSlice<'_, W, Id>
544where
545    Id: From<W::Index>,
546{
547    #[inline]
548    fn next_back(&mut self) -> Option<Self::Item> {
549        self.inner.next_back().map(|word| Id::from(word.get()))
550    }
551}
552
553impl<W: LayoutWord, Id> FusedIterator for IdSlice<'_, W, Id> where Id: From<W::Index> {}
554
555// ---------------------------------------------------------------------------
556// Build-time primitives
557// ---------------------------------------------------------------------------
558
559/// Reasons an ID failed dense bounds validation.
560///
561/// # Performance
562///
563/// Formatting is `O(message length)`.
564#[derive(Clone, Copy, Debug, Eq, PartialEq)]
565#[non_exhaustive]
566pub enum IdOutOfBounds {
567    /// The ID's value did not fit in `usize` on the current target.
568    UsizeOverflow,
569    /// The ID's slot was greater than or equal to the dense count.
570    OutOfRange {
571        /// Slot derived from the ID.
572        slot: usize,
573        /// Exclusive upper bound for the slot.
574        count: usize,
575    },
576}
577
578impl fmt::Display for IdOutOfBounds {
579    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
580        match self {
581            Self::UsizeOverflow => formatter.write_str("ID value did not fit in usize"),
582            Self::OutOfRange { slot, count } => write!(
583                formatter,
584                "ID slot {slot} is not less than the dense bound {count}"
585            ),
586        }
587    }
588}
589
590impl Error for IdOutOfBounds {}
591
592/// Reasons a `usize` could not be represented in a target index width during
593/// builder offset construction.
594///
595/// # Performance
596///
597/// Formatting is `O(message length)`.
598#[derive(Clone, Copy, Debug, Eq, PartialEq)]
599#[non_exhaustive]
600pub enum OffsetOverflow {
601    /// A `usize` value did not fit in the target [`LayoutIndex`] width.
602    IndexOverflow {
603        /// Value that did not fit.
604        value: usize,
605    },
606}
607
608impl fmt::Display for OffsetOverflow {
609    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
610        match self {
611            Self::IndexOverflow { value } => {
612                write!(
613                    formatter,
614                    "value {value} does not fit in the target index width"
615                )
616            }
617        }
618    }
619}
620
621impl Error for OffsetOverflow {}
622
623/// Maps an [`OffsetOverflow`] into a caller-chosen error via `on_overflow`.
624///
625/// Replaces the per-crate `map_offset_overflow` copies: each builder crate
626/// passes a closure constructing its own typed `IdOverflow { value }` variant.
627///
628/// # Performance
629///
630/// This function is `O(1)`.
631#[inline]
632pub fn map_offset_overflow<E>(error: OffsetOverflow, on_overflow: impl FnOnce(usize) -> E) -> E {
633    match error {
634        OffsetOverflow::IndexOverflow { value } => on_overflow(value),
635    }
636}
637
638/// Validates that `id`'s `usize` representation is less than `count`.
639///
640/// # Errors
641///
642/// Returns [`IdOutOfBounds::UsizeOverflow`] when the ID does not fit in
643/// `usize` on the current target, and [`IdOutOfBounds::OutOfRange`] when the
644/// slot is not less than `count`.
645///
646/// # Performance
647///
648/// This function is `O(1)`.
649#[inline]
650pub fn id_to_slot<I: LayoutIndex>(id: I, count: usize) -> Result<usize, IdOutOfBounds> {
651    let slot = id.to_usize().ok_or(IdOutOfBounds::UsizeOverflow)?;
652    if slot < count {
653        Ok(slot)
654    } else {
655        Err(IdOutOfBounds::OutOfRange { slot, count })
656    }
657}
658
659/// Returns `id`'s `usize` representation, or `usize::MAX` when it does not
660/// fit in `usize` on the current target.
661///
662/// Used for fallback display and for slot lookups guarded elsewhere by
663/// [`id_to_slot`]. The `usize::MAX` sentinel is safe because callers compare
664/// against an upper bound less than `usize::MAX` before indexing.
665///
666/// # Performance
667///
668/// This function is `O(1)`.
669#[inline]
670#[must_use]
671pub fn slot_or_max<I: LayoutIndex>(id: I) -> usize {
672    id.to_usize().unwrap_or(usize::MAX)
673}
674
675/// Converts a `usize` value into the target index width.
676///
677/// # Errors
678///
679/// Returns [`OffsetOverflow::IndexOverflow`] when `value` does not fit.
680///
681/// # Performance
682///
683/// This function is `O(1)`.
684#[inline]
685pub fn index_from_usize<O: LayoutIndex>(value: usize) -> Result<O, OffsetOverflow> {
686    O::from_usize(value).ok_or(OffsetOverflow::IndexOverflow { value })
687}
688
689/// Converts an already-validated index into `usize`.
690///
691/// Returns `None` only when the value does not fit in `usize`. Call sites that
692/// have proven the value fits (because a prior validation pass enforced it)
693/// should `unwrap_or_else(|| unreachable!("validated …"))` with a proof comment;
694/// this helper deliberately does not embed that `unreachable!`, because the
695/// proof obligation belongs to the validated call site, not the shared helper.
696///
697/// # Performance
698///
699/// This function is `O(1)`.
700#[inline]
701#[must_use]
702pub fn index_to_usize_validated<I: LayoutIndex>(value: I) -> Option<usize> {
703    value.to_usize()
704}
705
706/// Converts an already-validated `usize` slot into the target index width.
707///
708/// Returns `None` only when the value does not fit. See
709/// [`index_to_usize_validated`] for the proof-obligation convention.
710///
711/// # Performance
712///
713/// This function is `O(1)`.
714#[inline]
715#[must_use]
716pub fn usize_to_index_validated<I: LayoutIndex>(value: usize) -> Option<I> {
717    I::from_usize(value)
718}
719
720/// Lowers a native index slice into explicit little-endian storage words.
721///
722/// Replaces the per-crate `*_slice_to_le` copies in the CSR and BCSR builders.
723///
724/// # Performance
725///
726/// This function is `O(values.len())`.
727#[cfg(any(feature = "alloc", kani))]
728#[must_use]
729pub fn slice_to_le<W: SnapshotWidth>(values: &[W]) -> Vec<W::LittleEndianWord> {
730    values.iter().copied().map(W::to_le_word).collect()
731}
732
733/// Flattens per-bucket payloads into a `(offsets, items)` pair.
734///
735/// The returned `offsets` vector has length `buckets.len() + 1`. `offsets[0]`
736/// is zero, `offsets[i + 1] - offsets[i]` equals the i-th bucket's length, and
737/// `offsets[buckets.len()]` equals `items.len()`. Items appear in input order
738/// within each bucket and buckets are concatenated in input order.
739///
740/// # Errors
741///
742/// Returns [`OffsetOverflow::IndexOverflow`] when any cumulative offset does
743/// not fit in the target index width.
744///
745/// # Performance
746///
747/// This function is `O(n)` where `n` is the total item count across all
748/// buckets. Allocation matches a single-pass extend-and-grow; no second pass
749/// is performed.
750#[cfg(any(feature = "alloc", kani))]
751pub fn build_offset_index<O, T>(buckets: Vec<Vec<T>>) -> Result<(Vec<O>, Vec<T>), OffsetOverflow>
752where
753    O: LayoutIndex,
754{
755    let mut offsets = Vec::with_capacity(buckets.len() + 1);
756    let mut items: Vec<T> = Vec::new();
757    offsets.push(index_from_usize::<O>(0)?);
758    for bucket in buckets {
759        items.extend(bucket);
760        offsets.push(index_from_usize::<O>(items.len())?);
761    }
762    Ok((offsets, items))
763}
764
765// ---------------------------------------------------------------------------
766// Read-time offset-integrity primitives
767// ---------------------------------------------------------------------------
768
769/// Reasons a borrowed offset or value array failed structural validation.
770///
771/// # Performance
772///
773/// Formatting is `O(message length)`.
774#[derive(Clone, Copy, Debug, Eq, PartialEq)]
775#[non_exhaustive]
776pub enum OffsetIntegrityIssue {
777    /// Length did not match `count + 1`.
778    Length {
779        /// Expected length (`count + 1`).
780        expected: usize,
781        /// Observed length.
782        actual: usize,
783    },
784    /// `expected_count + 1` overflowed `usize`, so no valid length exists.
785    CountOverflow {
786        /// The element count whose successor overflowed.
787        count: usize,
788    },
789    /// `offsets[0]` was not zero.
790    FirstNonZero {
791        /// Observed first offset.
792        actual: usize,
793    },
794    /// `offsets[index] < offsets[index - 1]`.
795    NonMonotonic {
796        /// Index where monotonicity failed.
797        index: usize,
798        /// Offset at `index - 1`.
799        previous: usize,
800        /// Offset at `index`.
801        actual: usize,
802    },
803    /// `offsets[count]` did not match `value_len`.
804    FinalMismatch {
805        /// Observed final offset.
806        final_offset: usize,
807        /// Length of the values array.
808        value_len: usize,
809    },
810    /// A value at `index` was not less than the dense `bound`.
811    ValueOutOfRange {
812        /// Index of the offending value.
813        index: usize,
814        /// Observed value.
815        value: usize,
816        /// Exclusive upper bound.
817        bound: usize,
818    },
819    /// A word's value at `index` did not fit in `usize` on the current target.
820    UsizeOverflow {
821        /// Slice position of the offending word.
822        index: usize,
823    },
824}
825
826impl fmt::Display for OffsetIntegrityIssue {
827    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
828        match self {
829            Self::Length { expected, actual } => write!(
830                formatter,
831                "offsets length {actual} does not match expected {expected}"
832            ),
833            Self::CountOverflow { count } => {
834                write!(formatter, "element count {count} + 1 overflows usize")
835            }
836            Self::FirstNonZero { actual } => {
837                write!(formatter, "first offset {actual} must be zero")
838            }
839            Self::NonMonotonic {
840                index,
841                previous,
842                actual,
843            } => write!(
844                formatter,
845                "offsets[{index}] = {actual} is less than offsets[{}] = {previous}",
846                index - 1
847            ),
848            Self::FinalMismatch {
849                final_offset,
850                value_len,
851            } => write!(
852                formatter,
853                "final offset {final_offset} does not match values length {value_len}"
854            ),
855            Self::ValueOutOfRange {
856                index,
857                value,
858                bound,
859            } => write!(
860                formatter,
861                "values[{index}] = {value} is not less than bound {bound}"
862            ),
863            Self::UsizeOverflow { index } => write!(
864                formatter,
865                "word at slice index {index} did not fit in usize"
866            ),
867        }
868    }
869}
870
871impl Error for OffsetIntegrityIssue {}
872
873/// Verifies `offsets[0] == 0` and that `offsets` is non-decreasing.
874///
875/// # Errors
876///
877/// Returns [`OffsetIntegrityIssue::FirstNonZero`] when the first offset is
878/// non-zero, [`OffsetIntegrityIssue::NonMonotonic`] when an offset decreases
879/// from the previous, and [`OffsetIntegrityIssue::UsizeOverflow`] when a
880/// word's value does not fit in `usize`.
881///
882/// # Performance
883///
884/// This function is `O(offsets.len())`.
885pub fn check_offsets_monotonic<W: ZerocopyWord>(offsets: &[W]) -> Result<(), OffsetIntegrityIssue> {
886    let mut previous: usize = 0;
887    for (index, word) in offsets.iter().copied().enumerate() {
888        let offset = word
889            .read_as_usize()
890            .ok_or(OffsetIntegrityIssue::UsizeOverflow { index })?;
891        if index == 0 {
892            if offset != 0 {
893                return Err(OffsetIntegrityIssue::FirstNonZero { actual: offset });
894            }
895        } else if offset < previous {
896            return Err(OffsetIntegrityIssue::NonMonotonic {
897                index,
898                previous,
899                actual: offset,
900            });
901        }
902        previous = offset;
903    }
904    Ok(())
905}
906
907/// Verifies every value in `values` is less than `bound`.
908///
909/// # Errors
910///
911/// Returns [`OffsetIntegrityIssue::ValueOutOfRange`] when a value is at or
912/// above `bound`, and [`OffsetIntegrityIssue::UsizeOverflow`] when a word's
913/// value does not fit in `usize`.
914///
915/// # Performance
916///
917/// This function is `O(values.len())`.
918pub fn check_value_range<W: ZerocopyWord>(
919    values: &[W],
920    bound: usize,
921) -> Result<(), OffsetIntegrityIssue> {
922    for (index, word) in values.iter().copied().enumerate() {
923        let value = word
924            .read_as_usize()
925            .ok_or(OffsetIntegrityIssue::UsizeOverflow { index })?;
926        if value >= bound {
927            return Err(OffsetIntegrityIssue::ValueOutOfRange {
928                index,
929                value,
930                bound,
931            });
932        }
933    }
934    Ok(())
935}
936
937/// Validates one offset section against `expected_count` rows and a backing
938/// values array of length `value_len`.
939///
940/// Performs four checks: length matches `expected_count + 1`, first offset is
941/// zero, offsets are non-decreasing, and the final offset matches `value_len`.
942/// Because offsets are monotonic and the final offset equals `value_len`, every
943/// interior offset is implied to be `<= value_len`; the borrowed-view slicing in
944/// CSR/BCSR relies on that consequence and the proofs assert it directly.
945///
946/// # Errors
947///
948/// Returns the first [`OffsetIntegrityIssue`] encountered. Reports
949/// [`OffsetIntegrityIssue::CountOverflow`] when `expected_count + 1` overflows.
950///
951/// # Performance
952///
953/// This function is `O(offsets.len())`.
954pub fn check_offset_section<W: ZerocopyWord>(
955    offsets: &[W],
956    expected_count: usize,
957    value_len: usize,
958) -> Result<(), OffsetIntegrityIssue> {
959    let Some(expected) = expected_count.checked_add(1) else {
960        return Err(OffsetIntegrityIssue::CountOverflow {
961            count: expected_count,
962        });
963    };
964    if offsets.len() != expected {
965        return Err(OffsetIntegrityIssue::Length {
966            expected,
967            actual: offsets.len(),
968        });
969    }
970    check_offsets_monotonic(offsets)?;
971    let final_index = offsets.len() - 1;
972    let final_offset = offsets[final_index]
973        .read_as_usize()
974        .ok_or(OffsetIntegrityIssue::UsizeOverflow { index: final_index })?;
975    if final_offset != value_len {
976        return Err(OffsetIntegrityIssue::FinalMismatch {
977            final_offset,
978            value_len,
979        });
980    }
981    Ok(())
982}
983
984#[cfg(kani)]
985mod proofs;