Skip to main content

scirs2_numpy/
extended_int.rs

1//! Extended integer type support: `i128` and `u128`.
2//!
3//! NumPy does not expose native `int128` / `uint128` dtypes, but PyO3 can
4//! round-trip Rust `i128` / `u128` values through Python's arbitrary-precision
5//! `int` type.  This module provides:
6//!
7//! - Free functions [`to_i128`], [`from_i128`], [`to_u128`], [`from_u128`] for
8//!   single-value conversion.
9//! - [`I128Array`] and [`U128Array`] — Python-visible fixed-size containers
10//!   backed by `Vec<i128>` / `Vec<u128>` respectively.
11
12use pyo3::prelude::*;
13use pyo3::types::PyAnyMethods;
14
15// ──────────────────────────────────────────────────────────────────────────────
16// Free-standing conversion helpers
17// ──────────────────────────────────────────────────────────────────────────────
18
19/// Convert a Python `int` to a Rust `i128`.
20///
21/// PyO3 supports extracting `i128` directly from Python's arbitrary-precision
22/// integer type; values outside `[i128::MIN, i128::MAX]` raise `OverflowError`.
23///
24/// # Errors
25/// Returns a [`PyErr`] if the Python object is not an integer or if it
26/// overflows `i128`.
27#[pyfunction]
28pub fn to_i128(val: &Bound<'_, PyAny>) -> PyResult<i128> {
29    val.extract::<i128>()
30}
31
32/// Convert a Rust `i128` to a Python `int`.
33///
34/// The resulting Python object is an ordinary `int` and can be used with any
35/// Python code that accepts integers.
36///
37/// # Errors
38/// Returns a [`PyErr`] on internal conversion failure (should not happen in
39/// practice).
40#[pyfunction]
41pub fn from_i128(py: Python<'_>, val: i128) -> PyResult<Py<PyAny>> {
42    Ok(val.into_pyobject(py)?.into_any().unbind())
43}
44
45/// Convert a Python `int` to a Rust `u128`.
46///
47/// Values outside `[0, u128::MAX]` raise `OverflowError`.
48///
49/// # Errors
50/// Returns a [`PyErr`] if the value is not a non-negative integer or if it
51/// overflows `u128`.
52#[pyfunction]
53pub fn to_u128(val: &Bound<'_, PyAny>) -> PyResult<u128> {
54    val.extract::<u128>()
55}
56
57/// Convert a Rust `u128` to a Python `int`.
58///
59/// # Errors
60/// Returns a [`PyErr`] on internal conversion failure (should not happen in
61/// practice).
62#[pyfunction]
63pub fn from_u128(py: Python<'_>, val: u128) -> PyResult<Py<PyAny>> {
64    Ok(val.into_pyobject(py)?.into_any().unbind())
65}
66
67// ──────────────────────────────────────────────────────────────────────────────
68// I128Array
69// ──────────────────────────────────────────────────────────────────────────────
70
71/// A Python-visible fixed-size array of `i128` values.
72///
73/// NumPy does not expose a native `int128` dtype, so this type provides a
74/// pure-Rust container that can be passed to and from Python via PyO3's
75/// arbitrary-precision integer support.
76#[pyclass(name = "I128Array")]
77pub struct I128Array {
78    /// Backing storage.
79    data: Vec<i128>,
80}
81
82#[pymethods]
83impl I128Array {
84    /// Construct a zero-initialised array of `size` elements.
85    #[new]
86    pub fn new(size: usize) -> Self {
87        Self {
88            data: vec![0i128; size],
89        }
90    }
91
92    /// Return the element at `idx`.
93    ///
94    /// # Errors
95    /// Returns `IndexError` if `idx >= self.len()`.
96    pub fn get(&self, idx: usize) -> PyResult<i128> {
97        self.data.get(idx).copied().ok_or_else(|| {
98            pyo3::exceptions::PyIndexError::new_err(format!(
99                "index {} out of bounds for I128Array of length {}",
100                idx,
101                self.data.len()
102            ))
103        })
104    }
105
106    /// Set the element at `idx` to `val`.
107    ///
108    /// # Errors
109    /// Returns `IndexError` if `idx >= self.len()`.
110    pub fn set(&mut self, idx: usize, val: i128) -> PyResult<()> {
111        let len = self.data.len();
112        self.data.get_mut(idx).map(|e| *e = val).ok_or_else(|| {
113            pyo3::exceptions::PyIndexError::new_err(format!(
114                "index {} out of bounds for I128Array of length {}",
115                idx, len
116            ))
117        })
118    }
119
120    /// Number of elements.
121    pub fn len(&self) -> usize {
122        self.data.len()
123    }
124
125    /// Return `true` if the array is empty.
126    pub fn is_empty(&self) -> bool {
127        self.data.is_empty()
128    }
129
130    /// Compute the sum of all elements.
131    pub fn sum(&self) -> i128 {
132        self.data.iter().sum()
133    }
134
135    /// Return all elements as a Python list of `int` values.
136    pub fn to_list(&self) -> Vec<i128> {
137        self.data.clone()
138    }
139
140    /// Fill the array from a Python list of integers.
141    ///
142    /// # Errors
143    /// Returns a `ValueError` if `values` has a different length than `self`,
144    /// or a `TypeError`/`OverflowError` if an element cannot be converted to
145    /// `i128`.
146    pub fn from_list(&mut self, values: Vec<i128>) -> PyResult<()> {
147        if values.len() != self.data.len() {
148            return Err(pyo3::exceptions::PyValueError::new_err(format!(
149                "expected {} elements, got {}",
150                self.data.len(),
151                values.len()
152            )));
153        }
154        self.data.copy_from_slice(&values);
155        Ok(())
156    }
157}
158
159// ──────────────────────────────────────────────────────────────────────────────
160// U128Array
161// ──────────────────────────────────────────────────────────────────────────────
162
163/// A Python-visible fixed-size array of `u128` values.
164///
165/// Mirrors [`I128Array`] but for unsigned 128-bit integers.
166#[pyclass(name = "U128Array")]
167pub struct U128Array {
168    /// Backing storage.
169    data: Vec<u128>,
170}
171
172#[pymethods]
173impl U128Array {
174    /// Construct a zero-initialised array of `size` elements.
175    #[new]
176    pub fn new(size: usize) -> Self {
177        Self {
178            data: vec![0u128; size],
179        }
180    }
181
182    /// Return the element at `idx`.
183    ///
184    /// # Errors
185    /// Returns `IndexError` if `idx >= self.len()`.
186    pub fn get(&self, idx: usize) -> PyResult<u128> {
187        self.data.get(idx).copied().ok_or_else(|| {
188            pyo3::exceptions::PyIndexError::new_err(format!(
189                "index {} out of bounds for U128Array of length {}",
190                idx,
191                self.data.len()
192            ))
193        })
194    }
195
196    /// Set the element at `idx` to `val`.
197    ///
198    /// # Errors
199    /// Returns `IndexError` if `idx >= self.len()`.
200    pub fn set(&mut self, idx: usize, val: u128) -> PyResult<()> {
201        let len = self.data.len();
202        self.data.get_mut(idx).map(|e| *e = val).ok_or_else(|| {
203            pyo3::exceptions::PyIndexError::new_err(format!(
204                "index {} out of bounds for U128Array of length {}",
205                idx, len
206            ))
207        })
208    }
209
210    /// Number of elements.
211    pub fn len(&self) -> usize {
212        self.data.len()
213    }
214
215    /// Return `true` if the array is empty.
216    pub fn is_empty(&self) -> bool {
217        self.data.is_empty()
218    }
219
220    /// Compute the sum of all elements.
221    pub fn sum(&self) -> u128 {
222        self.data.iter().sum()
223    }
224
225    /// Return all elements as a Python list of `int` values.
226    pub fn to_list(&self) -> Vec<u128> {
227        self.data.clone()
228    }
229
230    /// Fill the array from a Python list of integers.
231    ///
232    /// # Errors
233    /// Returns a `ValueError` if `values` has a different length than `self`,
234    /// or a `TypeError`/`OverflowError` if an element cannot be converted to
235    /// `u128`.
236    pub fn from_list(&mut self, values: Vec<u128>) -> PyResult<()> {
237        if values.len() != self.data.len() {
238            return Err(pyo3::exceptions::PyValueError::new_err(format!(
239                "expected {} elements, got {}",
240                self.data.len(),
241                values.len()
242            )));
243        }
244        self.data.copy_from_slice(&values);
245        Ok(())
246    }
247}
248
249// ──────────────────────────────────────────────────────────────────────────────
250// Module registration
251// ──────────────────────────────────────────────────────────────────────────────
252
253/// Register extended-integer types and conversion functions into a PyO3 module.
254///
255/// Exposes [`I128Array`], [`U128Array`], [`to_i128`], [`from_i128`],
256/// [`to_u128`], and [`from_u128`].
257pub fn register_extended_int_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
258    m.add_class::<I128Array>()?;
259    m.add_class::<U128Array>()?;
260    m.add_function(wrap_pyfunction!(to_i128, m)?)?;
261    m.add_function(wrap_pyfunction!(from_i128, m)?)?;
262    m.add_function(wrap_pyfunction!(to_u128, m)?)?;
263    m.add_function(wrap_pyfunction!(from_u128, m)?)?;
264    Ok(())
265}
266
267// ──────────────────────────────────────────────────────────────────────────────
268// Tests
269// ──────────────────────────────────────────────────────────────────────────────
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn i128_array_basic_operations() {
277        let mut arr = I128Array::new(3);
278        assert_eq!(arr.len(), 3);
279        assert_eq!(arr.sum(), 0);
280
281        arr.set(0, 100).expect("set index 0");
282        arr.set(1, 200).expect("set index 1");
283        arr.set(2, 300).expect("set index 2");
284
285        assert_eq!(arr.get(1).expect("get index 1"), 200);
286        assert_eq!(arr.sum(), 600);
287        assert_eq!(arr.to_list(), vec![100i128, 200, 300]);
288    }
289
290    #[test]
291    fn i128_array_out_of_bounds() {
292        let arr = I128Array::new(2);
293        assert!(arr.get(99).is_err());
294    }
295
296    #[test]
297    fn i128_to_from_roundtrip() {
298        Python::attach(|py| {
299            // i128::MAX round-trips through Python int.
300            let py_int = from_i128(py, i128::MAX).expect("from_i128 failed");
301            let val = to_i128(py_int.bind(py)).expect("to_i128 failed");
302            assert_eq!(val, i128::MAX);
303        });
304    }
305
306    #[test]
307    fn u128_roundtrip() {
308        Python::attach(|py| {
309            let py_int = from_u128(py, u128::MAX).expect("from_u128 failed");
310            let val = to_u128(py_int.bind(py)).expect("to_u128 failed");
311            assert_eq!(val, u128::MAX);
312        });
313    }
314
315    #[test]
316    fn u128_array_basic_operations() {
317        let mut arr = U128Array::new(2);
318        arr.set(0, u128::MAX).expect("set 0");
319        assert_eq!(arr.get(0).expect("get 0"), u128::MAX);
320        assert!(arr.get(5).is_err());
321    }
322}