use std::hash::{BuildHasher, Hasher};
#[inline]
const fn read_u32(s: &[u8], i: usize) -> u32 {
(s[i] as u32) | ((s[i + 1] as u32) << 8) | ((s[i + 2] as u32) << 16) | ((s[i + 3] as u32) << 24)
}
#[inline]
#[expect(clippy::cast_possible_truncation)]
const fn mix(a: u32, b: u32) -> u32 {
let m = (a as u64).wrapping_mul(b as u64);
(m as u32) ^ ((m >> 32) as u32)
}
#[inline]
pub const fn ident_hash(s: &[u8]) -> u32 {
const SEED1: u32 = 0x9E37_79B9;
const SEED2: u32 = 0x85EB_CA6B;
let len = s.len();
if len == 0 {
return 0;
}
if len < 4 {
let packed = ((s[0] as u32) << 16) | ((s[len >> 1] as u32) << 8) | (s[len - 1] as u32);
mix(packed ^ SEED1, packed ^ SEED2)
} else if len <= 8 {
let head = read_u32(s, 0);
let tail = read_u32(s, len - 4);
mix(head ^ SEED1, tail ^ SEED2)
} else if len <= 16 {
let head = read_u32(s, 0);
let mid = read_u32(s, (len >> 1) - 2);
let tail = read_u32(s, len - 4);
mix(head ^ mid ^ SEED1, tail ^ SEED2)
} else {
let head = read_u32(s, 0);
let mid1 = read_u32(s, len / 3);
let mid2 = read_u32(s, 2 * len / 3);
let tail = read_u32(s, len - 4);
mix(head ^ mid1 ^ SEED1, mid2 ^ tail ^ SEED2)
}
}
#[inline]
pub const fn pack_len_hash(len: u32, hash: u32) -> u64 {
(len as u64) | ((hash as u64) << 32)
}
#[inline]
const fn hashbrown_state(len: u32, hash: u32) -> u64 {
let low = hash ^ len.rotate_left(16);
(low as u64) | ((hash as u64) << 32)
}
#[expect(clippy::cast_possible_truncation)]
#[inline]
const fn unpack_len_hash(packed: u64) -> (u32, u32) {
(packed as u32, (packed >> 32) as u32)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IdentBuildHasher;
impl BuildHasher for IdentBuildHasher {
type Hasher = IdentHasher;
#[inline]
fn build_hasher(&self) -> Self::Hasher {
IdentHasher { state: 0 }
}
}
#[derive(Debug, Clone, Copy)]
pub struct IdentHasher {
state: u64,
}
impl Hasher for IdentHasher {
#[inline]
fn write_u64(&mut self, i: u64) {
let (len, hash) = unpack_len_hash(i);
self.state = hashbrown_state(len, hash);
}
#[inline]
#[expect(clippy::cast_possible_truncation)] fn write(&mut self, bytes: &[u8]) {
let hash = ident_hash(bytes);
self.state = hashbrown_state(bytes.len() as u32, hash);
}
#[inline]
fn write_u8(&mut self, _: u8) {}
#[inline]
fn finish(&self) -> u64 {
self.state
}
}
#[cfg(test)]
mod test {
use std::hash::{BuildHasher, Hasher};
use super::{
IdentBuildHasher, IdentHasher, hashbrown_state, ident_hash, pack_len_hash, unpack_len_hash,
};
#[test]
fn hash_empty() {
assert_eq!(ident_hash(b""), 0);
}
#[test]
fn hash_nonzero_for_all_lengths() {
for s in
&[b"x" as &[u8], b"ab", b"abc", b"abcd", b"hello", b"useState", b"longIdentifierName"]
{
assert_ne!(
ident_hash(s),
0,
"hash should be non-zero for {:?}",
std::str::from_utf8(s)
);
}
}
#[test]
fn hash_discriminates_similar_idents() {
assert_ne!(ident_hash(b"null"), ident_hash(b"void"));
assert_ne!(ident_hash(b"this"), ident_hash(b"that"));
assert_ne!(ident_hash(b"abcd"), ident_hash(b"abce"));
assert_ne!(ident_hash(b"useState"), ident_hash(b"useEffect"));
assert_ne!(ident_hash(b"fooBar"), ident_hash(b"fooBaz"));
assert_ne!(ident_hash(b"a"), ident_hash(b"b"));
assert_ne!(ident_hash(b"ab"), ident_hash(b"ba")); }
#[test]
fn hash_four_byte_no_zero_collision() {
let keywords = [b"null", b"void", b"this", b"that", b"true", b"else", b"case", b"from"];
for kw in &keywords {
assert_ne!(ident_hash(*kw), 0, "4-byte keyword {kw:?} should not hash to 0");
}
for (i, a) in keywords.iter().enumerate() {
for b in &keywords[i + 1..] {
assert_ne!(
ident_hash(*a),
ident_hash(*b),
"{a:?} and {b:?} should have different hashes",
);
}
}
}
#[test]
fn hash_long_strings_with_middle_differences() {
assert_ne!(
ident_hash(b"privateInterfaceWithPrivatePropertyTypes"),
ident_hash(b"privateInterfaceWithPrivateParmeterTypes"), );
}
#[test]
fn pack_round_trip() {
let len = 42u32;
let hash = 0xDEAD_BEEFu32;
let packed = pack_len_hash(len, hash);
assert_eq!((packed & 0xFFFF_FFFF) as u32, len);
assert_eq!((packed >> 32) as u32, hash);
}
#[test]
fn hasher_write_u64_path() {
let mut hasher = IdentHasher { state: 0 };
let hash = ident_hash(b"hello");
let packed = pack_len_hash(5, hash);
hasher.write_u64(packed);
assert_eq!(hasher.finish(), hashbrown_state(5, hash));
}
#[test]
fn hasher_write_bytes_path() {
let mut hasher = IdentHasher { state: 0 };
hasher.write(b"hello");
let hash = ident_hash(b"hello");
let expected = hashbrown_state(5, hash);
assert_eq!(hasher.finish(), expected);
}
#[test]
fn str_hash_matches_ident_packed() {
let build_hasher = IdentBuildHasher;
let mut hasher = build_hasher.build_hasher();
hasher.write(b"fooBar");
hasher.write_u8(0xFF);
let str_hash = hasher.finish();
let mut hasher2 = build_hasher.build_hasher();
let packed = pack_len_hash(6, ident_hash(b"fooBar"));
hasher2.write_u64(packed);
let ident_hash_val = hasher2.finish();
assert_eq!(str_hash, ident_hash_val);
}
#[test]
fn build_hasher_default() {
let bh = IdentBuildHasher;
let hasher = bh.build_hasher();
assert_eq!(hasher.finish(), 0);
}
#[test]
fn std_str_hash_compatible() {
let build_hasher = IdentBuildHasher;
let std_hash = build_hasher.hash_one("hello");
let mut hasher2 = build_hasher.build_hasher();
hasher2.write(b"hello");
hasher2.write_u8(0xFF);
let manual_hash = hasher2.finish();
assert_eq!(std_hash, manual_hash);
}
#[test]
fn str_hash_matches_across_lengths() {
let test_cases = [
"x", "ab", "foo", "this", "hello", "fooBar", "useState", "useEffect", "myVariable123", "longIdentifierNm", "privateInterfaceWithPrivatePropertyTypes", ];
let build_hasher = IdentBuildHasher;
for s in &test_cases {
let str_hash = build_hasher.hash_one(*s);
let mut hasher = build_hasher.build_hasher();
#[expect(clippy::cast_possible_truncation)]
let packed = pack_len_hash(s.len() as u32, ident_hash(s.as_bytes()));
hasher.write_u64(packed);
let ident_hash_val = hasher.finish();
assert_eq!(str_hash, ident_hash_val, "hash mismatch for {s:?} (len={})", s.len());
}
}
#[test]
fn hashbrown_state_uses_hash_entropy_for_h1() {
let len = 6u32;
let hash1 = ident_hash(b"fooBar");
let hash2 = ident_hash(b"fooBaz");
let state1 = hashbrown_state(len, hash1);
let state2 = hashbrown_state(len, hash2);
assert_eq!((state1 >> 32) as u32, hash1);
assert_eq!((state2 >> 32) as u32, hash2);
let (low1, _) = unpack_len_hash(state1);
let (low2, _) = unpack_len_hash(state2);
assert_ne!(low1, low2);
}
}