lightweight-simd 0.1.0

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

//! Correctness tests for bitwise/shift operators, fused multiply-add, float
//! functions, comparisons/masks, and reductions.

#[cfg(not(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma")))]
use core::ops::{Add, Mul};
use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign};
#[cfg(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma"))]
use num_traits::MulAdd;
use num_traits::{Float, Num};

use lightweight_simd::{Simd, f64x4, f64x8, mask8};

fn check_bitwise<T, const N: usize>(mk: &impl Fn(usize) -> T)
where
    T: BitAnd<Output = T>
        + BitOr<Output = T>
        + BitXor<Output = T>
        + BitAndAssign
        + BitOrAssign
        + BitXorAssign
        + Copy
        + PartialEq
        + std::fmt::Debug,
{
    let a = Simd::<T, N>::from_fn(mk);
    let b = Simd::<T, N>::from_fn(|i| mk(i + 1));
    assert_eq!((a & b).to_array(), core::array::from_fn(|i| mk(i) & mk(i + 1)));
    assert_eq!((a | b).to_array(), core::array::from_fn(|i| mk(i) | mk(i + 1)));
    assert_eq!((a ^ b).to_array(), core::array::from_fn(|i| mk(i) ^ mk(i + 1)));
    let mut c = a;
    c &= b;
    assert_eq!(c, a & b);
    c |= b;
    assert_eq!(c.to_array(), core::array::from_fn(|i| (mk(i) & mk(i + 1)) | mk(i + 1)));
    c ^= b;
    assert_eq!(c.to_array(), core::array::from_fn(|i| ((mk(i) & mk(i + 1)) | mk(i + 1)) ^ mk(i + 1)));
}

#[test]
fn bitwise_ints_and_bool() {
    check_bitwise::<i64, 8>(&|i| i as i64 * 5 + 1);
    check_bitwise::<i64, 3>(&|i| i as i64 * 5 + 1);
    check_bitwise::<i32, 5>(&|i| i as i32 + 12345);
    check_bitwise::<bool, 8>(&|i| i % 3 == 0);
}

#[test]
fn shifts_i64() {
    let v = Simd::<i64, 4>::from([1, 2, 3, 4]);
    assert_eq!((v << 2).to_array(), [4, 8, 12, 16]);
    assert_eq!(((v << 3) >> 1).to_array(), [4, 8, 12, 16]);
}

/// Reference and crate method must agree under whichever evaluation mode the
/// crate was built in. The predicate mirrors src/ops.rs: fused `MulAdd` when
/// the target has hardware FMA (or the `use_libm_fma` feature pins it on),
/// separated `a * b + c` otherwise.
#[cfg(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma"))]
fn check_fused<T, const N: usize>(a_mk: &impl Fn(usize) -> T, b_mk: &impl Fn(usize) -> T)
where
    T: MulAdd<Output = T> + Copy + PartialEq + std::fmt::Debug,
{
    let a = Simd::<T, N>::from_fn(a_mk);
    let b = Simd::<T, N>::from_fn(b_mk);
    let c = Simd::<T, N>::from_fn(|i| a_mk(i + 7));
    assert_eq!(a.mul_add(b, c).to_array(), core::array::from_fn(|i| MulAdd::mul_add(a_mk(i), b_mk(i), a_mk(i + 7))));
    let mut acc = c;
    acc.fma_from(b, a);
    assert_eq!(acc.to_array(), core::array::from_fn(|i| MulAdd::mul_add(b_mk(i), a_mk(i), a_mk(i + 7))));
}

#[cfg(not(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma")))]
fn check_fused<T, const N: usize>(a_mk: &impl Fn(usize) -> T, b_mk: &impl Fn(usize) -> T)
where
    T: Mul<Output = T> + Add<Output = T> + Copy + PartialEq + std::fmt::Debug,
{
    let a = Simd::<T, N>::from_fn(a_mk);
    let b = Simd::<T, N>::from_fn(b_mk);
    let c = Simd::<T, N>::from_fn(|i| a_mk(i + 7));
    assert_eq!(a.mul_add(b, c).to_array(), core::array::from_fn(|i| a_mk(i) * b_mk(i) + a_mk(i + 7)));
    let mut acc = c;
    acc.fma_from(b, a);
    assert_eq!(acc.to_array(), core::array::from_fn(|i| b_mk(i) * a_mk(i) + a_mk(i + 7)));
}

#[test]
fn fused_floats() {
    check_fused::<f64, 4>(&|i| i as f64 * 0.5 + 1.0, &|i| i as f64 * 0.25 - 2.0);
    check_fused::<f64, 3>(&|i| i as f64 * 0.5 + 1.0, &|i| i as f64 * 0.25 - 2.0);
    check_fused::<f32, 8>(&|i| i as f32 * 0.5 + 1.0, &|i| i as f32 * 0.25 - 2.0);
    check_fused::<f32, 5>(&|i| i as f32 * 0.5 + 1.0, &|i| i as f32 * 0.25 - 2.0);

    // concrete values
    let v = f64x4::from([2.0; 4]);
    assert_eq!(v.mul_add(f64x4::from([3.0; 4]), f64x4::from([1.0; 4])).to_array(), [7.0; 4]);
    let mut acc = f64x4::from([1.0; 4]);
    acc.fma_from(f64x4::from([2.0; 4]), f64x4::from([3.0; 4]));
    assert_eq!(acc.to_array(), [7.0; 4]);
}

