leto 0.46.0

N-dimensional strided arrays, zero-copy views, and storage backends
Documentation
#![expect(
    clippy::unwrap_used,
    reason = "test scope: failed precondition = test failure"
)]

use leto::{Array, Layout, LetoError, SliceArg, VecStorage};
use proptest::prelude::*;

proptest! {
    #[test]
    fn c_contiguous_offsets_match_row_major_formula(
        rows in 1usize..8,
        cols in 1usize..8,
        depth in 1usize..8,
        row in 0usize..8,
        col in 0usize..8,
        lane in 0usize..8,
    ) {
        let row = row % rows;
        let col = col % cols;
        let lane = lane % depth;
        let layout = Layout::c_contiguous([rows, cols, depth]).unwrap();

        prop_assert_eq!(
            layout.offset_of([row, col, lane]).unwrap(),
            (row * cols * depth) + (col * depth) + lane
        );
        prop_assert_eq!(layout.checked_size().unwrap(), rows * cols * depth);
        prop_assert!(layout.validate_storage_len(rows * cols * depth).is_ok());
    }

    #[test]
    fn f_contiguous_offsets_match_column_major_formula(
        rows in 1usize..8,
        cols in 1usize..8,
        depth in 1usize..8,
        row in 0usize..8,
        col in 0usize..8,
        lane in 0usize..8,
    ) {
        let row = row % rows;
        let col = col % cols;
        let lane = lane % depth;
        let layout = Layout::f_contiguous([rows, cols, depth]).unwrap();

        prop_assert_eq!(
            layout.offset_of([row, col, lane]).unwrap(),
            row + (col * rows) + (lane * rows * cols)
        );
        prop_assert_eq!(layout.checked_size().unwrap(), rows * cols * depth);
        prop_assert!(layout.validate_storage_len(rows * cols * depth).is_ok());
    }

    #[test]
    fn transposed_views_preserve_physical_values(
        rows in 1usize..7,
        cols in 1usize..7,
        depth in 1usize..7,
        a in 0usize..7,
        b in 0usize..7,
        c in 0usize..7,
    ) {
        let a = a % depth;
        let b = b % rows;
        let c = c % cols;
        let len = rows * cols * depth;
        let array = Array::from_shape_vec([rows, cols, depth], (0..len).collect::<Vec<_>>()).unwrap();

        let transposed = array.transpose([2, 0, 1]).unwrap();

        prop_assert_eq!(transposed.shape(), [depth, rows, cols]);
        prop_assert_eq!(
            *transposed.get([a, b, c]).unwrap(),
            *array.get([b, c, a]).unwrap()
        );
    }

    #[test]
    fn reverse_slices_preserve_leto_style_value_order(
        rows in 1usize..8,
        cols in 1usize..8,
        row in 0usize..8,
        col in 0usize..8,
    ) {
        let row = row % rows;
        let col = col % cols;
        let len = rows * cols;
        let array = Array::from_shape_vec([rows, cols], (0..len).collect::<Vec<_>>()).unwrap();

        let view = array
            .slice_with::<2>(&[
                SliceArg::range(Some(-1), None, -1),
                SliceArg::All,
            ])
            .unwrap();

        prop_assert_eq!(view.shape(), [rows, cols]);
        prop_assert_eq!(
            *view.get([row, col]).unwrap(),
            *array.get([rows - 1 - row, col]).unwrap()
        );
    }

    #[test]
    fn broadcasted_singleton_axis_preserves_source_values(
        rows in 0usize..8,
        cols in 1usize..8,
        row in 0usize..8,
        col in 0usize..8,
    ) {
        let col = col % cols;
        let source = Array::from_shape_vec([1, cols], (0..cols).collect::<Vec<_>>()).unwrap();
        let broadcast = source.broadcast([rows, cols]).unwrap();

        prop_assert_eq!(broadcast.shape(), [rows, cols]);
        let expected_row_stride = if rows == 1 { cols as isize } else { 0 };
        prop_assert_eq!(broadcast.strides(), [expected_row_stride, 1]);

        if rows > 0 {
            let row = row % rows;
            prop_assert_eq!(*broadcast.get([row, col]).unwrap(), *source.get([0, col]).unwrap());
        }
    }

    #[test]
    fn negative_stride_layout_storage_span_is_checked(len in 1usize..32) {
        let valid = Layout::try_new([len], [-1], len - 1).unwrap();

        prop_assert_eq!(valid.checked_min_max_offsets().unwrap(), (0, len - 1));
        prop_assert!(valid.validate_storage_len(len).is_ok());

        if len > 1 {
            // Walking [0..len) at stride -1 from base 0 addresses offset
            // -(len - 1). The rejection now happens at construction rather
            // than in the downstream offset query.
            let rejects_negative_span = matches!(
                Layout::try_new([len], [-1], 0),
                Err(LetoError::StorageError { .. })
            );
            prop_assert!(rejects_negative_span);
        }
    }

    #[test]
    fn empty_axis_layouts_have_zero_size_without_storage_access(
        rows in 0usize..8,
        depth in 0usize..8,
    ) {
        let layout = Layout::c_contiguous([rows, 0, depth]).unwrap();
        let array = Array::<usize, _, 3>::new(layout, VecStorage::new(Vec::new())).unwrap();

        prop_assert_eq!(layout.checked_size().unwrap(), 0);
        prop_assert_eq!(layout.checked_min_max_offsets().unwrap(), (0, 0));
        prop_assert!(layout.validate_storage_len(0).is_ok());
        prop_assert_eq!(array.size(), 0);
        prop_assert_eq!(array.view().as_slice().unwrap(), &[]);
    }

    #[test]
    fn composed_slices_preserve_original_values(
        rows in 2usize..10,
        cols in 2usize..10,
        row_start in 0usize..10,
        row_take in 1usize..10,
        col_start in 0usize..10,
        col_take in 1usize..10,
        out_row in 0usize..10,
        out_col in 0usize..10,
    ) {
        let row_start = row_start % rows;
        let row_take = row_take.min(rows - row_start);
        let row_mid = row_start + row_take;
        let col_start = col_start % cols;
        let col_take = col_take.min(cols - col_start);
        let col_mid = col_start + col_take;
        let len = rows * cols;
        let array = Array::from_shape_vec([rows, cols], (0..len).collect::<Vec<_>>()).unwrap();

        let row_slice = array
            .slice_with::<2>(&[
                SliceArg::range(Some(row_start as isize), Some(row_mid as isize), 1),
                SliceArg::All,
            ])
            .unwrap();
        let composed = row_slice
            .slice_with::<2>(&[
                SliceArg::All,
                SliceArg::range(Some(col_start as isize), Some(col_mid as isize), 1),
            ])
            .unwrap();

        let out_row = out_row % row_take;
        let out_col = out_col % col_take;

        prop_assert_eq!(composed.shape(), [row_take, col_take]);
        prop_assert_eq!(
            *composed.get([out_row, out_col]).unwrap(),
            *array.get([row_start + out_row, col_start + out_col]).unwrap()
        );
    }
}