1use std::hash::{BuildHasher, Hasher};
11
12#[inline]
14const fn read_u32(s: &[u8], i: usize) -> u32 {
15 (s[i] as u32) | ((s[i + 1] as u32) << 8) | ((s[i + 2] as u32) << 16) | ((s[i + 3] as u32) << 24)
16}
17
18#[inline]
24#[expect(clippy::cast_possible_truncation)]
25const fn mix(a: u32, b: u32) -> u32 {
26 let m = (a as u64).wrapping_mul(b as u64);
27 (m as u32) ^ ((m >> 32) as u32)
28}
29
30#[inline]
41pub const fn ident_hash(s: &[u8]) -> u32 {
42 const SEED1: u32 = 0x9E37_79B9;
44 const SEED2: u32 = 0x85EB_CA6B;
45
46 let len = s.len();
47 if len == 0 {
48 return 0;
49 }
50
51 if len < 4 {
52 let packed = ((s[0] as u32) << 16) | ((s[len >> 1] as u32) << 8) | (s[len - 1] as u32);
55 mix(packed ^ SEED1, packed ^ SEED2)
56 } else if len <= 8 {
57 let head = read_u32(s, 0);
60 let tail = read_u32(s, len - 4);
61 mix(head ^ SEED1, tail ^ SEED2)
62 } else if len <= 16 {
63 let head = read_u32(s, 0);
65 let mid = read_u32(s, (len >> 1) - 2);
66 let tail = read_u32(s, len - 4);
67 mix(head ^ mid ^ SEED1, tail ^ SEED2)
68 } else {
69 let head = read_u32(s, 0);
71 let mid1 = read_u32(s, len / 3);
72 let mid2 = read_u32(s, 2 * len / 3);
73 let tail = read_u32(s, len - 4);
74 mix(head ^ mid1 ^ SEED1, mid2 ^ tail ^ SEED2)
75 }
76}
77
78#[inline]
85pub const fn pack_len_hash(len: u32, hash: u32) -> u64 {
86 (len as u64) | ((hash as u64) << 32)
87}
88
89#[inline]
95const fn hashbrown_state(len: u32, hash: u32) -> u64 {
96 let low = hash ^ len.rotate_left(16);
97 (low as u64) | ((hash as u64) << 32)
98}
99
100#[expect(clippy::cast_possible_truncation)]
102#[inline]
103const fn unpack_len_hash(packed: u64) -> (u32, u32) {
104 (packed as u32, (packed >> 32) as u32)
105}
106
107#[derive(Debug, Clone, Copy, Default)]
112pub struct IdentBuildHasher;
113
114impl BuildHasher for IdentBuildHasher {
115 type Hasher = IdentHasher;
116
117 #[inline]
118 fn build_hasher(&self) -> Self::Hasher {
119 IdentHasher { state: 0 }
120 }
121}
122
123#[derive(Debug, Clone, Copy)]
125pub struct IdentHasher {
126 state: u64,
127}
128
129impl Hasher for IdentHasher {
130 #[inline]
132 fn write_u64(&mut self, i: u64) {
133 let (len, hash) = unpack_len_hash(i);
134 self.state = hashbrown_state(len, hash);
135 }
136
137 #[inline]
139 #[expect(clippy::cast_possible_truncation)] fn write(&mut self, bytes: &[u8]) {
141 let hash = ident_hash(bytes);
142 self.state = hashbrown_state(bytes.len() as u32, hash);
143 }
144
145 #[inline]
147 fn write_u8(&mut self, _: u8) {}
148
149 #[inline]
150 fn finish(&self) -> u64 {
151 self.state
152 }
153}
154
155#[cfg(test)]
156mod test {
157 use std::hash::{BuildHasher, Hasher};
158
159 use super::{
160 IdentBuildHasher, IdentHasher, hashbrown_state, ident_hash, pack_len_hash, unpack_len_hash,
161 };
162
163 #[test]
166 fn hash_empty() {
167 assert_eq!(ident_hash(b""), 0);
168 }
169
170 #[test]
171 fn hash_nonzero_for_all_lengths() {
172 for s in
174 &[b"x" as &[u8], b"ab", b"abc", b"abcd", b"hello", b"useState", b"longIdentifierName"]
175 {
176 assert_ne!(
177 ident_hash(s),
178 0,
179 "hash should be non-zero for {:?}",
180 std::str::from_utf8(s)
181 );
182 }
183 }
184
185 #[test]
186 fn hash_discriminates_similar_idents() {
187 assert_ne!(ident_hash(b"null"), ident_hash(b"void"));
189 assert_ne!(ident_hash(b"this"), ident_hash(b"that"));
190 assert_ne!(ident_hash(b"abcd"), ident_hash(b"abce"));
191 assert_ne!(ident_hash(b"useState"), ident_hash(b"useEffect"));
192 assert_ne!(ident_hash(b"fooBar"), ident_hash(b"fooBaz"));
193 assert_ne!(ident_hash(b"a"), ident_hash(b"b"));
194 assert_ne!(ident_hash(b"ab"), ident_hash(b"ba")); }
196
197 #[test]
198 fn hash_four_byte_no_zero_collision() {
199 let keywords = [b"null", b"void", b"this", b"that", b"true", b"else", b"case", b"from"];
201 for kw in &keywords {
202 assert_ne!(ident_hash(*kw), 0, "4-byte keyword {kw:?} should not hash to 0");
203 }
204 for (i, a) in keywords.iter().enumerate() {
206 for b in &keywords[i + 1..] {
207 assert_ne!(
208 ident_hash(*a),
209 ident_hash(*b),
210 "{a:?} and {b:?} should have different hashes",
211 );
212 }
213 }
214 }
215
216 #[test]
217 fn hash_long_strings_with_middle_differences() {
218 assert_ne!(
220 ident_hash(b"privateInterfaceWithPrivatePropertyTypes"),
221 ident_hash(b"privateInterfaceWithPrivateParmeterTypes"), );
223 }
224
225 #[test]
228 fn pack_round_trip() {
229 let len = 42u32;
230 let hash = 0xDEAD_BEEFu32;
231 let packed = pack_len_hash(len, hash);
232 assert_eq!((packed & 0xFFFF_FFFF) as u32, len);
233 assert_eq!((packed >> 32) as u32, hash);
234 }
235
236 #[test]
239 fn hasher_write_u64_path() {
240 let mut hasher = IdentHasher { state: 0 };
241 let hash = ident_hash(b"hello");
242 let packed = pack_len_hash(5, hash);
243 hasher.write_u64(packed);
244 assert_eq!(hasher.finish(), hashbrown_state(5, hash));
245 }
246
247 #[test]
248 fn hasher_write_bytes_path() {
249 let mut hasher = IdentHasher { state: 0 };
250 hasher.write(b"hello");
251 let hash = ident_hash(b"hello");
252 let expected = hashbrown_state(5, hash);
253 assert_eq!(hasher.finish(), expected);
254 }
255
256 #[test]
257 fn str_hash_matches_ident_packed() {
258 let build_hasher = IdentBuildHasher;
260 let mut hasher = build_hasher.build_hasher();
261 hasher.write(b"fooBar");
262 hasher.write_u8(0xFF);
263 let str_hash = hasher.finish();
264
265 let mut hasher2 = build_hasher.build_hasher();
267 let packed = pack_len_hash(6, ident_hash(b"fooBar"));
268 hasher2.write_u64(packed);
269 let ident_hash_val = hasher2.finish();
270
271 assert_eq!(str_hash, ident_hash_val);
272 }
273
274 #[test]
275 fn build_hasher_default() {
276 let bh = IdentBuildHasher;
277 let hasher = bh.build_hasher();
278 assert_eq!(hasher.finish(), 0);
279 }
280
281 #[test]
284 fn std_str_hash_compatible() {
285 let build_hasher = IdentBuildHasher;
286 let std_hash = build_hasher.hash_one("hello");
287
288 let mut hasher2 = build_hasher.build_hasher();
289 hasher2.write(b"hello");
290 hasher2.write_u8(0xFF);
291 let manual_hash = hasher2.finish();
292
293 assert_eq!(std_hash, manual_hash);
294 }
295
296 #[test]
298 fn str_hash_matches_across_lengths() {
299 let test_cases = [
300 "x", "ab", "foo", "this", "hello", "fooBar", "useState", "useEffect", "myVariable123", "longIdentifierNm", "privateInterfaceWithPrivatePropertyTypes", ];
312
313 let build_hasher = IdentBuildHasher;
314 for s in &test_cases {
315 let str_hash = build_hasher.hash_one(*s);
317
318 let mut hasher = build_hasher.build_hasher();
320 #[expect(clippy::cast_possible_truncation)]
321 let packed = pack_len_hash(s.len() as u32, ident_hash(s.as_bytes()));
322 hasher.write_u64(packed);
323 let ident_hash_val = hasher.finish();
324
325 assert_eq!(str_hash, ident_hash_val, "hash mismatch for {s:?} (len={})", s.len());
326 }
327 }
328
329 #[test]
330 fn hashbrown_state_uses_hash_entropy_for_h1() {
331 let len = 6u32;
332 let hash1 = ident_hash(b"fooBar");
333 let hash2 = ident_hash(b"fooBaz");
334
335 let state1 = hashbrown_state(len, hash1);
336 let state2 = hashbrown_state(len, hash2);
337
338 assert_eq!((state1 >> 32) as u32, hash1);
340 assert_eq!((state2 >> 32) as u32, hash2);
341 let (low1, _) = unpack_len_hash(state1);
343 let (low2, _) = unpack_len_hash(state2);
344 assert_ne!(low1, low2);
345 }
346}