Skip to main content

xingo_sparse/
index.rs

1//! # Chapter 5.7 — Typed indices
2//!
3//! ## Purpose
4//! Columns and rows of a matrix are both numbered from zero, and passing a
5//! row number where a column number was expected is the classic silent
6//! bug of sparse codes. [`Col`] and [`Row`] are distinct newtypes, so the
7//! compiler rejects that confusion at no runtime cost. They lived in
8//! `xingo-core` (Chapter 1.1) until task L1.2; the containers (Chapter 5.8)
9//! and the basis references (Chapter 5.1) speak them, so they moved down
10//! with the containers and `xingo-core` re-exports them unchanged.
11//!
12//! ## Notation
13//! A matrix has `n` columns, indexed by `j ∈ {0, …, n−1}`, and `m` rows,
14//! indexed by `i ∈ {0, …, m−1}`. `Col(j)` names column `j` and `Row(i)`
15//! names row `i`. In the solver a column is a variable and a row a
16//! constraint.
17//!
18//! ## Representation
19//! Both are `u32` wrappers (`PLAN.md` §3.2; D-0080, Q3): 2³² − 1 entities
20//! is ample, and halving the width of `row_idx` compared with `usize`
21//! saves memory bandwidth in every sparse kernel. Offsets into nonzero
22//! arrays (`col_ptr`, `row_ptr`) are `usize`, because `nnz` may exceed
23//! 2³².
24//!
25//! ## Invariants
26//! (I1) Conversion from `usize` never truncates: [`Col::from_usize`] and
27//!      [`Row::from_usize`] panic beyond `u32::MAX`.
28//! (I2) Range validity (`j < n`, `i < m`) is a property of the structure
29//!      that stores the index and is checked by its `check_invariants()`.
30
31use std::fmt;
32
33use crate::SparseError;
34
35macro_rules! typed_index {
36    ($(#[$doc:meta])* $name:ident, $what:literal, $prefix:literal) => {
37        $(#[$doc])*
38        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
39        pub struct $name(pub u32);
40
41        impl $name {
42            #[doc = concat!("Builds a ", $what, " index from a `usize`.")]
43            ///
44            /// # Panics
45            /// If `i > u32::MAX` (invariant I1).
46            #[inline]
47            pub fn from_usize(i: usize) -> Self {
48                Self(u32::try_from(i).expect(concat!($what, " index exceeds u32::MAX")))
49            }
50
51            /// Returns the index as a `usize`, ready for slice indexing.
52            #[inline]
53            pub const fn index(self) -> usize {
54                self.0 as usize
55            }
56        }
57
58        impl From<u32> for $name {
59            #[inline]
60            fn from(i: u32) -> Self {
61                Self(i)
62            }
63        }
64
65        impl From<$name> for usize {
66            #[inline]
67            fn from(i: $name) -> usize {
68                i.index()
69            }
70        }
71
72        impl fmt::Display for $name {
73            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74                write!(f, concat!($prefix, "{}"), self.0)
75            }
76        }
77    };
78}
79
80typed_index!(
81    /// Index of a column (in the solver: a variable), `0 ≤ j < n`.
82    Col,
83    "column",
84    "c"
85);
86
87typed_index!(
88    /// Index of a row (in the solver: a constraint), `0 ≤ i < m`.
89    Row,
90    "row",
91    "r"
92);
93
94/// Converts a dimension to `u32`, reporting [`SparseError::TooLarge`] on
95/// overflow.
96pub(crate) fn dim_to_u32(what: &'static str, value: usize) -> Result<u32, SparseError> {
97    u32::try_from(value).map_err(|_| SparseError::TooLarge { what, value })
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn round_trip_and_display() {
106        let j = Col::from_usize(7);
107        assert_eq!(j.index(), 7);
108        assert_eq!(usize::from(j), 7);
109        assert_eq!(j.to_string(), "c7");
110        assert_eq!(Row::from(3).to_string(), "r3");
111    }
112
113    #[test]
114    #[cfg(target_pointer_width = "64")]
115    #[should_panic(expected = "exceeds u32::MAX")]
116    fn from_usize_rejects_overflow() {
117        let _ = Row::from_usize(1 << 33);
118    }
119
120    #[test]
121    fn dim_to_u32_reports_overflow() {
122        assert_eq!(dim_to_u32("rows", 5), Ok(5));
123        #[cfg(target_pointer_width = "64")]
124        assert!(matches!(
125            dim_to_u32("rows", 1 << 33),
126            Err(SparseError::TooLarge { .. })
127        ));
128    }
129}