#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasherDefault, Hasher};
const SEED: u64 = 0x517c_c1b7_2722_0a95;
const ROTATE: u32 = 5;
#[inline]
pub fn fmix64(mut h: u64) -> u64 {
h ^= h >> 33;
h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
h ^= h >> 33;
h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
h ^= h >> 33;
h
}
#[inline]
fn mix(state: u64, word: u64) -> u64 {
(state.rotate_left(ROTATE) ^ word).wrapping_mul(SEED)
}
#[inline]
fn hash_bytes_pipelined(bytes: &[u8]) -> u64 {
const S1: u64 = 0x243f_6a88_85a3_08d3;
const S2: u64 = 0x1319_8a2e_0370_7344;
const ANTI_ZERO: u64 = 0xa409_3822_299f_31d0;
let len = bytes.len();
let mut s0 = S1;
let mut s1 = S2;
if len <= 16 {
if len >= 8 {
s0 ^= u64::from_le_bytes(bytes[0..8].try_into().unwrap());
s1 ^= u64::from_le_bytes(bytes[len - 8..].try_into().unwrap());
} else if len >= 4 {
s0 ^= u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as u64;
s1 ^= u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64;
} else if len > 0 {
let lo = bytes[0];
let mid = bytes[len / 2];
let hi = bytes[len - 1];
s0 ^= lo as u64;
s1 ^= ((hi as u64) << 8) | mid as u64;
}
} else {
let mut bulk = &bytes[..len - 1];
while let Some((chunk, rest)) = bulk.split_first_chunk::<16>() {
let x = u64::from_le_bytes(chunk[..8].try_into().unwrap());
let y = u64::from_le_bytes(chunk[8..].try_into().unwrap());
let t = multiply_mix(s0 ^ x, ANTI_ZERO ^ y);
s0 = s1;
s1 = t;
bulk = rest;
}
let suffix = &bytes[len - 16..];
s0 ^= u64::from_le_bytes(suffix[0..8].try_into().unwrap());
s1 ^= u64::from_le_bytes(suffix[8..16].try_into().unwrap());
}
let folded = multiply_mix(s0, s1) ^ (len as u64);
fmix64(folded)
}
#[inline]
fn multiply_mix(x: u64, y: u64) -> u64 {
let full = (x as u128).wrapping_mul(y as u128);
let lo = full as u64;
let hi = (full >> 64) as u64;
lo ^ hi
}
#[derive(Default)]
pub struct FxHasher(u64);
impl Hasher for FxHasher {
#[inline]
fn finish(&self) -> u64 {
fmix64(self.0)
}
#[inline]
fn write(&mut self, mut bytes: &[u8]) {
let mut state = self.0;
while bytes.len() >= 8 {
let word = u64::from_le_bytes(bytes[..8].try_into().unwrap());
state = mix(state, word);
bytes = &bytes[8..];
}
if bytes.len() >= 4 {
let word = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as u64;
state = mix(state, word);
bytes = &bytes[4..];
}
for &b in bytes {
state = mix(state, b as u64);
}
self.0 = state;
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.0 = mix(self.0, i);
}
#[inline]
fn write_usize(&mut self, i: usize) {
self.0 = mix(self.0, i as u64);
}
}
pub type FxBuildHasher = BuildHasherDefault<FxHasher>;
pub trait KevyHash {
fn kevy_hash(&self) -> u64;
}
impl KevyHash for [u8] {
#[inline]
fn kevy_hash(&self) -> u64 {
hash_bytes_pipelined(self)
}
}
impl KevyHash for Vec<u8> {
#[inline]
fn kevy_hash(&self) -> u64 {
self.as_slice().kevy_hash()
}
}
impl KevyHash for u64 {
#[inline]
fn kevy_hash(&self) -> u64 {
fmix64(mix(0, *self))
}
}
impl KevyHash for u32 {
#[inline]
fn kevy_hash(&self) -> u64 {
fmix64(mix(0, *self as u64))
}
}
impl KevyHash for i32 {
#[inline]
fn kevy_hash(&self) -> u64 {
fmix64(mix(0, *self as i64 as u64))
}
}
impl KevyHash for usize {
#[inline]
fn kevy_hash(&self) -> u64 {
fmix64(mix(0, *self as u64))
}
}
pub type FxHashMap<K, V> = HashMap<K, V, FxBuildHasher>;
pub type FxHashSet<T> = HashSet<T, FxBuildHasher>;
#[cfg(test)]
mod tests {
use super::*;
use std::hash::BuildHasher;
fn h(bytes: &[u8]) -> u64 {
FxBuildHasher::default().hash_one(bytes)
}
#[test]
fn deterministic_across_instances() {
assert_eq!(h(b"hello"), h(b"hello"));
assert_ne!(h(b"hello"), h(b"hellp"));
assert_ne!(h(b""), h(b"\0"));
}
#[test]
fn map_roundtrip() {
let mut m: FxHashMap<Vec<u8>, u64> = FxHashMap::default();
for i in 0..10_000u64 {
m.insert(format!("key:{i}").into_bytes(), i);
}
assert_eq!(m.len(), 10_000);
for i in 0..10_000u64 {
assert_eq!(m.get(format!("key:{i}").into_bytes().as_slice()), Some(&i));
}
}
#[test]
fn kevy_hash_bytes_is_deterministic_and_distinct() {
let key = b"hello-world".as_slice();
assert_eq!(key.kevy_hash(), key.kevy_hash());
assert_ne!(key.kevy_hash(), b"hello-worle".as_slice().kevy_hash());
assert_ne!(b"abc".as_slice().kevy_hash(), b"abcd".as_slice().kevy_hash());
let mut staged = FxHasher::default();
staged.write(key);
let _fx_legacy = staged.finish();
}
#[test]
fn kevy_hash_integer_paths_differ_per_value() {
let a: u64 = 1;
let b: u64 = 2;
assert_ne!(a.kevy_hash(), b.kevy_hash());
let i: i32 = -1;
let j: i32 = 1;
assert_ne!(i.kevy_hash(), j.kevy_hash());
}
#[test]
fn kevy_hash_top7_bits_distribute() {
let mut top = [0u32; 128];
for i in 0..4096u64 {
let mut k = format!("key:{i}").into_bytes();
k.resize(12, b'x');
let hash = k.as_slice().kevy_hash();
top[(hash >> 57) as usize] += 1;
}
let max = *top.iter().max().unwrap();
assert!(max < 128, "top-7-bit skew {max} (mean 32) — avalanche failing");
}
#[test]
fn integer_keys_roundtrip() {
let mut m: FxHashMap<u64, u64> = FxHashMap::default();
for i in 0..1_000u64 {
m.insert(i, i * 2);
}
assert_eq!(m.get(&500), Some(&1_000));
assert_eq!(m.get(&999), Some(&1_998));
}
#[test]
fn no_catastrophic_clustering_on_low_entropy_keys() {
let keys: Vec<Vec<u8>> = (0..4096u64)
.map(|i| {
let mut k = format!("key:{i}").into_bytes();
k.resize(12, b'x');
k
})
.collect();
let mut low = [0u32; 256];
let mut top = [0u32; 128];
for k in &keys {
let hash = h(k);
low[(hash & 0xff) as usize] += 1;
top[(hash >> 57) as usize] += 1;
}
let max_low = *low.iter().max().unwrap();
let max_top = *top.iter().max().unwrap();
assert!(max_low < 64, "low-bit skew {max_low} (mean 16) — avalanche failing");
assert!(max_top < 128, "top-bit skew {max_top} (mean 32) — avalanche failing");
}
#[test]
fn kevy_hash_vec_u8_agrees_with_slice() {
let v: Vec<u8> = b"hello-world".to_vec();
assert_eq!(v.kevy_hash(), v.as_slice().kevy_hash());
}
#[test]
fn kevy_hash_u32_agrees_with_widened_u64() {
let n: u32 = 0xCAFE_BABE;
assert_eq!(n.kevy_hash(), (n as u64).kevy_hash());
let m: u32 = n.wrapping_add(1);
assert_ne!(n.kevy_hash(), m.kevy_hash());
}
#[test]
fn kevy_hash_usize_agrees_with_u64() {
let n: usize = 42;
assert_eq!(n.kevy_hash(), (n as u64).kevy_hash());
let m: usize = 43;
assert_ne!(n.kevy_hash(), m.kevy_hash());
}
}