/// The two evaluation modes genuinely differ in rounding: with
/// a = 1 + 2^-52, b = 1 - 2^-53, c = 2^-53, the exact a*b + c lies just
/// below 1 + 2^-52, so a single rounding keeps it there, while rounding
/// a*b first (to 1.0) and then adding c (tie to even) lands on 1.0.
/// Expected values are bit literals, not compiler-evaluated expressions.
#[test]
fn mul_add_rounding_mode() {
    let a = f64x8::splat(1.0 + f64::EPSILON);
    let b = f64x8::splat(1.0 - 0.5 * f64::EPSILON);
    let c = f64x8::splat(0.5 * f64::EPSILON);
    let got = a.mul_add(b, c)[0].to_bits();
    // predicate mirrors src/ops.rs (a plain `cargo test` builds for generic
    // x86-64, so the default run takes the separated path on this host)
    #[cfg(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma"))]
    assert_eq!(got, 0x3ff0000000000001); // fused: 1 + 2^-52
    #[cfg(not(any(feature = "use_libm_fma", target_arch = "aarch64", target_feature = "fma")))]
    assert_eq!(got, 0x3ff0000000000000); // separated: rounds to 1.0
}

fn check_float<T, const N: usize>(mk: &impl Fn(usize) -> T)
where
    T: Float + Copy + PartialEq + std::fmt::Debug,
{
    let a = Simd::<T, N>::from_fn(mk);
    assert_eq!(a.abs().to_array(), core::array::from_fn(|i| mk(i).abs()));
    assert_eq!(a.sqrt().to_array(), core::array::from_fn(|i| mk(i).sqrt()));
    assert_eq!(a.recip().to_array(), core::array::from_fn(|i| mk(i).recip()));
    assert_eq!(a.floor().to_array(), core::array::from_fn(|i| mk(i).floor()));
    assert_eq!(a.ceil().to_array(), core::array::from_fn(|i| mk(i).ceil()));
    assert_eq!(a.round().to_array(), core::array::from_fn(|i| mk(i).round()));
    assert_eq!(a.trunc().to_array(), core::array::from_fn(|i| mk(i).trunc()));
    assert_eq!(a.fract().to_array(), core::array::from_fn(|i| mk(i).fract()));
    let b = Simd::<T, N>::from_fn(|i| mk(i + 1));
    assert_eq!(a.min(b).to_array(), core::array::from_fn(|i| mk(i).min(mk(i + 1))));
    assert_eq!(a.max(b).to_array(), core::array::from_fn(|i| mk(i).max(mk(i + 1))));
    assert_eq!(a.copysign(b).to_array(), core::array::from_fn(|i| mk(i).copysign(mk(i + 1))));
}

#[test]
fn float_generic() {
    check_float::<f64, 8>(&|i| i as f64 * 0.5 + 0.5);
    check_float::<f64, 3>(&|i| i as f64 * 0.5 + 0.5);
    check_float::<f32, 5>(&|i| i as f32 * 0.5 + 0.5);
    check_float::<f32, 4>(&|i| i as f32 * 0.5 + 0.5);
}

#[test]
fn float_concrete_f64() {
    let v = f64x4::from([4.0, 9.0, 0.25, 2.25]);
    assert_eq!(v.sqrt().to_array(), [2.0, 3.0, 0.5, 1.5]);
    let t = f64x4::from([0.5, 1.5, 2.5, -0.5]);
    assert_eq!(t.floor().to_array(), [0.0, 1.0, 2.0, -1.0]);
    assert_eq!(t.ceil().to_array(), [1.0, 2.0, 3.0, -0.0]);
    assert_eq!(t.round().to_array(), [1.0, 2.0, 3.0, -1.0]);
    assert_eq!(t.trunc().to_array(), [0.0, 1.0, 2.0, -0.0]);
    assert_eq!(t.fract().to_array(), [0.5, 0.5, 0.5, -0.5]);
    assert_eq!(t.abs().to_array(), [0.5, 1.5, 2.5, 0.5]);
    assert_eq!(t.copysign(f64x4::splat(1.0)).to_array(), [0.5, 1.5, 2.5, 0.5]);
    assert_eq!(t.copysign(f64x4::splat(-1.0)).to_array(), [-0.5, -1.5, -2.5, -0.5]);
    assert_eq!(f64x4::splat(2.0).recip().to_array(), [0.5; 4]);
    let u = f64x4::from([1.5, 2.5, -1.5, -2.5]);
    assert_eq!(u.round_ties_even().to_array(), [2.0, 2.0, -2.0, -2.0]);
    // scalar right-hand side
    assert_eq!(f64x4::splat(1.0) * 2.0, f64x4::splat(2.0));
}

