lightweight-simd 0.1.1

Lightweight generic fixed-array vector types relying on compiler auto-vectorization
Documentation
// Most or all contents generated by AI (model glm-5.3)

//! Lightweight generic fixed-array vector types.
//!
//! This crate provides [`Simd`], a fixed-lane vector type `Simd<T, N>` over
//! `[T; N]`, with lane-wise arithmetic (vector/vector and vector/scalar),
//! multiply-add (fused where the target has hardware FMA; see below), float
//! functions, comparisons producing masks, mask selection, horizontal
//! reductions, and slice load/store — all implemented without intrinsics and
//! without runtime instruction dispatch.
//!
//! Performance relies on the compiler auto-vectorizing this portable code;
//! build with `-C target-cpu=native` (or a similar `target-cpu` setting) to
//! obtain SIMD-level efficiency. Correctness does not depend on it.
//!
//! Element categories are expressed by trait bounds rather than by distinct
//! types: arithmetic requires [`num_traits::Num`], the float functions require
//! [`num_traits::Float`], multiply-add requires [`num_traits::MulAdd`] where
//! the target has hardware FMA and [`core::ops::Mul`] with [`core::ops::Add`]
//! otherwise, and bitwise operations require the element's own
//! [`core::ops`] implementations. `bool` is a valid element, and
//! `Simd<bool, N>` doubles as the mask type. The lane count `N` is an
//! arbitrary const generic.
//!
//! A full one-file map of the API surface — written for users and AI coding
//! agents — lives in the [`guide`] module (kept as `src/guide.md` in the
//! repository).
//!
//! Concrete aliases such as `f64x8` are enforced-alignment types
//! ([`Aligned64<f64, 8>`](Aligned64)): array-backed siblings of [`Simd`] that
//! share one implementation — every operation is defined once for `Simd` and
//! the five aligned types together. The alignment is the vector's byte size
//! rounded up to a power of two, capped at 64 bytes; conversions between plain
//! and aligned vectors are `From`/`.0`/`to_simd`. Masks (`Simd<bool, N>` and
//! the `maskN` aliases) carry no enforced alignment; per-lane selection is
//! [`mask_select`](Simd::mask_select) on the value (available on every vector
//! type), or [`Simd::select`] on the mask for plain vectors.
//!
//! # Multiply-add semantics
//!
//! [`mul_add`](Simd::mul_add) and [`fma_from`](Simd::fma_from) follow the
//! compilation target (see `docs/adr/0005` in the repository for the
//! measured trade-off):
//!
//! - **targets with hardware FMA** — x86 built with `-C target-cpu` of `x86-64-v3` or higher
//!   (including `native`), and all aarch64: the element's fused [`num_traits::MulAdd`], a single
//!   rounding per lane, one FMA instruction per register, no extra flags.
//! - **targets without** — generic x86-64, `x86-64-v2`, and architectures the predicate does not
//!   know: a separated multiply and add (two roundings per lane) that never degrades into the slow
//!   software-fused libm `fma` calls (~4.6x slower than separated there). Under `-C
//!   llvm-args=-fp-contract=fast` this form still fuses into a hardware FMA instruction on capable
//!   targets.
//! - the non-default cargo feature **`use_libm_fma`** pins the fused form on *every* target,
//!   accepting the libm software `fma` where hardware is absent — use it when results must be
//!   bit-identical across build targets.
//!
//! # Example
//!
//! Same example as the README; kept as a doctest so it is verified by CI.
//!
//! ```
//! use lightweight_simd::{f64x8, Simd};
//!
//! // typed aliases (enforced alignment) or the generic Simd<T, N>
//! let a = f64x8::from([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
//! let b = f64x8::splat(2.0);
//!
//! let c = a.mul_add(b, f64x8::one()); // a*b + 1, fused on FMA targets
//! assert_eq!(c.reduce_sum(), 80.0);
//!
//! let mask = a.simd_gt(f64x8::splat(4.0)); // comparisons -> masks
//! assert_eq!(a.mask_select(mask, b).to_array(), [2.0, 2.0, 2.0, 2.0, 5.0, 6.0, 7.0, 8.0]);
//!
//! let mut buf = [0.0; 8];
//! a.store_slice(&mut buf); // slice load/store
//!
//! // any element type and lane count
//! let u = Simd::from([10u32, 20, 30]);
//! assert_eq!((u * 3).to_array(), [30, 60, 90]);
//! ```

#![warn(missing_docs)]
#![deny(unsafe_op_in_unsafe_fn)]

use core::ops::{Index, IndexMut};
use num_traits::{Num, One, Zero};

mod alias;
pub mod aligned;
mod memory;
mod ops;
mod reduce;

/// Dummy doc-only module: renders `src/guide.md` (the one-file API overview,
/// also linked from the README) into the rustdoc output.
#[cfg(doc)]
pub mod guide {
    #![doc = include_str!("guide.md")]
}

pub use alias::*;
pub use aligned::{Aligned4, Aligned8, Aligned16, Aligned32, Aligned64};

