Skip to main content

scirs2_core/memory_efficient/
views.rs

1use super::validation;
2use crate::error::{CoreError, ErrorContext, ErrorLocation};
3use ::ndarray::{
4    Array, ArrayBase, ArrayView as NdArrayView, ArrayViewMut as NdArrayViewMut, Data, Dimension,
5    Ix1, Ix2, RawDataMut,
6};
7
8/// A type alias for ndarray's ArrayView with additional functionality
9pub type ArrayView<'a, A, D> = NdArrayView<'a, A, D>;
10
11/// A type alias for ndarray's ArrayViewMut with additional functionality
12pub type ViewMut<'a, A, D> = NdArrayViewMut<'a, A, D>;
13
14/// Create a view of an array with a different element type
15///
16/// This function creates a view of the given array interpreting its elements
17/// as a different type. This is useful for viewing binary data as different
18/// types without copying.
19///
20/// # Safety
21///
22/// This function is unsafe because it does not check that the memory layout
23/// of the source type is compatible with the destination type.
24///
25/// # Arguments
26///
27/// * `array` - The array to view
28///
29/// # Returns
30///
31/// A view of the array with elements interpreted as the new type
32pub unsafe fn view_as<A, B, S, D>(array: &ArrayBase<S, D>) -> Result<ArrayView<'_, B, D>, CoreError>
33where
34    A: Clone,
35    S: Data<Elem = A>,
36    D: Dimension,
37{
38    validation::check_not_empty(array)?;
39
40    // Calculate new shape based on type sizes
41    let a_size = std::mem::size_of::<A>();
42    let b_size = std::mem::size_of::<B>();
43
44    if a_size == 0 || b_size == 0 {
45        return Err(CoreError::ValidationError(
46            ErrorContext::new("Cannot reinterpret view of zero-sized type".to_string())
47                .with_location(ErrorLocation::new(file!(), line!())),
48        ));
49    }
50
51    if a_size != b_size {
52        // Reinterpreting between differently-sized element types requires a shape change
53        // that cannot be expressed within the generic `D` dimension parameter; use a
54        // 1-D-specific helper or reshape the array to Ix1 before calling view_as.
55        return Err(CoreError::NotImplementedError(
56            ErrorContext::new(format!(
57                "view_as with differing element sizes ({a_size} vs {b_size}) requires a shape \
58                 change not representable in the generic dimension D; \
59                 reshape to Ix1 first or use a dedicated 1-D helper"
60            ))
61            .with_location(ErrorLocation::new(file!(), line!())),
62        ));
63    }
64
65    // Alignment check: the data pointer must be aligned for B.
66    let ptr = array.as_ptr() as *const B;
67    if (ptr as usize) % std::mem::align_of::<B>() != 0 {
68        return Err(CoreError::ValidationError(
69            ErrorContext::new(format!(
70                "Data pointer is not aligned for the target type (required alignment: {})",
71                std::mem::align_of::<B>()
72            ))
73            .with_location(ErrorLocation::new(file!(), line!())),
74        ));
75    }
76
77    // SAFETY: We have verified:
78    //   1. The array is non-empty (checked by check_not_empty above).
79    //   2. Both element types have the same size, so the stride values remain valid
80    //      for B (ndarray's cast asserts size equality internally as well).
81    //   3. The pointer is properly aligned for B (alignment check above).
82    //   4. The caller, by using this `unsafe fn`, guarantees that the byte
83    //      representation of every A element is a valid B value.
84    //   5. The lifetime 'a of the returned view is tied to the lifetime of
85    //      `array`, so no use-after-free is possible.
86    let raw_view = array.raw_view().cast::<B>();
87    Ok(raw_view.deref_into_view())
88}
89
90/// Create a mutable view of an array with a different element type
91///
92/// # Safety
93///
94/// This function is unsafe because it does not check that the memory layout
95/// of the source type is compatible with the destination type.
96///
97/// # Arguments
98///
99/// * `array` - The array to view
100///
101/// # Returns
102///
103/// A mutable view of the array with elements interpreted as the new type
104pub unsafe fn view_mut_as<A, B, S, D>(
105    array: &mut ArrayBase<S, D>,
106) -> Result<ViewMut<'_, B, D>, CoreError>
107where
108    A: Clone,
109    S: Data<Elem = A> + RawDataMut,
110    D: Dimension,
111{
112    validation::check_not_empty(array)?;
113
114    // Calculate new shape based on type sizes
115    let a_size = std::mem::size_of::<A>();
116    let b_size = std::mem::size_of::<B>();
117
118    if a_size == 0 || b_size == 0 {
119        return Err(CoreError::ValidationError(
120            ErrorContext::new("Cannot reinterpret view of zero-sized type".to_string())
121                .with_location(ErrorLocation::new(file!(), line!())),
122        ));
123    }
124
125    if a_size != b_size {
126        // Reinterpreting between differently-sized element types requires a shape change
127        // that cannot be expressed within the generic `D` dimension parameter; use a
128        // 1-D-specific helper or reshape the array to Ix1 before calling view_mut_as.
129        return Err(CoreError::NotImplementedError(
130            ErrorContext::new(format!(
131                "view_mut_as with differing element sizes ({a_size} vs {b_size}) requires a \
132                 shape change not representable in the generic dimension D; \
133                 reshape to Ix1 first or use a dedicated 1-D helper"
134            ))
135            .with_location(ErrorLocation::new(file!(), line!())),
136        ));
137    }
138
139    // Alignment check: the data pointer must be aligned for B.
140    let ptr = array.as_ptr() as *const B;
141    if (ptr as usize) % std::mem::align_of::<B>() != 0 {
142        return Err(CoreError::ValidationError(
143            ErrorContext::new(format!(
144                "Data pointer is not aligned for the target type (required alignment: {})",
145                std::mem::align_of::<B>()
146            ))
147            .with_location(ErrorLocation::new(file!(), line!())),
148        ));
149    }
150
151    // SAFETY: We have verified:
152    //   1. The array is non-empty (checked by check_not_empty above).
153    //   2. Both element types have the same size, so strides remain valid for B.
154    //   3. The pointer is properly aligned for B (alignment check above).
155    //   4. The caller, by using this `unsafe fn`, guarantees that the byte
156    //      representation of every A element is a valid B value, and that
157    //      writing valid B values leaves valid A byte patterns (since sizes
158    //      are equal, the allocation remains properly sized).
159    //   5. The lifetime 'a of the returned view is tied to the mutable borrow
160    //      of `array`, enforcing exclusive access for the view's lifetime.
161    let raw_view_mut = array.raw_view_mut().cast::<B>();
162    Ok(raw_view_mut.deref_into_view_mut())
163}
164
165/// Create a transposed copy of a 2D array
166///
167/// # Arguments
168///
169/// * `array` - The array to transpose
170///
171/// # Returns
172///
173/// A transposed copy of the array
174#[allow(dead_code)]
175pub fn transpose_view<A, S>(array: &ArrayBase<S, Ix2>) -> Result<Array<A, Ix2>, CoreError>
176where
177    A: Clone,
178    S: Data<Elem = A>,
179{
180    validation::check_not_empty(array)?;
181
182    // Create a transposed owned copy
183    Ok(array.to_owned().t().to_owned())
184}
185
186/// Create a copy of the diagonal of a 2D array
187///
188/// # Arguments
189///
190/// * `array` - The array to view
191///
192/// # Returns
193///
194/// A copy of the diagonal of the array
195#[allow(dead_code)]
196pub fn diagonal_view<A, S>(array: &ArrayBase<S, Ix2>) -> Result<Array<A, Ix1>, CoreError>
197where
198    A: Clone,
199    S: Data<Elem = A>,
200{
201    validation::check_not_empty(array)?;
202    validation::check_square(array)?;
203
204    // Create a diagonal copy
205    Ok(array.diag().to_owned())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use ndarray::array;
212
213    /// `f32` and `u32` share the same size (4 bytes) and same alignment (4 bytes).
214    /// Viewing an f32 array as u32 reads the raw IEEE-754 bit pattern.
215    #[test]
216    fn test_view_as_same_size_f32_as_u32() {
217        // Positive zero: bit pattern 0x0000_0000
218        let arr = array![0.0_f32, 1.0_f32];
219        // SAFETY: u32 is valid for any 4-byte bit pattern; f32 values we use are
220        // well-defined IEEE-754 values, so reading them as u32 is well-defined.
221        let view = unsafe { view_as::<f32, u32, _, _>(&arr) };
222        assert!(view.is_ok(), "view_as should succeed for same-size types");
223        let view = view.expect("view_as returned Err unexpectedly");
224        // 0.0f32 → 0x0000_0000, 1.0f32 → 0x3F80_0000
225        assert_eq!(view[0], 0x0000_0000_u32);
226        assert_eq!(view[1], 0x3F80_0000_u32);
227    }
228
229    /// `u32` and `i32` share the same size and alignment; reinterpreting should
230    /// succeed and expose the two's-complement representation.
231    #[test]
232    fn test_view_as_u32_as_i32_round_trip() {
233        let arr = array![0_u32, 0xFFFF_FFFF_u32];
234        // SAFETY: every u32 bit pattern is a valid i32 (two's complement).
235        let view = unsafe { view_as::<u32, i32, _, _>(&arr) };
236        assert!(view.is_ok());
237        let view = view.expect("view_as returned Err unexpectedly");
238        assert_eq!(view[0], 0_i32);
239        assert_eq!(view[1], -1_i32);
240    }
241
242    /// Attempting to view a `u8` array as `u32` (different sizes: 1 vs 4) must
243    /// return a `NotImplementedError` because the shape would need to change.
244    #[test]
245    fn test_view_as_different_sizes_returns_error() {
246        let arr = array![0_u8, 1_u8, 2_u8, 3_u8];
247        // SAFETY: irrelevant — we expect an Err before any unsafe memory access.
248        let result = unsafe { view_as::<u8, u32, _, _>(&arr) };
249        assert!(
250            result.is_err(),
251            "view_as with different element sizes must fail"
252        );
253        match result.expect_err("expected an error") {
254            CoreError::NotImplementedError(_) => {}
255            other => panic!("Expected NotImplementedError, got: {other:?}"),
256        }
257    }
258
259    /// `view_mut_as` should succeed for same-size types, and mutations through
260    /// the reinterpreted view must be visible in the original array.
261    #[test]
262    fn test_view_mut_as_same_size_mutates_original() {
263        let mut arr = array![0_u32, 1_u32];
264        {
265            // SAFETY: i32 is valid for any u32 bit pattern we set below.
266            let mut view = unsafe { view_mut_as::<u32, i32, _, _>(&mut arr) }
267                .expect("view_mut_as returned Err unexpectedly");
268            view[0] = -1_i32; // sets bit pattern 0xFFFF_FFFF
269        }
270        // After the view is dropped, inspect via u32 lens
271        assert_eq!(arr[0], 0xFFFF_FFFF_u32);
272        assert_eq!(arr[1], 1_u32); // unchanged
273    }
274
275    /// Attempting to view an empty array must return a `ValidationError`.
276    #[test]
277    fn test_view_as_empty_array_returns_error() {
278        let arr = ndarray::Array1::<f32>::zeros(0);
279        // SAFETY: no memory is accessed; we expect an Err for the empty check.
280        let result = unsafe { view_as::<f32, u32, _, _>(&arr) };
281        assert!(result.is_err(), "view_as on empty array must fail");
282        match result.expect_err("expected an error") {
283            CoreError::ValidationError(_) => {}
284            other => panic!("Expected ValidationError, got: {other:?}"),
285        }
286    }
287}