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}