/// A fixed-length vector of `N` lanes of element type `T`.
///
/// All operations are lane-wise and implemented as plain loops over the
/// underlying `[T; N]`, so the compiler can auto-vectorize them for any
/// element type and lane count. The lane count `N` is arbitrary (powers of two
/// that fill whole SIMD registers are the sweet spot, but not required).
///
/// The type carries no enforced alignment beyond the element's natural
/// alignment; for enforced alignment use the [`aligned`] sibling types, which
/// share this type's full operation surface. The field is public for
/// zero-friction interoperability with array code.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)]
pub struct Simd<T, const N: usize>(pub [T; N]);

#[duplicate::duplicate_item(
    _mod_        _vec_;
    [simd_impls] [Simd];
    [impls_a4]   [Aligned4];
    [impls_a8]   [Aligned8];
    [impls_a16]  [Aligned16];
    [impls_a32]  [Aligned32];
    [impls_a64]  [Aligned64];
)]
mod _mod_ {
    use super::*;

    /* #region constructors and access */

    impl<T, const N: usize> _vec_<T, N> {
        /// Returns a vector whose lane `i` is `f(i)`.
        #[inline(always)]
        pub fn from_fn<F>(f: F) -> Self
        where
            F: FnMut(usize) -> T,
        {
            Self(core::array::from_fn(f))
        }

        /// Applies `f` to each lane, returning a new vector.
        ///
        /// This is the generic escape hatch for operations the crate does not
        /// provide (e.g. transcendental functions: `v.map(f64::exp)`).
        #[inline(always)]
        pub fn map<F>(self, f: F) -> Self
        where
            F: FnMut(T) -> T,
        {
            Self(self.0.map(f))
        }

        /// Returns the underlying array by reference.
        #[inline(always)]
        pub const fn as_array(&self) -> &[T; N] {
            &self.0
        }

        /// Returns the underlying array by mutable reference.
        #[inline(always)]
        pub fn as_array_mut(&mut self) -> &mut [T; N] {
            &mut self.0
        }

        /// Returns the underlying array by value.
        #[inline(always)]
        pub fn to_array(self) -> [T; N] {
            self.0
        }
    }

    impl<T: Copy, const N: usize> _vec_<T, N> {
        /// Returns a vector with all lanes set to `val`.
        #[inline(always)]
        pub const fn splat(val: T) -> Self {
            Self([val; N])
        }

        /// Sets all lanes to `val`.
        #[inline(always)]
        pub fn fill(&mut self, val: T) {
            self.0 = [val; N];
        }

        /// Per-lane select: keeps lanes of `self` where `mask` is `true` and
        /// lanes of `other` where it is `false`. This value-side form is
        /// available on every vector type; the mask-side
        /// [`select`](Simd::select) applies to plain vectors.
        #[inline(always)]
        pub fn mask_select(self, mask: Simd<bool, N>, other: Self) -> Self {
            let mut out = self.0;
            for ((o, f), m) in out.iter_mut().zip(other.0).zip(mask.0) {
                if !m {
                    *o = f;
                }
            }
            Self(out)
        }

        /// Returns an uninitialized vector, intended for scratch buffers whose
        /// lanes are fully written before being read.
        ///
        /// # Safety
        ///
        /// The caller must initialize all lanes before any read. The element
        /// type must additionally admit arbitrary bit patterns (true for
        /// floats and integers; not for types with niches such as references).
        #[inline(always)]
        #[allow(clippy::uninit_assumed_init)]
        pub unsafe fn uninit() -> Self {
            // SAFETY: the caller contract requires full initialization before
            // any read of the returned value.
            unsafe { core::mem::MaybeUninit::uninit().assume_init() }
        }
    }

    impl<T: Zero + Copy, const N: usize> _vec_<T, N> {
        /// Returns a vector with all lanes set to zero.
        #[inline(always)]
        pub fn zero() -> Self {
            Self::splat(T::zero())
        }
    }

    impl<T: One + Copy, const N: usize> _vec_<T, N> {
        /// Returns a vector with all lanes set to one.
        #[inline(always)]
        pub fn one() -> Self {
            Self::splat(T::one())
        }
    }

    /* #endregion */

    /* #region common traits */

    impl<T: Default, const N: usize> Default for _vec_<T, N> {
        #[inline(always)]
        fn default() -> Self {
            Self(core::array::from_fn(|_| T::default()))
        }
    }

    impl<T, const N: usize> From<[T; N]> for _vec_<T, N> {
        #[inline(always)]
        fn from(array: [T; N]) -> Self {
            Self(array)
        }
    }

    impl<T, const N: usize> Index<usize> for _vec_<T, N> {
        type Output = T;

        #[inline(always)]
        fn index(&self, index: usize) -> &Self::Output {
            &self.0[index]
        }
    }

    impl<T, const N: usize> IndexMut<usize> for _vec_<T, N> {
        #[inline(always)]
        fn index_mut(&mut self, index: usize) -> &mut Self::Output {
            &mut self.0[index]
        }
    }

    impl<T: Num + Copy, const N: usize> Zero for _vec_<T, N> {
        #[inline(always)]
        fn zero() -> Self {
            Self::splat(T::zero())
        }

        #[inline(always)]
        fn is_zero(&self) -> bool {
            self.0.iter().all(Zero::is_zero)
        }
    }

    impl<T: Num + Copy, const N: usize> One for _vec_<T, N> {
        #[inline(always)]
        fn one() -> Self {
            Self::splat(T::one())
        }
    }

    /* #endregion */
}