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    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::OffsetLengthOverflow { .. }
222            | Self::OffsetLength { .. }
223            | Self::HyperedgeOffsetLengthMismatch { .. }
224            | Self::VertexOffsetLengthMismatch { .. } => fmt_length_variant(self, formatter),
225            Self::FirstOffset { .. }
226            | Self::NonMonotonicOffset { .. }
227            | Self::FinalOffset { .. } => fmt_offset_variant(self, formatter),
228            Self::VertexOutOfRange { .. }
229            | Self::HyperedgeOutOfRange { .. }
230            | Self::NotStrictlyAscending { .. } => fmt_value_variant(self, formatter),
231            Self::OutgoingTotalMismatch { .. }
232            | Self::IncomingTotalMismatch { .. }
233            | Self::UsizeOverflow { .. }
234            | Self::TotalIncidenceCountOverflow { .. }
235            | Self::CrossDirectionMismatch { .. } => fmt_total_variant(self, formatter),
236        }
237    }
238}
239
240/// Formats the length-shape variants of [`BcsrError`].
241fn fmt_length_variant(error: &BcsrError, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242    match error {
243        BcsrError::OffsetLengthOverflow { count } => {
244            write!(formatter, "offset length overflow for count {count}")
245        }
246        BcsrError::OffsetLength {
247            section,
248            expected,
249            actual,
250        } => write!(
251            formatter,
252            "{section} has wrong length: expected {expected}, got {actual}"
253        ),
254        BcsrError::HyperedgeOffsetLengthMismatch {
255            head_offsets_len,
256            tail_offsets_len,
257        } => write!(
258            formatter,
259            "head_offsets length {head_offsets_len} disagrees with tail_offsets length {tail_offsets_len}"
260        ),
261        BcsrError::VertexOffsetLengthMismatch {
262            outgoing_offsets_len,
263            incoming_offsets_len,
264        } => write!(
265            formatter,
266            "vertex_outgoing_offsets length {outgoing_offsets_len} disagrees with vertex_incoming_offsets length {incoming_offsets_len}"
267        ),
268        _ => Ok(()),
269    }
270}
271
272/// Formats the offset-shape variants of [`BcsrError`].
273fn fmt_offset_variant(error: &BcsrError, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274    match error {
275        BcsrError::FirstOffset { section, actual } => {
276            write!(formatter, "{section} first offset must be 0, got {actual}")
277        }
278        BcsrError::NonMonotonicOffset {
279            section,
280            index,
281            previous,
282            actual,
283        } => write!(
284            formatter,
285            "{section} offset at index {index} is not monotonic: previous {previous}, got {actual}"
286        ),
287        BcsrError::FinalOffset {
288            section,
289            final_offset,
290            value_len,
291        } => write!(
292            formatter,
293            "{section} final offset {final_offset} does not match value length {value_len}"
294        ),
295        _ => Ok(()),
296    }
297}
298
299/// Formats the in-range-value and ascending-order variants of [`BcsrError`].
300fn fmt_value_variant(error: &BcsrError, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301    match error {
302        BcsrError::VertexOutOfRange {
303            section,
304            index,
305            vertex,
306            vertex_count,
307        } => write!(
308            formatter,
309            "{section} vertex {vertex} at index {index} is out of range (vertex count {vertex_count})"
310        ),
311        BcsrError::HyperedgeOutOfRange {
312            section,
313            index,
314            hyperedge,
315            hyperedge_count,
316        } => write!(
317            formatter,
318            "{section} hyperedge {hyperedge} at index {index} is out of range (hyperedge count {hyperedge_count})"
319        ),
320        BcsrError::NotStrictlyAscending {
321            section,
322            index,
323            previous,
324            actual,
325        } => write!(
326            formatter,
327            "{section} value at index {index} is not strictly ascending: previous {previous}, got {actual}"
328        ),
329        _ => Ok(()),
330    }
331}
332
333/// Formats the cross-section total / overflow / consistency variants.
334fn fmt_total_variant(error: &BcsrError, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
335    match error {
336        BcsrError::OutgoingTotalMismatch {
337            head_participants_len,
338            outgoing_hyperedges_len,
339        } => write!(
340            formatter,
341            "head_participants length {head_participants_len} disagrees with vertex_outgoing_hyperedges length {outgoing_hyperedges_len}"
342        ),
343        BcsrError::IncomingTotalMismatch {
344            tail_participants_len,
345            incoming_hyperedges_len,
346        } => write!(
347            formatter,
348            "tail_participants length {tail_participants_len} disagrees with vertex_incoming_hyperedges length {incoming_hyperedges_len}"
349        ),
350        BcsrError::UsizeOverflow { value } => {
351            write!(formatter, "BCSR index value {value} does not fit usize")
352        }
353        BcsrError::TotalIncidenceCountOverflow { p_head, p_tail } => write!(
354            formatter,
355            "incidence ID space P_head ({p_head}) + P_tail ({p_tail}) overflows usize"
356        ),
357        BcsrError::CrossDirectionMismatch {
358            side,
359            hyperedge,
360            vertex,
361        } => write!(
362            formatter,
363            "cross-direction mismatch on {side}: hyperedge {hyperedge} and vertex {vertex} disagree"
364        ),
365        _ => Ok(()),
366    }
367}
368
369impl core::error::Error for BcsrError {}
370
371/// Error returned when a snapshot cannot be opened as a bipartite-CSR
372/// hypergraph view.
373///
374/// # Performance
375///
376/// `perf: unspecified`; errors are returned only from snapshot-bound paths.
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub enum BcsrSnapshotError {
379    /// The snapshot is missing one of the eight required sections.
380    MissingSection {
381        /// Which section was missing.
382        section: BcsrSection,
383        /// The kind constant the lookup used.
384        kind: u32,
385    },
386    /// A required section payload could not be borrowed as `[U32<LE>]`.
387    SectionView {
388        /// Which section failed the typed-slice cast.
389        section: BcsrSection,
390        /// The underlying snapshot error.
391        error: SectionViewError,
392    },
393    /// One of the offset sections was empty; bipartite-CSR requires at least
394    /// one entry for the n-plus-one layout.
395    OffsetsEmpty {
396        /// Which offset section was empty.
397        section: BcsrSection,
398    },
399    /// A derived count would not fit in `u32`.
400    CountOverflow {
401        /// Which section's length triggered the overflow.
402        section: BcsrSection,
403        /// Length of the offsets section.
404        offsets_len: usize,
405    },
406    /// Bipartite-CSR layout-shape error surfaced through the borrowed sections.
407    Bcsr(BcsrError),
408}
409
410impl fmt::Display for BcsrSnapshotError {
411    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            Self::MissingSection { section, kind } => write!(
414                formatter,
415                "snapshot has no {section} section (kind 0x{kind:04x})"
416            ),
417            Self::SectionView { section, error } => write!(
418                formatter,
419                "{section} cannot be borrowed as little-endian u32: {error}"
420            ),
421            Self::OffsetsEmpty { section } => write!(formatter, "{section} section is empty"),
422            Self::CountOverflow {
423                section,
424                offsets_len,
425            } => write!(
426                formatter,
427                "derived count from {section} length {offsets_len} does not fit u32"
428            ),
429            Self::Bcsr(error) => {
430                write!(formatter, "bipartite-CSR validation failed: {error}")
431            }
432        }
433    }
434}
435
436impl core::error::Error for BcsrSnapshotError {}
437
438impl From<BcsrError> for BcsrSnapshotError {
439    fn from(error: BcsrError) -> Self {
440        Self::Bcsr(error)
441    }
442}