fn check_compare<T, const N: usize>(mk: &impl Fn(usize) -> T)
where
    T: PartialOrd + Copy + std::fmt::Debug,
{
    let a = Simd::<T, N>::from_fn(mk);
    let b = Simd::<T, N>::from_fn(|i| mk(i + 1));
    assert_eq!(a.simd_lt(b).to_array(), core::array::from_fn(|i| a[i] < b[i]));
    assert_eq!(a.simd_le(b).to_array(), core::array::from_fn(|i| a[i] <= b[i]));
    assert_eq!(a.simd_gt(b).to_array(), core::array::from_fn(|i| a[i] > b[i]));
    assert_eq!(a.simd_ge(b).to_array(), core::array::from_fn(|i| a[i] >= b[i]));
    assert_eq!(a.simd_eq(a).to_array(), [true; N]);
    assert_eq!(a.simd_ne(a).to_array(), [false; N]);
    // select
    let m = a.simd_ge(b);
    assert_eq!(m.select(a, b).to_array(), core::array::from_fn(|i| if a[i] >= b[i] { a[i] } else { b[i] }));
    // mask composition via bitwise operators
    let m1 = a.simd_lt(b);
    let m2 = a.simd_le(b);
    assert_eq!((m1 & m2).to_array(), core::array::from_fn(|i| (a[i] < b[i]) & (a[i] <= b[i])));
    assert_eq!((m1 | m2).to_array(), core::array::from_fn(|i| (a[i] < b[i]) | (a[i] <= b[i])));
    assert_eq!((m1 ^ m2).to_array(), core::array::from_fn(|i| (a[i] < b[i]) ^ (a[i] <= b[i])));
    assert_eq!((!m1).to_array(), core::array::from_fn(|i| a[i] >= b[i]));
}

#[test]
fn compare_and_masks() {
    check_compare::<f64, 8>(&|i| i as f64 + 0.5);
    check_compare::<f64, 3>(&|i| i as f64 + 0.5);
    check_compare::<i64, 5>(&|i| i as i64 * 3);
    check_compare::<f32, 4>(&|i| i as f32 * 1.5);
}

#[test]
fn mask_concrete() {
    let m = mask8::from([true, false, true, true, false, false, true, false]);
    assert!(m.any_true());
    assert!(!m.all_true());
    assert_eq!(f64x8::splat(1.0).mask_select(m, f64x8::splat(2.0)).reduce_sum(), 12.0);
    assert!(!(m & mask8::splat(false)).any_true());
    assert!((m | mask8::splat(true)).all_true());
}

fn check_reduce<T, const N: usize>(mk: &impl Fn(usize) -> T)
where
    T: Num + Copy + PartialEq + std::fmt::Debug,
{
    let v = Simd::<T, N>::from_fn(mk);
    let mut sum = T::zero();
    let mut prod = T::one();
    for i in 0..N {
        sum = sum + mk(i);
        prod = prod * mk(i);
    }
    assert_eq!(v.reduce_sum(), sum);
    assert_eq!(v.reduce_product(), prod);
    for n in 0..=N {
        let mut s = T::zero();
        let mut p = T::one();
        for i in 0..n {
            s = s + mk(i);
            p = p * mk(i);
        }
        assert_eq!(v.reduce_sum_n(n), s);
        assert_eq!(v.reduce_product_n(n), p);
    }
}

#[test]
fn reduce_floats_and_ints() {
    check_reduce::<f64, 1>(&|i| i as f64 * 0.5 + 1.0);
    check_reduce::<f64, 3>(&|i| i as f64 * 0.5 + 1.0);
    check_reduce::<f64, 8>(&|i| i as f64 * 0.5 + 1.0);
    check_reduce::<f64, 32>(&|i| i as f64 * 0.5 + 1.0);
    check_reduce::<f32, 5>(&|i| i as f32 * 0.5 + 1.0);
    check_reduce::<i64, 8>(&|i| i as i64 % 2 + 1);
    check_reduce::<i64, 32>(&|i| i as i64 % 2 + 1);
}

#[cfg(feature = "complex")]
#[test]
fn complex_aliases() {
    use num_complex::Complex;
    let v: lightweight_simd::c64x8 = lightweight_simd::c64x8::splat(Complex::new(1.0, -1.0));
    assert_eq!(v.reduce_sum(), Complex::new(8.0, -8.0));
}

#[cfg(feature = "half")]
#[test]
fn half_aliases() {
    use half::{bf16, f16};
    let v = lightweight_simd::f16x8::splat(f16::from_f32(1.0));
    assert_eq!(v.reduce_sum_n(4), f16::from_f32(4.0));
    let w = lightweight_simd::bf16x16::splat(bf16::from_f32(0.5));
    assert_eq!(w.reduce_sum(), bf16::from_f32(8.0));
}