oxgraph_layout_util/build.rs
1//! Build-time layout primitives.
2//!
3//! Builders validate dense IDs against a known count ([`id_to_slot`],
4//! [`slot_or_max`]), convert `usize` slots back into a typed index width
5//! ([`index_from_usize`], [`next_dense_index`]), flatten per-bucket payloads
6//! into CSR-style `(offsets, items)` pairs ([`build_offset_index`]), and lower
7//! native index slices into explicit little-endian words ([`slice_to_le`]).
8//!
9//! Helpers return small typed data enums ([`IdOutOfBounds`], [`OffsetOverflow`])
10//! instead of crate-specific error types; callers map the issue to their own
11//! typed error at the boundary ([`map_offset_overflow`]).
12
13#[cfg(any(feature = "alloc", kani))]
14use alloc::vec::Vec;
15use core::{error::Error, fmt};
16
17use crate::LayoutIndex;
18#[cfg(any(feature = "alloc", kani))]
19use crate::SnapshotWidth;
20
21/// Reasons an ID failed dense bounds validation.
22///
23/// # Performance
24///
25/// Formatting is `O(message length)`.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27#[non_exhaustive]
28pub enum IdOutOfBounds {
29 /// The ID's value did not fit in `usize` on the current target.
30 UsizeOverflow,
31 /// The ID's slot was greater than or equal to the dense count.
32 OutOfRange {
33 /// Slot derived from the ID.
34 slot: usize,
35 /// Exclusive upper bound for the slot.
36 count: usize,
37 },
38}
39
40impl fmt::Display for IdOutOfBounds {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::UsizeOverflow => formatter.write_str("ID value did not fit in usize"),
44 Self::OutOfRange { slot, count } => write!(
45 formatter,
46 "ID slot {slot} is not less than the dense bound {count}"
47 ),
48 }
49 }
50}
51
52impl Error for IdOutOfBounds {}
53
54/// Reasons a `usize` could not be represented in a target index width during
55/// builder offset construction.
56///
57/// # Performance
58///
59/// Formatting is `O(message length)`.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum OffsetOverflow {
63 /// A `usize` value did not fit in the target [`LayoutIndex`] width.
64 IndexOverflow {
65 /// Value that did not fit.
66 value: usize,
67 },
68}
69
70impl fmt::Display for OffsetOverflow {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Self::IndexOverflow { value } => {
74 write!(
75 formatter,
76 "value {value} does not fit in the target index width"
77 )
78 }
79 }
80 }
81}
82
83impl Error for OffsetOverflow {}
84
85/// Maps an [`OffsetOverflow`] into a caller-chosen error via `on_overflow`.
86///
87/// Replaces the per-crate `map_offset_overflow` copies: each builder crate
88/// passes a closure constructing its own typed `IdOverflow { value }` variant.
89///
90/// # Performance
91///
92/// This function is `O(1)`.
93#[inline]
94pub fn map_offset_overflow<E>(error: OffsetOverflow, on_overflow: impl FnOnce(usize) -> E) -> E {
95 match error {
96 OffsetOverflow::IndexOverflow { value } => on_overflow(value),
97 }
98}
99
100/// Allocates the next dense ID from `counter`: returns `counter`'s current
101/// value as an `Index` and advances the counter by one.
102///
103/// Replaces the per-builder allocation copies: each builder passes a closure
104/// constructing its own typed `IdOverflow { value }` variant. The error value
105/// is the count that failed to fit the index width, or `usize::MAX` when the
106/// counter itself can no longer advance.
107///
108/// # Errors
109///
110/// Returns `on_overflow(*counter)` when the current count does not fit
111/// `Index`, and `on_overflow(usize::MAX)` when the counter would overflow
112/// `usize`.
113///
114/// # Performance
115///
116/// This function is `O(1)`.
117#[inline]
118pub fn next_dense_index<Index: LayoutIndex, E>(
119 counter: &mut usize,
120 on_overflow: impl Fn(usize) -> E,
121) -> Result<Index, E> {
122 let id = Index::from_usize(*counter).ok_or_else(|| on_overflow(*counter))?;
123 *counter = counter
124 .checked_add(1)
125 .ok_or_else(|| on_overflow(usize::MAX))?;
126 Ok(id)
127}
128
129/// Validates that `id`'s `usize` representation is less than `count`.
130///
131/// # Errors
132///
133/// Returns [`IdOutOfBounds::UsizeOverflow`] when the ID does not fit in
134/// `usize` on the current target, and [`IdOutOfBounds::OutOfRange`] when the
135/// slot is not less than `count`.
136///
137/// # Performance
138///
139/// This function is `O(1)`.
140#[inline]
141pub fn id_to_slot<I: LayoutIndex>(id: I, count: usize) -> Result<usize, IdOutOfBounds> {
142 let slot = id.to_usize().ok_or(IdOutOfBounds::UsizeOverflow)?;
143 if slot < count {
144 Ok(slot)
145 } else {
146 Err(IdOutOfBounds::OutOfRange { slot, count })
147 }
148}
149
150/// Returns `id`'s `usize` representation, or `usize::MAX` when it does not
151/// fit in `usize` on the current target.
152///
153/// Used for fallback display and for slot lookups guarded elsewhere by
154/// [`id_to_slot`]. The `usize::MAX` sentinel is safe because callers compare
155/// against an upper bound less than `usize::MAX` before indexing.
156///
157/// # Performance
158///
159/// This function is `O(1)`.
160#[inline]
161#[must_use]
162pub fn slot_or_max<I: LayoutIndex>(id: I) -> usize {
163 id.to_usize().unwrap_or(usize::MAX)
164}
165
166/// Converts a `usize` value into the target index width.
167///
168/// # Errors
169///
170/// Returns [`OffsetOverflow::IndexOverflow`] when `value` does not fit.
171///
172/// # Performance
173///
174/// This function is `O(1)`.
175#[inline]
176pub fn index_from_usize<O: LayoutIndex>(value: usize) -> Result<O, OffsetOverflow> {
177 O::from_usize(value).ok_or(OffsetOverflow::IndexOverflow { value })
178}
179
180/// Lowers a native index slice into explicit little-endian storage words.
181///
182/// Replaces the per-crate `*_slice_to_le` copies in the CSR and BCSR builders.
183///
184/// # Performance
185///
186/// This function is `O(values.len())`.
187#[cfg(any(feature = "alloc", kani))]
188#[must_use]
189pub fn slice_to_le<W: SnapshotWidth>(values: &[W]) -> Vec<W::LittleEndianWord> {
190 values.iter().copied().map(W::to_le_word).collect()
191}
192
193/// Flattens per-bucket payloads into a `(offsets, items)` pair.
194///
195/// The returned `offsets` vector has length `buckets.len() + 1`. `offsets[0]`
196/// is zero, `offsets[i + 1] - offsets[i]` equals the i-th bucket's length, and
197/// `offsets[buckets.len()]` equals `items.len()`. Items appear in input order
198/// within each bucket and buckets are concatenated in input order.
199///
200/// # Errors
201///
202/// Returns [`OffsetOverflow::IndexOverflow`] when any cumulative offset does
203/// not fit in the target index width.
204///
205/// # Performance
206///
207/// This function is `O(n)` where `n` is the total item count across all
208/// buckets. Allocation matches a single-pass extend-and-grow; no second pass
209/// is performed.
210#[cfg(any(feature = "alloc", kani))]
211pub fn build_offset_index<O, T>(buckets: Vec<Vec<T>>) -> Result<(Vec<O>, Vec<T>), OffsetOverflow>
212where
213 O: LayoutIndex,
214{
215 let mut offsets = Vec::with_capacity(buckets.len() + 1);
216 let mut items: Vec<T> = Vec::new();
217 offsets.push(index_from_usize::<O>(0)?);
218 for bucket in buckets {
219 items.extend(bucket);
220 offsets.push(index_from_usize::<O>(items.len())?);
221 }
222 Ok((offsets, items))
223}