leto 0.45.2

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

use leto::{
    domain::{RankMarker, RemoveAxis},
    Array, CowStorage, Layout, LetoError, Storage, VecStorage,
};

#[cfg(feature = "mnemosyne-alloc")]
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc,
};

#[cfg(feature = "mnemosyne-alloc")]
use leto::MnemosyneStorage;

#[test]
fn test_rank_marker_removes_axis_shape_and_strides() {
    let marker = RankMarker::<3>;

    assert_eq!(marker.remove_shape([2, 3, 4], 0).unwrap(), [3, 4]);
    assert_eq!(marker.remove_shape([2, 3, 4], 1).unwrap(), [2, 4]);
    assert_eq!(marker.remove_shape([2, 3, 4], 2).unwrap(), [2, 3]);

    assert_eq!(marker.remove_strides([12, 4, 1], 0).unwrap(), [4, 1]);
    assert_eq!(marker.remove_strides([12, 4, 1], 1).unwrap(), [12, 1]);
    assert_eq!(marker.remove_strides([12, 4, 1], 2).unwrap(), [12, 4]);
}

#[test]
fn test_rank_marker_rejects_out_of_bounds_axis() {
    let marker = RankMarker::<2>;

    assert!(matches!(
        marker.remove_shape([2, 3], 2),
        Err(LetoError::StorageError { .. })
    ));
    assert!(matches!(
        marker.remove_strides([3, 1], 2),
        Err(LetoError::StorageError { .. })
    ));
}

#[test]
fn test_cow_storage_borrows_without_copying_on_read() {
    let backing = [10, 11, 12, 13];
    let layout = Layout::c_contiguous([2, 2]).unwrap();
    let array = Array::new(layout, CowStorage::borrowed(&backing)).unwrap();

    assert!(array.storage().is_borrowed());
    assert!(std::ptr::eq(
        array.storage().as_borrowed().unwrap().as_ptr(),
        backing.as_ptr()
    ));
    assert!(array.storage().as_owned().is_none());
    assert!(std::ptr::eq(
        array.storage().as_slice().as_ptr(),
        backing.as_ptr()
    ));
    assert_eq!(*array.get([1, 1]).unwrap(), 13);
}

#[test]
fn test_cow_storage_detaches_on_mutation() {
    let backing = [1, 2, 3, 4];
    let layout = Layout::c_contiguous([2, 2]).unwrap();
    let mut array = Array::new(layout, CowStorage::borrowed(&backing)).unwrap();

    *array.get_mut([0, 1]).unwrap() = 20;

    assert!(array.storage().is_owned());
    assert!(array.storage().as_borrowed().is_none());
    assert_eq!(array.storage().as_owned().unwrap(), &[1, 20, 3, 4]);
    assert_ne!(array.storage().as_slice().as_ptr(), backing.as_ptr());
    assert_eq!(backing, [1, 2, 3, 4]);
    assert_eq!(array.storage().as_slice(), &[1, 20, 3, 4]);
}

