axhash-core 1.0.0

Platform-agnostic AxHash core for Rust with no_std compatibility.
Documentation
use core::hash::{BuildHasher, Hasher};

// Helper: compute hash via the architecture's SIMD backend, if one is available.
// On scalar-only targets this falls back to scalar so the test still runs.
#[allow(unused_variables)]
unsafe fn hash_long_simd(ptr: *const u8, len: usize, acc: u64) -> u64 {
    #[cfg(target_arch = "aarch64")]
    {
        if std::arch::is_aarch64_feature_detected!("neon") {
            return unsafe { crate::backend::aarch64::hash_bytes_long(ptr, len, acc) };
        }
    }
    #[cfg(target_arch = "x86_64")]
    {
        if std::arch::is_x86_feature_detected!("avx2") {
            return unsafe { crate::backend::x86_64::hash_bytes_long(ptr, len, acc) };
        }
    }
    unsafe { crate::backend::scalar::hash_bytes_long(ptr, len, acc) }
}

#[test]
fn backend_parity_short_inputs() {
    let seed = 0x1234_5678_9abc_def0u64;
    let data = vec![0xABu8; 128];

    for len in [0usize, 1, 8, 16, 17, 32, 33, 64, 65, 96, 128] {
        let slice = &data[..len];
        let h1 = crate::axhash_seeded(slice, seed);
        let h2 = crate::axhash_seeded(slice, seed);
        assert_eq!(h1, h2, "non-deterministic at len={}", len);
    }
}

// The critical test: prove scalar and the native SIMD backend produce
// bit-identical output for every length the long path covers. If this fails,
// the cross-device hash divergence is back.
#[test]
fn backend_parity_scalar_vs_simd_long_all_lengths() {
    let seed = 0x1234_5678_9abc_def0u64;
    let data: Vec<u8> = (0..2048u32).map(|i| (i as u8).wrapping_mul(7)).collect();

    for len in 129..=2048usize {
        let slice = &data[..len];
        let acc = crate::math::seed_lane(seed, 0).rotate_right(len as u32);

        let scalar = unsafe { crate::backend::scalar::hash_bytes_long(slice.as_ptr(), len, acc) };
        let simd = unsafe { hash_long_simd(slice.as_ptr(), len, acc) };

        assert_eq!(
            scalar, simd,
            "scalar != simd at len={} (scalar=0x{:016x}, simd=0x{:016x})",
            len, scalar, simd,
        );
    }
}

#[test]
fn backend_parity_scalar_vs_simd_long_diverse_seeds() {
    let seeds = [
        0u64,
        1,
        0xFFFF_FFFF_FFFF_FFFF,
        0xDEAD_BEEF_CAFE_BABE,
        0x0123_4567_89AB_CDEF,
        0x9999_AAAA_BBBB_CCCC,
    ];
    let lens = [129usize, 130, 191, 192, 193, 255, 256, 257, 511, 512, 1023, 1024, 1025, 2048];
    let data: Vec<u8> = (0..2048u32).map(|i| (i as u8).wrapping_mul(13)).collect();

    for &seed in &seeds {
        for &len in &lens {
            let slice = &data[..len];
            let acc = crate::math::seed_lane(seed, 0).rotate_right(len as u32);

            let scalar = unsafe {
                crate::backend::scalar::hash_bytes_long(slice.as_ptr(), len, acc)
            };
            let simd = unsafe { hash_long_simd(slice.as_ptr(), len, acc) };

            assert_eq!(
                scalar, simd,
                "scalar != simd at seed=0x{:016x} len={}",
                seed, len
            );
        }
    }
}

#[test]
fn backend_parity_scalar_vs_simd_long_diverse_patterns() {
    let seed = 0x7777_8888_9999_AAAAu64;
    let lens = [200usize, 256, 333, 500, 777, 1024];

    let patterns: Vec<(&str, Box<dyn Fn(usize) -> Vec<u8>>)> = vec![
        ("zeros", Box::new(|n| vec![0x00; n])),
        ("ones", Box::new(|n| vec![0xFF; n])),
        ("a5", Box::new(|n| vec![0xA5; n])),
        ("ascending", Box::new(|n| (0..n).map(|i| i as u8).collect())),
        (
            "descending",
            Box::new(|n| (0..n).map(|i| 255u8.wrapping_sub(i as u8)).collect()),
        ),
        ("xor55", Box::new(|n| (0..n).map(|i| (i as u8) ^ 0x55).collect())),
    ];

    for (label, make) in &patterns {
        for &len in &lens {
            let data = make(len);
            let acc = crate::math::seed_lane(seed, 0).rotate_right(len as u32);

            let scalar = unsafe {
                crate::backend::scalar::hash_bytes_long(data.as_ptr(), len, acc)
            };
            let simd = unsafe { hash_long_simd(data.as_ptr(), len, acc) };

            assert_eq!(
                scalar, simd,
                "scalar != simd pattern={} len={}",
                label, len
            );
        }
    }
}

#[test]
fn backend_api_paths_consistent_all_lengths() {
    let seed = 0xDEAD_BEEF_CAFE_BABEu64;
    let data: Vec<u8> = (0..1024u32).map(|i| (i as u8).wrapping_mul(11)).collect();

    for len in [
        0usize, 1, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 96, 128, 129, 200, 256, 500, 1024,
    ] {
        let slice = &data[..len];

        let one_shot = crate::axhash_seeded(slice, seed);

        let mut stream = crate::AxHasher::new_with_seed(seed);
        stream.write(slice);
        let streaming = stream.finish();

        let builder = crate::AxBuildHasher::with_seed(seed);
        let mut from_builder = builder.build_hasher();
        from_builder.write(slice);
        let built = from_builder.finish();

        assert_eq!(one_shot, streaming, "one_shot != streaming at len={}", len);
        assert_eq!(one_shot, built, "one_shot != built at len={}", len);
    }
}