Skip to main content

oxgraph_hyper_bcsr/
error.rs

1//! Validation and snapshot-binding errors for bipartite-CSR hypergraph views.
2
3use core::fmt;
4
5use oxgraph_snapshot::SectionViewError;
6
7/// Bipartite-CSR validation error.
8///
9/// Returned by [`BcsrHypergraph::open`](crate::BcsrHypergraph::open) and
10/// [`BcsrHypergraph::open_with`](crate::BcsrHypergraph::open_with) when one
11/// of the eight section payloads fails validation.
12///
13/// # Performance
14///
15/// `perf: unspecified`; errors are returned only from validation paths.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub enum BcsrError {
18    /// `count + 1` overflowed `usize`, so the offset slice length cannot fit.
19    OffsetLengthOverflow {
20        /// The count for which `count + 1` overflowed.
21        count: usize,
22    },
23    /// An offset slice has the wrong length.
24    OffsetLength {
25        /// Which offset section this error came from.
26        section: BcsrSection,
27        /// Expected length (`count + 1`).
28        expected: usize,
29        /// Actual length seen.
30        actual: usize,
31    },
32    /// `head_offsets` and `tail_offsets` disagree on `hyperedge_count + 1`.
33    HyperedgeOffsetLengthMismatch {
34        /// `head_offsets.len()`.
35        head_offsets_len: usize,
36        /// `tail_offsets.len()`.
37        tail_offsets_len: usize,
38    },
39    /// `vertex_outgoing_offsets` and `vertex_incoming_offsets` disagree on
40    /// `vertex_count + 1`.
41    VertexOffsetLengthMismatch {
42        /// `vertex_outgoing_offsets.len()`.
43        outgoing_offsets_len: usize,
44        /// `vertex_incoming_offsets.len()`.
45        incoming_offsets_len: usize,
46    },
47    /// The first offset in an offset slice was not zero.
48    FirstOffset {
49        /// Which offset section this error came from.
50        section: BcsrSection,
51        /// Actual first offset.
52        actual: usize,
53    },
54    /// Offsets were not monotonically non-decreasing.
55    NonMonotonicOffset {
56        /// Which offset section this error came from.
57        section: BcsrSection,
58        /// Offset index where monotonicity failed.
59        index: usize,
60        /// Previous offset value.
61        previous: usize,
62        /// Actual offset value at `index`.
63        actual: usize,
64    },
65    /// Final offset does not match the corresponding value slice length.
66    FinalOffset {
67        /// Which offset section this error came from.
68        section: BcsrSection,
69        /// Final offset value.
70        final_offset: usize,
71        /// Length of the value slice this offset references.
72        value_len: usize,
73    },
74    /// A vertex ID was outside `0..vertex_count`.
75    VertexOutOfRange {
76        /// Which value section the bad ID came from.
77        section: BcsrSection,
78        /// Position within that section.
79        index: usize,
80        /// The bad vertex ID.
81        vertex: usize,
82        /// `vertex_count`.
83        vertex_count: usize,
84    },
85    /// A hyperedge ID was outside `0..hyperedge_count`.
86    HyperedgeOutOfRange {
87        /// Which value section the bad ID came from.
88        section: BcsrSection,
89        /// Position within that section.
90        index: usize,
91        /// The bad hyperedge ID.
92        hyperedge: usize,
93        /// `hyperedge_count`.
94        hyperedge_count: usize,
95    },
96    /// `head_participants.len()` and `vertex_outgoing_hyperedges.len()`
97    /// disagree on the total outgoing-incidence count.
98    OutgoingTotalMismatch {
99        /// `head_participants.len()` (`P_head`).
100        head_participants_len: usize,
101        /// `vertex_outgoing_hyperedges.len()` (`P_outgoing`).
102        outgoing_hyperedges_len: usize,
103    },
104    /// `tail_participants.len()` and `vertex_incoming_hyperedges.len()`
105    /// disagree on the total incoming-incidence count.
106    IncomingTotalMismatch {
107        /// `tail_participants.len()` (`P_tail`).
108        tail_participants_len: usize,
109        /// `vertex_incoming_hyperedges.len()` (`P_incoming`).
110        incoming_hyperedges_len: usize,
111    },
112    /// A range-local sequence (e.g. one hyperedge's head participants, or
113    /// one vertex's outgoing hyperedges) was not strictly ascending.
114    ///
115    /// Bipartite-CSR requires set semantics within each range: vertex IDs
116    /// inside a single hyperedge's head/tail and hyperedge IDs inside a
117    /// single vertex's outgoing/incoming must be strictly increasing.
118    NotStrictlyAscending {
119        /// Which value section the bad pair came from.
120        section: BcsrSection,
121        /// Position of the offending value within the section.
122        index: usize,
123        /// Previous value at `index - 1`.
124        previous: usize,
125        /// Actual value at `index`.
126        actual: usize,
127    },
128    /// A stored index value did not fit in `usize` on this target platform.
129    UsizeOverflow {
130        /// Value that could not be represented as `usize`.
131        value: usize,
132    },
133    /// `P_head + P_tail` overflowed `usize`, so the incidence ID space cannot
134    /// be indexed on this target.
135    TotalIncidenceCountOverflow {
136        /// Total head-side incidences (`P_head == P_outgoing`).
137        p_head: usize,
138        /// Total tail-side incidences (`P_tail == P_incoming`).
139        p_tail: usize,
140    },
141    /// Cross-CSR consistency check (Strict-only) found a hyperedge that is
142    /// recorded in one direction but missing from the other.
143    CrossDirectionMismatch {
144        /// Which side of the bipartite index disagreed.
145        side: BcsrRoleSide,
146        /// The hyperedge ID that did not match across the two indexes.
147        hyperedge: usize,
148        /// The vertex ID that did not match across the two indexes.
149        vertex: usize,
150    },
151}
152
153/// Names a single bipartite-CSR section for error reporting.
154///
155/// Carrying this in error variants avoids stringly-typed reasons while
156/// keeping the failing section identifiable from the error alone.
157///
158/// # Performance
159///
160/// `perf: unspecified`; this is a metadata enum.
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum BcsrSection {
163    /// `BCSR_HEAD_OFFSETS`.
164    HeadOffsets,
165    /// `BCSR_HEAD_PARTICIPANTS`.
166    HeadParticipants,
167    /// `BCSR_TAIL_OFFSETS`.
168    TailOffsets,
169    /// `BCSR_TAIL_PARTICIPANTS`.
170    TailParticipants,
171    /// `BCSR_VERTEX_OUTGOING_OFFSETS`.
172    VertexOutgoingOffsets,
173    /// `BCSR_VERTEX_OUTGOING_HYPEREDGES`.
174    VertexOutgoingHyperedges,
175    /// `BCSR_VERTEX_INCOMING_OFFSETS`.
176    VertexIncomingOffsets,
177    /// `BCSR_VERTEX_INCOMING_HYPEREDGES`.
178    VertexIncomingHyperedges,
179}
180
181impl fmt::Display for BcsrSection {
182    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183        formatter.write_str(match self {
184            Self::HeadOffsets => "BCSR_HEAD_OFFSETS",
185            Self::HeadParticipants => "BCSR_HEAD_PARTICIPANTS",
186            Self::TailOffsets => "BCSR_TAIL_OFFSETS",
187            Self::TailParticipants => "BCSR_TAIL_PARTICIPANTS",
188            Self::VertexOutgoingOffsets => "BCSR_VERTEX_OUTGOING_OFFSETS",
189            Self::VertexOutgoingHyperedges => "BCSR_VERTEX_OUTGOING_HYPEREDGES",
190            Self::VertexIncomingOffsets => "BCSR_VERTEX_INCOMING_OFFSETS",
191            Self::VertexIncomingHyperedges => "BCSR_VERTEX_INCOMING_HYPEREDGES",
192        })
193    }
194}
195
196/// Side of the bipartite index that produced a cross-direction mismatch.
197///
198/// # Performance
199///
200/// `perf: unspecified`; this is a metadata enum.
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub enum BcsrRoleSide {
203    /// Hyperedge-major head versus vertex-major outgoing.
204    Outgoing,
205    /// Hyperedge-major tail versus vertex-major incoming.
206    Incoming,
207}
208
209impl fmt::Display for BcsrRoleSide {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        formatter.write_str(match self {
212            Self::Outgoing => "outgoing",
213            Self::Incoming => "incoming",
214        })
215    }
216}
217
218impl fmt::Display for BcsrError {
219    #[expect(
220        clippy::too_many_lines,
221        reason = "one flat exhaustive Display match over every BcsrError variant; splitting it reintroduces the silent _ => Ok(()) wildcards this consolidation removed"
222    )]
223    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224        // One flat, exhaustive match: a new variant forces a Display arm at
225        // compile time, and there is no `_ => Ok(())` wildcard that could
226        // silently format nothing for a mis-routed variant.
227        match self {
228            Self::OffsetLengthOverflow { count } => {
229                write!(formatter, "offset length overflow for count {count}")
230            }
231            Self::OffsetLength {
232                section,
233                expected,
234                actual,
235            } => write!(
236                formatter,
237                "{section} has wrong length: expected {expected}, got {actual}"
238            ),
239            Self::HyperedgeOffsetLengthMismatch {
240                head_offsets_len,
241                tail_offsets_len,
242            } => write!(
243                formatter,
244                "head_offsets length {head_offsets_len} disagrees with tail_offsets length {tail_offsets_len}"
245            ),
246            Self::VertexOffsetLengthMismatch {
247                outgoing_offsets_len,
248                incoming_offsets_len,
249            } => write!(
250                formatter,
251                "vertex_outgoing_offsets length {outgoing_offsets_len} disagrees with vertex_incoming_offsets length {incoming_offsets_len}"
252            ),
253            Self::FirstOffset { section, actual } => {
254                write!(formatter, "{section} first offset must be 0, got {actual}")
255            }
256            Self::NonMonotonicOffset {
257                section,
258                index,
259                previous,
260                actual,
261            } => write!(
262                formatter,
263                "{section} offset at index {index} is not monotonic: previous {previous}, got {actual}"
264            ),
265            Self::FinalOffset {
266                section,
267                final_offset,
268                value_len,
269            } => write!(
270                formatter,
271                "{section} final offset {final_offset} does not match value length {value_len}"
272            ),
273            Self::VertexOutOfRange {
274                section,
275                index,
276                vertex,
277                vertex_count,
278            } => write!(
279                formatter,
280                "{section} vertex {vertex} at index {index} is out of range (vertex count {vertex_count})"
281            ),
282            Self::HyperedgeOutOfRange {
283                section,
284                index,
285                hyperedge,
286                hyperedge_count,
287            } => write!(
288                formatter,
289                "{section} hyperedge {hyperedge} at index {index} is out of range (hyperedge count {hyperedge_count})"
290            ),
291            Self::NotStrictlyAscending {
292                section,
293                index,
294                previous,
295                actual,
296            } => write!(
297                formatter,
298                "{section} value at index {index} is not strictly ascending: previous {previous}, got {actual}"
299            ),
300            Self::OutgoingTotalMismatch {
301                head_participants_len,
302                outgoing_hyperedges_len,
303            } => write!(
304                formatter,
305                "head_participants length {head_participants_len} disagrees with vertex_outgoing_hyperedges length {outgoing_hyperedges_len}"
306            ),
307            Self::IncomingTotalMismatch {
308                tail_participants_len,
309                incoming_hyperedges_len,
310            } => write!(
311                formatter,
312                "tail_participants length {tail_participants_len} disagrees with vertex_incoming_hyperedges length {incoming_hyperedges_len}"
313            ),
314            Self::UsizeOverflow { value } => {
315                write!(formatter, "BCSR index value {value} does not fit usize")
316            }
317            Self::TotalIncidenceCountOverflow { p_head, p_tail } => write!(
318                formatter,
319                "incidence ID space P_head ({p_head}) + P_tail ({p_tail}) overflows usize"
320            ),
321            Self::CrossDirectionMismatch {
322                side,
323                hyperedge,
324                vertex,
325            } => write!(
326                formatter,
327                "cross-direction mismatch on {side}: hyperedge {hyperedge} and vertex {vertex} disagree"
328            ),
329        }
330    }
331}
332
333impl core::error::Error for BcsrError {}
334
335/// Error returned when a snapshot cannot be opened as a bipartite-CSR
336/// hypergraph view.
337///
338/// # Performance
339///
340/// `perf: unspecified`; errors are returned only from snapshot-bound paths.
341#[derive(Clone, Debug, Eq, PartialEq)]
342pub enum BcsrSnapshotError {
343    /// The snapshot is missing one of the eight required sections.
344    MissingSection {
345        /// Which section was missing.
346        section: BcsrSection,
347        /// The kind constant the lookup used.
348        kind: u32,
349    },
350    /// A required section was present but its version did not match.
351    VersionMismatch {
352        /// Which section had the wrong version.
353        section: BcsrSection,
354        /// The kind constant the lookup used.
355        kind: u32,
356        /// Version the reader required.
357        expected: u32,
358        /// Version recorded in the snapshot.
359        actual: u32,
360    },
361    /// A required section payload could not be borrowed as `[U32<LE>]`.
362    SectionView {
363        /// Which section failed the typed-slice cast.
364        section: BcsrSection,
365        /// The underlying snapshot error.
366        error: SectionViewError,
367    },
368    /// One of the offset sections was empty; bipartite-CSR requires at least
369    /// one entry for the n-plus-one layout.
370    OffsetsEmpty {
371        /// Which offset section was empty.
372        section: BcsrSection,
373    },
374    /// A derived count would not fit in `u32`.
375    CountOverflow {
376        /// Which section's length triggered the overflow.
377        section: BcsrSection,
378        /// Length of the offsets section.
379        offsets_len: usize,
380    },
381    /// Bipartite-CSR layout-shape error surfaced through the borrowed sections.
382    Bcsr(BcsrError),
383}
384
385impl fmt::Display for BcsrSnapshotError {
386    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
387        match self {
388            Self::MissingSection { section, kind } => write!(
389                formatter,
390                "snapshot has no {section} section (kind 0x{kind:04x})"
391            ),
392            Self::VersionMismatch {
393                section,
394                kind,
395                expected,
396                actual,
397            } => write!(
398                formatter,
399                "{section} section (kind 0x{kind:04x}) version {actual} does not match expected {expected}"
400            ),
401            Self::SectionView { section, error } => write!(
402                formatter,
403                "{section} cannot be borrowed as little-endian u32: {error}"
404            ),
405            Self::OffsetsEmpty { section } => write!(formatter, "{section} section is empty"),
406            Self::CountOverflow {
407                section,
408                offsets_len,
409            } => write!(
410                formatter,
411                "derived count from {section} length {offsets_len} does not fit u32"
412            ),
413            Self::Bcsr(error) => {
414                write!(formatter, "bipartite-CSR validation failed: {error}")
415            }
416        }
417    }
418}
419
420impl core::error::Error for BcsrSnapshotError {}
421
422impl From<BcsrError> for BcsrSnapshotError {
423    fn from(error: BcsrError) -> Self {
424        Self::Bcsr(error)
425    }
426}