#[test]
fn test_leto_parity_constructors_and_into_vec() {
    let zeros = Array::<i32, VecStorage<i32>, 2>::zeros([2, 2]);
    assert_eq!(zeros.storage().as_slice(), &[0, 0, 0, 0]);

    let filled = Array::<i32, VecStorage<i32>, 2>::from_elem([2, 2], 7);
    assert_eq!(filled.storage().as_slice(), &[7, 7, 7, 7]);

    let generated = Array::<i32, VecStorage<i32>, 2>::from_shape_fn([2, 3], |[row, col]| {
        (row as i32) * 10 + col as i32
    });
    assert_eq!(generated.storage().as_slice(), &[0, 1, 2, 10, 11, 12]);

    let from_vec =
        Array::<i32, VecStorage<i32>, 2>::from_shape_vec([2, 2], vec![1, 2, 3, 4]).unwrap();
    assert_eq!(from_vec.into_vec(), vec![1, 2, 3, 4]);

    assert!(matches!(
        Array::<i32, VecStorage<i32>, 2>::from_vec([2, 2], vec![1, 2, 3]),
        Err(LetoError::StorageError { .. })
    ));
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_array_constructor_parity_and_into_vec() {
    let zeros = Array::<i32, MnemosyneStorage<i32>, 2>::zeros_mnemosyne([2, 2]);
    assert_eq!(zeros.storage().as_slice(), &[0, 0, 0, 0]);

    let generated =
        Array::<i32, MnemosyneStorage<i32>, 2>::from_mnemosyne_shape_fn([2, 3], |[row, col]| {
            (row as i32) * 10 + col as i32
        });
    assert_eq!(generated.storage().as_slice(), &[0, 1, 2, 10, 11, 12]);

    let from_vec =
        Array::<i32, MnemosyneStorage<i32>, 2>::from_mnemosyne_shape_vec([2, 2], vec![1, 2, 3, 4])
            .unwrap();
    assert_eq!(from_vec.into_vec(), vec![1, 2, 3, 4]);

    assert!(matches!(
        Array::<i32, MnemosyneStorage<i32>, 2>::from_mnemosyne_vec([2, 2], vec![1, 2, 3]),
        Err(LetoError::StorageError { .. })
    ));
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_storage_moves_non_clone_values() {
    #[derive(Debug, PartialEq, Eq)]
    struct Payload(usize);

    let storage = MnemosyneStorage::from_vec(vec![Payload(1), Payload(2), Payload(3)]);
    assert_eq!(storage.as_slice(), &[Payload(1), Payload(2), Payload(3)]);

    let values = storage.into_vec();
    assert_eq!(values, vec![Payload(1), Payload(2), Payload(3)]);
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_storage_zst_drop() {
    static DROP_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);

    struct ZstDrop;
    impl Drop for ZstDrop {
        fn drop(&mut self) {
            DROP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        }
    }

    DROP_COUNT.store(0, std::sync::atomic::Ordering::SeqCst);
    {
        let vec = vec![ZstDrop, ZstDrop, ZstDrop];
        let _storage = MnemosyneStorage::from_vec(vec);
    }
    assert_eq!(DROP_COUNT.load(std::sync::atomic::Ordering::SeqCst), 3);
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_shape_generator_drops_non_copy_values_once() {
    struct Payload {
        index: usize,
        drops: Arc<AtomicUsize>,
    }

    impl Drop for Payload {
        fn drop(&mut self) {
            self.drops.fetch_add(1, Ordering::SeqCst);
        }
    }

    let drops = Arc::new(AtomicUsize::new(0));
    {
        let array = Array::<Payload, MnemosyneStorage<Payload>, 2>::from_mnemosyne_shape_fn(
            [2, 2],
            |[row, column]| Payload {
                index: row * 2 + column,
                drops: Arc::clone(&drops),
            },
        );
        assert_eq!(
            array
                .storage()
                .as_slice()
                .iter()
                .map(|value| value.index)
                .collect::<Vec<_>>(),
            vec![0, 1, 2, 3]
        );
        assert_eq!(drops.load(Ordering::SeqCst), 0);
    }
    assert_eq!(drops.load(Ordering::SeqCst), 4);
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_shape_generator_cleans_initialized_prefix_on_panic() {
    struct Payload(Arc<AtomicUsize>);

    impl Drop for Payload {
        fn drop(&mut self) {
            self.0.fetch_add(1, Ordering::SeqCst);
        }
    }

    let calls = Arc::new(AtomicUsize::new(0));
    let drops = Arc::new(AtomicUsize::new(0));
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe({
        let calls = Arc::clone(&calls);
        let drops = Arc::clone(&drops);
        move || {
            let _ = Array::<Payload, MnemosyneStorage<Payload>, 1>::from_mnemosyne_shape_fn(
                [4],
                |_| {
                    let call = calls.fetch_add(1, Ordering::SeqCst);
                    if call == 2 {
                        panic!("intentional generator panic");
                    }
                    Payload(Arc::clone(&drops))
                },
            );
        }
    }));

    let Err(payload) = result else {
        panic!("the third generator call must panic");
    };
    assert_eq!(
        payload.downcast_ref::<&str>().copied(),
        Some("intentional generator panic")
    );
    assert_eq!(calls.load(Ordering::SeqCst), 3);
    assert_eq!(drops.load(Ordering::SeqCst), 2);
}

#[cfg(feature = "mnemosyne-alloc")]
#[test]
fn test_mnemosyne_empty_shape_does_not_call_generator() {
    let calls = AtomicUsize::new(0);
    let array = Array::<usize, MnemosyneStorage<usize>, 2>::from_mnemosyne_shape_fn([0, 4], |_| {
        calls.fetch_add(1, Ordering::SeqCst)
    });

    assert_eq!(array.shape(), [0, 4]);
    assert!(array.storage().as_slice().is_empty());
    assert_eq!(calls.load(Ordering::SeqCst), 0);
}

#[test]
fn test_slice_step_isize_min() {
    let layout = Layout::c_contiguous([5]).unwrap();
    let ranges = [(0, 5, isize::MIN)];
    let res = layout.slice(&ranges);
    let error = res.expect_err("a step of isize::MIN must be rejected");
    assert_eq!(
        error.to_string(),
        "Incompatible slice: range (0, 5), shape [5]"
    );
}