Skip to main content

bun_wyhash/
lib.rs

1//
2// `Wyhash11` is a copy of Wyhash from the zig standard library, version v0.11.0-dev.2609+5e19250a1
3// (the older 32-byte-round, 5-prime variant kept in src/wyhash/wyhash.zig).
4//
5// `Wyhash` is the current `std.hash.Wyhash` (final4 variant, 48-byte rounds, 4 secrets) ported
6// from vendor/zig/lib/std/hash/wyhash.zig — used by RuntimeTranspilerCache, `bun.hash()`, router.
7//
8// THESE ARE DIFFERENT ALGORITHMS. They produce different outputs for the same input.
9//
10
11// ════════════════════════════════════════════════════════════════════════════
12// Wyhash11 (legacy, 32-byte rounds, 5 primes)
13// ════════════════════════════════════════════════════════════════════════════
14
15// Dual-mode: nightly keeps the prefix-free fast path (`write_length_prefix`
16// override, no length mix — matches Zig's `bun.StringHashMapContext.hash`);
17// stable compiles without the override, so `Hasher`'s provided default
18// (`write_usize(len)`) mixes the length instead. Hash VALUES differ between
19// channels (stable tables just reorder); both are behaviorally correct and
20// pinned separately by the test suite.
21#![cfg_attr(bao_nightly, feature(hasher_prefixfree_extras))]
22const PRIMES: [u64; 5] = [
23    0xa0761d6478bd642f,
24    0xe7037ed1a0b428db,
25    0x8ebc6af09c88c6e3,
26    0x589965cc75374cc3,
27    0x1d8e4e27c47d124f,
28];
29
30// Zig: `inline fn read(comptime T, data) { mem.readInt(T, data[0..@sizeOf(T)], .little) }`
31// — a single unaligned load. Mirrors the `Wyhash::read4`/`read8` treatment
32// below (~lib.rs:600): the per-byte `[data[0], data[1], ...]` spelling left a
33// cmp/je-to-panic ladder per byte in `WyhashStateless::final_`/`round`, and
34// `final_` is the `StringHashMap` short-key hash — hit on every parser /
35// resolver / module-registry HashMap probe during startup (identifier/path
36// keys are <32B so `aligned_len == 0` and the whole key goes through `final_`).
37// Every caller proves the length first (the `1..=31` arms of `final_` slice
38// `rem_key = &b[0..rem_len]` with `rem_len` == the match scrutinee; `round`
39// takes a `&[u8]` it `debug_assert!`s == 32), so match Zig's codegen exactly.
40#[inline(always)]
41fn read_bytes<const BYTES: u8>(data: &[u8]) -> u64 {
42    debug_assert!(data.len() >= usize::from(BYTES));
43    // Zig: const T = std.meta.Int(.unsigned, 8 * bytes); mem.readInt(T, data[0..bytes], .little)
44    // Rust cannot mint an integer type from a const generic; dispatch on the only values used.
45    match BYTES {
46        1 => u64::from(data[0]),
47        2 => u64::from(u16::from_le_bytes([data[0], data[1]])),
48        // SAFETY: `data.len() >= BYTES == 4` (asserted above; every caller
49        // proves it). `read_unaligned` imposes no alignment requirement.
50        4 => u64::from(u32::from_le(unsafe {
51            core::ptr::read_unaligned(data.as_ptr().cast::<u32>())
52        })),
53        // SAFETY: `data.len() >= BYTES == 8` (asserted above; every caller
54        // proves it). `read_unaligned` imposes no alignment requirement.
55        8 => u64::from_le(unsafe { core::ptr::read_unaligned(data.as_ptr().cast::<u64>()) }),
56        _ => unreachable!(),
57    }
58}
59
60#[inline(always)]
61fn read_8bytes_swapped(data: &[u8]) -> u64 {
62    (read_bytes::<4>(data) << 32) | read_bytes::<4>(&data[4..])
63}
64
65// `#[inline(always)]`, not just `#[inline]`: these 4 helpers (`mum` + the two
66// `mix*` wrappers, alongside the already-`always` `read_8bytes_swapped`) are the
67// entire body of the short-key (`0..=16` byte) hash that `StringHashMap` /
68// hashbrown runs per identifier / keyword / module-path key while parsing every
69// module. `#[inline]` (hint) was being declined across the bun_wyhash →
70// bun_collections / bun_js_parser crate boundary, leaving an out-of-line `mum`
71// (and a `call` per mix step) inside the hashbrown probe loop. Force the 4 mix
72// steps to fold in with no call/ret — same reasoning the surrounding `final_*`
73// helpers are `#[inline(always)]` / `#[cold]`-split for. Zig force-inlines the
74// equivalent `mum`/`mix0`/`mix1` via `@call(bun.callmod_inline, ...)`.
75#[inline(always)]
76fn mum(a: u64, b: u64) -> u64 {
77    let mut r = (a as u128) * (b as u128);
78    r = (r >> 64) ^ r;
79    r as u64
80}
81
82#[inline(always)]
83fn mix0(a: u64, b: u64, seed: u64) -> u64 {
84    mum(a ^ seed ^ PRIMES[0], b ^ seed ^ PRIMES[1])
85}
86
87#[inline(always)]
88fn mix1(a: u64, b: u64, seed: u64) -> u64 {
89    mum(a ^ seed ^ PRIMES[2], b ^ seed ^ PRIMES[3])
90}
91
92/// Cold tail of [`WyhashStateless::final_`] — the `17..=31`-byte remainder
93/// arms. Split out (and marked `#[cold] #[inline(never)]`) so the common
94/// short-key path (`0..=16`, which covers virtually every identifier/path key
95/// the parser/resolver/module-registry hash through `StringHashMap`) stays
96/// small enough to inline into the hashbrown probe loop. `key.len()` is in
97/// `17..=31`; `seed` is the running `WyhashStateless::seed`.
98#[cold]
99#[inline(never)]
100fn final_long(seed: u64, key: &[u8]) -> u64 {
101    debug_assert!((17..32).contains(&key.len()));
102
103    let head = mix0(
104        read_8bytes_swapped(key),
105        read_8bytes_swapped(&key[8..]),
106        seed,
107    );
108    let tail = match key.len() {
109        17 => mix1(read_bytes::<1>(&key[16..]), PRIMES[4], seed),
110        18 => mix1(read_bytes::<2>(&key[16..]), PRIMES[4], seed),
111        19 => mix1(
112            (read_bytes::<2>(&key[16..]) << 8) | read_bytes::<1>(&key[18..]),
113            PRIMES[4],
114            seed,
115        ),
116        20 => mix1(read_bytes::<4>(&key[16..]), PRIMES[4], seed),
117        21 => mix1(
118            (read_bytes::<4>(&key[16..]) << 8) | read_bytes::<1>(&key[20..]),
119            PRIMES[4],
120            seed,
121        ),
122        22 => mix1(
123            (read_bytes::<4>(&key[16..]) << 16) | read_bytes::<2>(&key[20..]),
124            PRIMES[4],
125            seed,
126        ),
127        23 => mix1(
128            (read_bytes::<4>(&key[16..]) << 24)
129                | (read_bytes::<2>(&key[20..]) << 8)
130                | read_bytes::<1>(&key[22..]),
131            PRIMES[4],
132            seed,
133        ),
134        24 => mix1(read_8bytes_swapped(&key[16..]), PRIMES[4], seed),
135        25 => mix1(
136            read_8bytes_swapped(&key[16..]),
137            read_bytes::<1>(&key[24..]),
138            seed,
139        ),
140        26 => mix1(
141            read_8bytes_swapped(&key[16..]),
142            read_bytes::<2>(&key[24..]),
143            seed,
144        ),
145        27 => mix1(
146            read_8bytes_swapped(&key[16..]),
147            (read_bytes::<2>(&key[24..]) << 8) | read_bytes::<1>(&key[26..]),
148            seed,
149        ),
150        28 => mix1(
151            read_8bytes_swapped(&key[16..]),
152            read_bytes::<4>(&key[24..]),
153            seed,
154        ),
155        29 => mix1(
156            read_8bytes_swapped(&key[16..]),
157            (read_bytes::<4>(&key[24..]) << 8) | read_bytes::<1>(&key[28..]),
158            seed,
159        ),
160        30 => mix1(
161            read_8bytes_swapped(&key[16..]),
162            (read_bytes::<4>(&key[24..]) << 16) | read_bytes::<2>(&key[28..]),
163            seed,
164        ),
165        31 => mix1(
166            read_8bytes_swapped(&key[16..]),
167            (read_bytes::<4>(&key[24..]) << 24)
168                | (read_bytes::<2>(&key[28..]) << 8)
169                | read_bytes::<1>(&key[30..]),
170            seed,
171        ),
172        _ => unreachable!(),
173    };
174    head ^ tail
175}
176
177// Wyhash version which does not store internal state for handling partial buffers.
178// This is needed so that we can maximize the speed for the short key case, which will
179// use the non-iterative api which the public Wyhash exposes.
180#[derive(Clone, Copy)]
181struct WyhashStateless {
182    seed: u64,
183    msg_len: usize,
184}
185
186impl WyhashStateless {
187    #[inline(always)]
188    pub(crate) fn init(seed: u64) -> WyhashStateless {
189        WyhashStateless { seed, msg_len: 0 }
190    }
191
192    #[inline(always)] // Zig: `@call(bun.callmod_inline, self.round, ...)`
193    fn round(&mut self, b: &[u8]) {
194        debug_assert!(b.len() == 32);
195
196        self.seed = mix0(
197            read_bytes::<8>(&b[0..]),
198            read_bytes::<8>(&b[8..]),
199            self.seed,
200        ) ^ mix1(
201            read_bytes::<8>(&b[16..]),
202            read_bytes::<8>(&b[24..]),
203            self.seed,
204        );
205    }
206
207    #[inline(always)] // Zig: `@call(bun.callmod_inline, c.update, ...)`
208    pub(crate) fn update(&mut self, b: &[u8]) {
209        debug_assert!(b.len().is_multiple_of(32));
210
211        let mut off: usize = 0;
212        while off < b.len() {
213            self.round(&b[off..off + 32]);
214            // @call(bun.callmod_inline, self.round, .{b[off .. off + 32]});
215            off += 32;
216        }
217
218        self.msg_len += b.len();
219    }
220
221    // `final_` is the `StringHashMap` short-key hash (every parser / resolver /
222    // module-registry probe during startup). `#[inline(always)]` alone wasn't
223    // enough — the 31-arm length switch is large enough that LLVM still emitted
224    // an out-of-line `final_` symbol and `call`ed it. Identifier/path keys are
225    // almost always <17B, so split the cold `17..=31` tail into a separate
226    // `#[cold] #[inline(never)] final_long`, leaving `final_` with just the
227    // `0..=16` arms — small enough to inline cleanly into every hashbrown probe.
228    // Zig spells the call as `@call(bun.callmod_inline, c.final, ...)`.
229    #[inline(always)]
230    pub(crate) fn final_(&mut self, b: &[u8]) -> u64 {
231        debug_assert!(b.len() < 32);
232
233        let seed = self.seed;
234        // Zig: @as(u5, @intCast(b.len)) — Rust has no u5; b.len() < 32 is asserted above.
235        let rem_len = b.len();
236        let rem_key = &b[0..rem_len];
237
238        self.seed = match rem_len {
239            0 => seed,
240            1 => mix0(read_bytes::<1>(rem_key), PRIMES[4], seed),
241            2 => mix0(read_bytes::<2>(rem_key), PRIMES[4], seed),
242            3 => mix0(
243                (read_bytes::<2>(rem_key) << 8) | read_bytes::<1>(&rem_key[2..]),
244                PRIMES[4],
245                seed,
246            ),
247            4 => mix0(read_bytes::<4>(rem_key), PRIMES[4], seed),
248            5 => mix0(
249                (read_bytes::<4>(rem_key) << 8) | read_bytes::<1>(&rem_key[4..]),
250                PRIMES[4],
251                seed,
252            ),
253            6 => mix0(
254                (read_bytes::<4>(rem_key) << 16) | read_bytes::<2>(&rem_key[4..]),
255                PRIMES[4],
256                seed,
257            ),
258            7 => mix0(
259                (read_bytes::<4>(rem_key) << 24)
260                    | (read_bytes::<2>(&rem_key[4..]) << 8)
261                    | read_bytes::<1>(&rem_key[6..]),
262                PRIMES[4],
263                seed,
264            ),
265            8 => mix0(read_8bytes_swapped(rem_key), PRIMES[4], seed),
266            9 => mix0(
267                read_8bytes_swapped(rem_key),
268                read_bytes::<1>(&rem_key[8..]),
269                seed,
270            ),
271            10 => mix0(
272                read_8bytes_swapped(rem_key),
273                read_bytes::<2>(&rem_key[8..]),
274                seed,
275            ),
276            11 => mix0(
277                read_8bytes_swapped(rem_key),
278                (read_bytes::<2>(&rem_key[8..]) << 8) | read_bytes::<1>(&rem_key[10..]),
279                seed,
280            ),
281            12 => mix0(
282                read_8bytes_swapped(rem_key),
283                read_bytes::<4>(&rem_key[8..]),
284                seed,
285            ),
286            13 => mix0(
287                read_8bytes_swapped(rem_key),
288                (read_bytes::<4>(&rem_key[8..]) << 8) | read_bytes::<1>(&rem_key[12..]),
289                seed,
290            ),
291            14 => mix0(
292                read_8bytes_swapped(rem_key),
293                (read_bytes::<4>(&rem_key[8..]) << 16) | read_bytes::<2>(&rem_key[12..]),
294                seed,
295            ),
296            15 => mix0(
297                read_8bytes_swapped(rem_key),
298                (read_bytes::<4>(&rem_key[8..]) << 24)
299                    | (read_bytes::<2>(&rem_key[12..]) << 8)
300                    | read_bytes::<1>(&rem_key[14..]),
301                seed,
302            ),
303            16 => mix0(
304                read_8bytes_swapped(rem_key),
305                read_8bytes_swapped(&rem_key[8..]),
306                seed,
307            ),
308            // Keys ≥17B are rare among identifier/path keys; keep this tail out
309            // of line so the `0..=16` arms above inline into every caller.
310            _ => final_long(seed, rem_key),
311        };
312
313        self.msg_len += b.len();
314        mum(self.seed ^ (self.msg_len as u64), PRIMES[4])
315    }
316
317    // perf on build/create-next showed `WyhashStateless::hash` out-lined as a
318    // standalone symbol (91 self-samples) and `call`ed from
319    // `find_symbol_with_record_usage` and every `StringHashMap`/hashbrown probe
320    // — `#[inline]` (hint) was being declined across the bun_wyhash →
321    // bun_js_parser/bun_collections crate boundary. Zig force-inlines the whole
322    // hash into the caller via `@call(bun.callmod_inline, ...)`; match that.
323    #[inline(always)]
324    pub(crate) fn hash(seed: u64, input: &[u8]) -> u64 {
325        let aligned_len = input.len() - (input.len() % 32);
326
327        let mut c = WyhashStateless::init(seed);
328        c.update(&input[0..aligned_len]);
329        // @call(bun.callmod_inline, c.update, .{input[0..aligned_len]});
330        c.final_(&input[aligned_len..])
331        // return @call(bun.callmod_inline, c.final, .{input[aligned_len..]});
332    }
333}
334
335/// Fast non-cryptographic 64bit hash function.
336/// See https://github.com/wangyi-fudan/wyhash
337pub struct Wyhash11 {
338    state: WyhashStateless,
339
340    buf: [u8; 32],
341    buf_len: usize,
342}
343
344impl Wyhash11 {
345    #[inline]
346    pub fn init(seed: u64) -> Wyhash11 {
347        Wyhash11 {
348            state: WyhashStateless::init(seed),
349            buf: [0; 32], // Zig: undefined
350            buf_len: 0,
351        }
352    }
353
354    #[inline]
355    pub fn update(&mut self, b: &[u8]) {
356        let mut off: usize = 0;
357
358        if self.buf_len != 0 && self.buf_len + b.len() >= 32 {
359            off += 32 - self.buf_len;
360            self.buf[self.buf_len..self.buf_len + off].copy_from_slice(&b[0..off]);
361            self.state.update(&self.buf[0..]);
362            self.buf_len = 0;
363        }
364
365        let remain_len = b.len() - off;
366        let aligned_len = remain_len - (remain_len % 32);
367        self.state.update(&b[off..off + aligned_len]);
368
369        let tail = &b[off + aligned_len..];
370        self.buf[self.buf_len..self.buf_len + tail.len()].copy_from_slice(tail);
371        self.buf_len += usize::from(u8::try_from(tail.len()).expect("int cast"));
372    }
373
374    // Force-inline so no out-of-line copy of `WyhashStateless::final_`'s
375    // length-switch survives on the `Hasher`-driven streaming path.
376    #[inline(always)]
377    pub fn final_(&mut self) -> u64 {
378        let rem_key = &self.buf[0..self.buf_len];
379
380        self.state.final_(rem_key)
381    }
382
383    #[inline(always)]
384    pub fn hash(seed: u64, input: &[u8]) -> u64 {
385        WyhashStateless::hash(seed, input)
386    }
387}
388
389// Allow `Wyhash11` to be used with `core::hash::Hash::hash` (e.g., as the
390// state for std/HashMap-style hashing). Mirrors how Zig's `std.hash_map`
391// AutoContext drives a Wyhash state.
392impl core::hash::Hasher for Wyhash11 {
393    #[inline]
394    fn write(&mut self, bytes: &[u8]) {
395        self.update(bytes);
396    }
397    #[inline]
398    fn finish(&self) -> u64 {
399        // `final_` mutates `state`; clone so `Hasher::finish(&self)` stays
400        // semantically pure (matches std contract).
401        let mut s = self.state;
402        s.final_(&self.buf[0..self.buf_len])
403    }
404}
405
406// ════════════════════════════════════════════════════════════════════════════
407// Wyhash (std.hash.Wyhash — final4 variant, 48-byte rounds, 4 secrets)
408// Ported from vendor/zig/lib/std/hash/wyhash.zig
409// ════════════════════════════════════════════════════════════════════════════
410
411/// `std.hash.Wyhash` — the wyhash final4 variant. Used by `bun.hash()`,
412/// `RuntimeTranspilerCache`, the router, and Zig's std `HashMap` autocontext.
413///
414/// NOT interchangeable with [`Wyhash11`].
415#[derive(Clone, Copy)]
416pub struct Wyhash {
417    a: u64,
418    b: u64,
419    state: [u64; 3],
420    total_len: usize,
421
422    buf: [u8; 48],
423    buf_len: usize,
424}
425
426impl Wyhash {
427    const SECRET: [u64; 4] = [
428        0xa0761d6478bd642f,
429        0xe7037ed1a0b428db,
430        0x8ebc6af09c88c6e3,
431        0x589965cc75374cc3,
432    ];
433
434    #[inline]
435    pub fn init(seed: u64) -> Wyhash {
436        let s0 = seed ^ Self::mix(seed ^ Self::SECRET[0], Self::SECRET[1]);
437        Wyhash {
438            a: 0, // Zig: undefined
439            b: 0, // Zig: undefined
440            state: [s0, s0, s0],
441            total_len: 0,
442            buf: [0; 48], // Zig: undefined
443            buf_len: 0,
444        }
445    }
446
447    // This is subtly different from other hash function update calls. Wyhash requires the last
448    // full 48-byte block to be run through final1 if is exactly aligned to 48-bytes.
449    #[inline]
450    pub fn update(&mut self, input: &[u8]) {
451        self.total_len += input.len();
452
453        if input.len() <= 48 - self.buf_len {
454            self.buf[self.buf_len..self.buf_len + input.len()].copy_from_slice(input);
455            self.buf_len += input.len();
456            return;
457        }
458
459        let mut i: usize = 0;
460
461        if self.buf_len > 0 {
462            i = 48 - self.buf_len;
463            self.buf[self.buf_len..48].copy_from_slice(&input[0..i]);
464            let buf = self.buf;
465            self.round(&buf);
466            self.buf_len = 0;
467        }
468
469        while i + 48 < input.len() {
470            self.round(
471                input[i..i + 48]
472                    .try_into()
473                    .expect("infallible: size matches"),
474            );
475            i += 48;
476        }
477
478        let remaining_bytes = &input[i..];
479        if remaining_bytes.len() < 16 && i >= 48 {
480            let rem = 16 - remaining_bytes.len();
481            // self.buf[self.buf.len - rem ..] = input[i - rem .. i]
482            self.buf[48 - rem..48].copy_from_slice(&input[i - rem..i]);
483        }
484        self.buf[0..remaining_bytes.len()].copy_from_slice(remaining_bytes);
485        self.buf_len = remaining_bytes.len();
486    }
487
488    #[inline(always)]
489    pub fn final_(&self) -> u64 {
490        let input: &[u8] = &self.buf[0..self.buf_len];
491        let mut new_self = self.shallow_copy(); // ensure idempotency
492
493        if self.total_len <= 16 {
494            new_self.small_key(input);
495        } else {
496            let mut scratch: [u8; 16] = [0; 16];
497            let (input, offset) = if self.buf_len < 16 {
498                let rem = 16 - self.buf_len;
499                scratch[0..rem].copy_from_slice(&self.buf[48 - rem..48]);
500                scratch[rem..rem + self.buf_len].copy_from_slice(&self.buf[0..self.buf_len]);
501                // Same as input but with additional bytes preceding start in case of a short buffer
502                (&scratch[..], rem)
503            } else {
504                (input, 0usize)
505            };
506
507            new_self.final0();
508            new_self.final1(input, offset);
509        }
510
511        new_self.final2()
512    }
513
514    // Copies the core wyhash state but not any internal buffers.
515    #[inline]
516    fn shallow_copy(&self) -> Wyhash {
517        Wyhash {
518            a: self.a,
519            b: self.b,
520            state: self.state,
521            total_len: self.total_len,
522            buf: [0; 48], // Zig: undefined
523            buf_len: 0,   // Zig: undefined
524        }
525    }
526
527    #[inline(always)] // Zig: `inline fn smallKey`
528    fn small_key(&mut self, input: &[u8]) {
529        debug_assert!(input.len() <= 16);
530
531        if input.len() >= 4 {
532            let end = input.len() - 4;
533            let quarter = (input.len() >> 3) << 2;
534            self.a = (Self::read4(&input[0..]) << 32) | Self::read4(&input[quarter..]);
535            self.b = (Self::read4(&input[end..]) << 32) | Self::read4(&input[end - quarter..]);
536        } else if !input.is_empty() {
537            self.a = (u64::from(input[0]) << 16)
538                | (u64::from(input[input.len() >> 1]) << 8)
539                | u64::from(input[input.len() - 1]);
540            self.b = 0;
541        } else {
542            self.a = 0;
543            self.b = 0;
544        }
545    }
546
547    #[inline]
548    fn round(&mut self, input: &[u8; 48]) {
549        // Zig: inline for (0..3) |i| — manually unrolled.
550        let a0 = Self::read8(&input[0..]);
551        let b0 = Self::read8(&input[8..]);
552        self.state[0] = Self::mix(a0 ^ Self::SECRET[1], b0 ^ self.state[0]);
553
554        let a1 = Self::read8(&input[16..]);
555        let b1 = Self::read8(&input[24..]);
556        self.state[1] = Self::mix(a1 ^ Self::SECRET[2], b1 ^ self.state[1]);
557
558        let a2 = Self::read8(&input[32..]);
559        let b2 = Self::read8(&input[40..]);
560        self.state[2] = Self::mix(a2 ^ Self::SECRET[3], b2 ^ self.state[2]);
561    }
562
563    // Zig: `inline fn read(comptime T, data) { mem.readInt(T, data[0..@sizeOf(T)], .little) }`
564    // — a single unaligned load. The previous per-byte `[data[0], data[1], ...]`
565    // spelling left a cmp/je-to-panic ladder per byte in the `small_key` disasm
566    // (8 bounds checks for the 4× `read4` at len∈[4,16]). All call sites slice
567    // from a buffer whose length is already proven ≥4/≥8 by the enclosing
568    // branch (`small_key`: `len >= 4`; `round`: `&[u8; 48]`; `final1`:
569    // `i + 16 < len` / `len - 16` / `len - 8`), so match Zig's codegen exactly.
570    #[inline(always)]
571    fn read4(data: &[u8]) -> u64 {
572        debug_assert!(data.len() >= 4);
573        // SAFETY: every caller passes a slice with ≥4 bytes (see comment above);
574        // `read_unaligned` imposes no alignment requirement.
575        u64::from(u32::from_le(unsafe {
576            core::ptr::read_unaligned(data.as_ptr().cast::<u32>())
577        }))
578    }
579
580    #[inline(always)]
581    fn read8(data: &[u8]) -> u64 {
582        debug_assert!(data.len() >= 8);
583        // SAFETY: every caller passes a slice with ≥8 bytes (see comment above);
584        // `read_unaligned` imposes no alignment requirement.
585        u64::from_le(unsafe { core::ptr::read_unaligned(data.as_ptr().cast::<u64>()) })
586    }
587
588    #[inline]
589    fn mum_(a: &mut u64, b: &mut u64) {
590        let x = (*a as u128).wrapping_mul(*b as u128);
591        *a = x as u64; // @truncate
592        *b = (x >> 64) as u64; // @truncate
593    }
594
595    #[inline]
596    fn mix(a_: u64, b_: u64) -> u64 {
597        let mut a = a_;
598        let mut b = b_;
599        Self::mum_(&mut a, &mut b);
600        a ^ b
601    }
602
603    #[inline]
604    fn final0(&mut self) {
605        self.state[0] ^= self.state[1] ^ self.state[2];
606    }
607
608    // input_lb must be at least 16-bytes long (in shorter key cases the small_key function will be
609    // used instead). We use an index into a slice for comptime processing as opposed to if we
610    // used pointers.
611    #[inline]
612    fn final1(&mut self, input_lb: &[u8], start_pos: usize) {
613        debug_assert!(input_lb.len() >= 16);
614        debug_assert!(input_lb.len() - start_pos <= 48);
615        let input = &input_lb[start_pos..];
616
617        let mut i: usize = 0;
618        while i + 16 < input.len() {
619            self.state[0] = Self::mix(
620                Self::read8(&input[i..]) ^ Self::SECRET[1],
621                Self::read8(&input[i + 8..]) ^ self.state[0],
622            );
623            i += 16;
624        }
625
626        self.a = Self::read8(&input_lb[input_lb.len() - 16..]);
627        self.b = Self::read8(&input_lb[input_lb.len() - 8..]);
628    }
629
630    #[inline(always)] // Zig: `inline fn final2`
631    fn final2(&mut self) -> u64 {
632        self.a ^= Self::SECRET[1];
633        self.b ^= self.state[0];
634        Self::mum_(&mut self.a, &mut self.b);
635        Self::mix(
636            self.a ^ Self::SECRET[0] ^ (self.total_len as u64),
637            self.b ^ Self::SECRET[1],
638        )
639    }
640
641    // perf on build/create-next showed `Wyhash::hash` out-lined and `call`ed
642    // from every wyhash-backed hashbrown probe; Zig's `std.hash.Wyhash.hash`
643    // inlines fully into its caller. `#[inline]` (hint) is declined across the
644    // crate boundary for this body size — force it.
645    #[inline(always)]
646    pub fn hash(seed: u64, input: &[u8]) -> u64 {
647        let mut this = Wyhash::init(seed);
648
649        if input.len() <= 16 {
650            this.small_key(input);
651        } else {
652            let mut i: usize = 0;
653            if input.len() >= 48 {
654                // Hot path for `Bun.hash` over large buffers. Zig spells the
655                // body as `inline fn round` calling `inline fn read`/`mix`/
656                // `mum`, all of which are *force*-inlined; Rust's `#[inline]`
657                // is a hint that opt-level 0 ignores, so the helper-function
658                // shape costs ~12 callee frames per 48-byte block. Instead,
659                // hoist state into locals and open-code one round per
660                // iteration (3× 128-bit multiply, 6× le-u64 read).
661                let [mut s0, mut s1, mut s2] = this.state;
662                let (k1, k2, k3) = (Self::SECRET[1], Self::SECRET[2], Self::SECRET[3]);
663                // Six little-endian u64 reads per round. Zig's
664                // `std.mem.readInt(u64, p, .little)` is a single unaligned
665                // load (`@bitCast` of `*const [8]u8`). The previous safe-Rust
666                // spellings here — `input[i..i+48].try_into()` plus per-byte
667                // shift-or — left a slice bounds-check + panic edge per
668                // iteration and 48 scalar byte loads per round, which made
669                // `Wyhash::hash` the #1 self-time symbol when
670                // `RuntimeTranspilerCache` hashes multi-MB sources. Match the
671                // Zig codegen exactly: take the base pointer once and issue
672                // six `read_unaligned::<u64>` per round.
673                //
674                // `i + 48 < len` ⇔ `i < len - 48`; len ≥ 48 is guaranteed
675                // by the enclosing branch, so the subtraction is safe and we
676                // skip a per-iteration overflow check on the addition.
677                let bound = input.len() - 48;
678                let p = input.as_ptr();
679                while i < bound {
680                    macro_rules! r8 {
681                        ($o:literal) => {
682                            // SAFETY: loop invariant `i < len - 48` ⇒ `i + 48 ≤ len`,
683                            // so every `p.add(i + off)` for off ∈ {0,8,16,24,32,40}
684                            // addresses an 8-byte window wholly inside `input`.
685                            // `read_unaligned` imposes no alignment requirement.
686                            u64::from_le(unsafe {
687                                core::ptr::read_unaligned(p.add(i + $o).cast::<u64>())
688                            })
689                        };
690                    }
691                    // u64×u64 → u128 cannot overflow; `wrapping_mul` skips
692                    // the debug-mode overflow check that plain `*` emits.
693                    let m0 = ((r8!(0) ^ k1) as u128).wrapping_mul((r8!(8) ^ s0) as u128);
694                    s0 = (m0 as u64) ^ ((m0 >> 64) as u64);
695                    let m1 = ((r8!(16) ^ k2) as u128).wrapping_mul((r8!(24) ^ s1) as u128);
696                    s1 = (m1 as u64) ^ ((m1 >> 64) as u64);
697                    let m2 = ((r8!(32) ^ k3) as u128).wrapping_mul((r8!(40) ^ s2) as u128);
698                    s2 = (m2 as u64) ^ ((m2 >> 64) as u64);
699                    i += 48;
700                }
701                this.state = [s0, s1, s2];
702                this.final0();
703            }
704            this.final1(input, i);
705        }
706
707        this.total_len = input.len();
708        this.final2()
709    }
710}
711
712// Allow `Wyhash` to be used with `core::hash::Hash::hash` (e.g., as the state
713// for std/HashMap-style hashing). Mirrors Zig's `std.hash_map` AutoContext.
714impl core::hash::Hasher for Wyhash {
715    #[inline]
716    fn write(&mut self, bytes: &[u8]) {
717        self.update(bytes);
718    }
719    #[inline]
720    fn finish(&self) -> u64 {
721        // `final_` already operates on a shallow copy → idempotent on `&self`.
722        self.final_()
723    }
724}
725
726impl Default for Wyhash {
727    #[inline]
728    fn default() -> Self {
729        Wyhash::init(0)
730    }
731}
732
733/// One-shot `Hasher` for `std::collections::HashMap`. Unlike [`Wyhash`]'s
734/// streaming `Hasher` impl (which buffers into a 48-byte scratch and re-zeroes
735/// it on every `init`/`shallow_copy` — a measurable zero-fill cost the Zig
736/// `= undefined` avoids), this folds each `write` through the stateless
737/// one-shot path seeded by the running hash. Matches Zig's
738/// `std.hash_map.getAutoHashFn` shape: no buffering, no per-key allocation.
739///
740/// Hash values are deterministic across runs (seed 0) but are NOT
741/// bit-identical to Zig's `Wyhash.hash(0, key)` because Rust's `Hash for [T]`
742/// prepends a length prefix; that's fine — these are in-memory tables only
743/// (lockfile/on-disk hashes go through [`Wyhash11`] / [`hash`] directly).
744#[derive(Default, Clone, Copy)]
745pub struct OneShotHasher {
746    hash: u64,
747}
748
749impl core::hash::Hasher for OneShotHasher {
750    // perf on build/create-next: `OneShotHasher::write` showed up as a
751    // standalone 58-self-sample symbol `call`ed from hashbrown's probe loop —
752    // `#[inline]` was being declined across the bun_wyhash → bun_collections
753    // crate boundary. Force-inline so the whole hash collapses into the caller
754    // (matches Zig's `getAutoHashFn`, which is fully comptime-inlined).
755    #[inline(always)]
756    fn write(&mut self, bytes: &[u8]) {
757        // Each chunk re-seeds from the previous output, so multi-`write` keys
758        // (e.g. `(&[u8], u32)`) still mix without buffering. For the
759        // overwhelmingly common single-`write` case this is one stateless
760        // wyhash over the slice.
761        self.hash = Wyhash11::hash(self.hash, bytes);
762    }
763    #[inline(always)]
764    fn write_u8(&mut self, n: u8) {
765        self.write_u64(u64::from(n));
766    }
767    #[inline(always)]
768    fn write_u16(&mut self, n: u16) {
769        self.write_u64(u64::from(n));
770    }
771    #[inline(always)]
772    fn write_u32(&mut self, n: u32) {
773        self.write_u64(u64::from(n));
774    }
775    #[inline(always)]
776    fn write_u64(&mut self, n: u64) {
777        // Cheap diffusion for integer keys — one 128-bit multiply, same
778        // primitive wyhash uses internally (`mum`).
779        self.hash = mum(self.hash ^ n, PRIMES[4]);
780    }
781    /// No-op (nightly only): keys hashed through this hasher are never
782    /// cross-type, so the prefix-freedom guarantee `<[T] as Hash>` buys is
783    /// unused. Skipping it makes `<[u8] as Hash>` collapse to a single
784    /// `write(bytes)` → `Wyhash11::hash(0, bytes)` — the exact shape of Zig's
785    /// `bun.StringHashMapContext.hash`. perf showed `hashbrown::make_hash`
786    /// outlined with the extra `mum` from the length prefix as dead weight.
787    /// On stable the override is absent and `Hasher`'s provided default
788    /// (`write_usize`) applies — see the crate-root dual-mode note.
789    #[cfg(bao_nightly)]
790    #[inline(always)]
791    fn write_length_prefix(&mut self, _len: usize) {}
792    #[inline(always)]
793    fn write_usize(&mut self, n: usize) {
794        self.write_u64(n as u64);
795    }
796    #[inline(always)]
797    fn finish(&self) -> u64 {
798        self.hash
799    }
800}
801
802/// Hash any `K: Hash` through [`OneShotHasher`] — the `Hash` → wyhash thunk
803/// shared by both auto-context map types. See the [`OneShotHasher`] doc-comment
804/// for the length-prefix divergence from Zig's `Wyhash.hash(0, asBytes(&k))`.
805#[inline]
806pub fn auto_hash<K: core::hash::Hash + ?Sized>(key: &K) -> u64 {
807    let mut h = OneShotHasher::default();
808    key.hash(&mut h);
809    core::hash::Hasher::finish(&h)
810}
811
812/// `BuildHasher` for `std::collections::HashMap` so containers can opt out of
813/// SipHash. Deterministic across runs and ~3-5× faster than `RandomState` on
814/// the short identifier keys the parser/printer/renamer churn.
815pub type BuildHasher = core::hash::BuildHasherDefault<OneShotHasher>;
816
817/// `bun.hash(bytes)` — `std.hash.Wyhash` with seed 0.
818#[inline]
819pub fn hash(bytes: &[u8]) -> u64 {
820    Wyhash::hash(0, bytes)
821}
822
823#[inline]
824pub fn hash32(bytes: &[u8]) -> u32 {
825    hash(bytes) as u32 // @truncate
826}
827
828/// `bun.hashWithSeed(seed, bytes)` — `std.hash.Wyhash` with explicit seed.
829#[inline]
830pub fn hash_with_seed(seed: u64, bytes: &[u8]) -> u64 {
831    Wyhash::hash(seed, bytes)
832}
833
834/// `std.hash.Wyhash` over the ASCII-lowercased view of `bytes`, streamed
835/// through a 48-byte stack scratch so no heap allocation occurs regardless of
836/// input length. ASCII-only (`b'A'..=b'Z' → b'a'..=b'z'`); non-ASCII bytes
837/// pass through unchanged.
838///
839/// Chunk size and "copy unconditionally" vs "borrow if already lowercase" are
840/// output-irrelevant — streaming Wyhash is chunk-invariant and the bytes fed
841/// to the hasher are identical either way — so this collapses the three
842/// open-coded copies in `http::hash_header_name`,
843/// `s3_signing::S3Credentials::hash_const`, and
844/// `collections::CaseInsensitiveAsciiStringContext::hash_bytes` (Zig has the
845/// same triplication: http.zig:828, credentials.zig:23, bun.zig:1011).
846#[inline]
847pub fn hash_ascii_lowercase(seed: u64, bytes: &[u8]) -> u64 {
848    let mut buf = [0u8; 48];
849    if bytes.len() <= buf.len() {
850        // Fast path: one-shot hash on the lowered copy — skips the streaming
851        // `Wyhash` state's 48-byte buf zero-fill + `final_` shallow-copy.
852        let dst = &mut buf[..bytes.len()];
853        for (d, &s) in dst.iter_mut().zip(bytes) {
854            *d = s.to_ascii_lowercase();
855        }
856        return Wyhash::hash(seed, dst);
857    }
858    let mut h = Wyhash::init(seed);
859    let mut remain = bytes;
860    while !remain.is_empty() {
861        let n = remain.len().min(buf.len());
862        let dst = &mut buf[..n];
863        for (d, &s) in dst.iter_mut().zip(&remain[..n]) {
864            *d = s.to_ascii_lowercase();
865        }
866        h.update(dst);
867        remain = &remain[n..];
868    }
869    h.final_()
870}
871
872/// `std.hash.Wyhash.hash(seed, input)` as a `const fn`.
873///
874/// This is a parallel one-shot port of [`Wyhash::hash`] for compile-time
875/// evaluation (e.g. `generated_symbol_name!` in `js_parser`, which must match
876/// Zig's `comptime std.hash.Wyhash.hash(0, name)` byte-for-byte). The runtime
877/// [`Wyhash::hash`] is intentionally NOT `const fn` — its perf-tuned hot loop
878/// uses `slice.try_into()` (trait call) and the streaming API uses
879/// `copy_from_slice`, neither of which is const-compatible. Keep the two in
880/// lock-step; `tests::hash_const_matches_runtime` guards drift.
881pub const fn hash_const(seed: u64, input: &[u8]) -> u64 {
882    // ── std.hash.Wyhash (final4 variant) — const-fn re-port. ──
883    const SECRET: [u64; 4] = Wyhash::SECRET;
884
885    #[inline]
886    const fn mix(a: u64, b: u64) -> u64 {
887        let x = (a as u128).wrapping_mul(b as u128);
888        (x as u64) ^ ((x >> 64) as u64)
889    }
890    #[inline]
891    const fn read4(d: &[u8], o: usize) -> u64 {
892        u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]) as u64
893    }
894    #[inline]
895    const fn read8(d: &[u8], o: usize) -> u64 {
896        u64::from_le_bytes([
897            d[o],
898            d[o + 1],
899            d[o + 2],
900            d[o + 3],
901            d[o + 4],
902            d[o + 5],
903            d[o + 6],
904            d[o + 7],
905        ])
906    }
907
908    let s0 = seed ^ mix(seed ^ SECRET[0], SECRET[1]);
909    let mut state = [s0, s0, s0];
910    let len = input.len();
911    let a: u64;
912    let b: u64;
913
914    if len <= 16 {
915        // small_key
916        if len >= 4 {
917            let end = len - 4;
918            let quarter = (len >> 3) << 2;
919            a = (read4(input, 0) << 32) | read4(input, quarter);
920            b = (read4(input, end) << 32) | read4(input, end - quarter);
921        } else if len > 0 {
922            a = ((input[0] as u64) << 16)
923                | ((input[len >> 1] as u64) << 8)
924                | (input[len - 1] as u64);
925            b = 0;
926        } else {
927            a = 0;
928            b = 0;
929        }
930    } else {
931        let mut i: usize = 0;
932        if len >= 48 {
933            while i + 48 < len {
934                // round
935                state[0] = mix(read8(input, i) ^ SECRET[1], read8(input, i + 8) ^ state[0]);
936                state[1] = mix(
937                    read8(input, i + 16) ^ SECRET[2],
938                    read8(input, i + 24) ^ state[1],
939                );
940                state[2] = mix(
941                    read8(input, i + 32) ^ SECRET[3],
942                    read8(input, i + 40) ^ state[2],
943                );
944                i += 48;
945            }
946            // final0
947            state[0] ^= state[1] ^ state[2];
948        }
949        // final1
950        let mut j = i;
951        while j + 16 < len {
952            state[0] = mix(read8(input, j) ^ SECRET[1], read8(input, j + 8) ^ state[0]);
953            j += 16;
954        }
955        a = read8(input, len - 16);
956        b = read8(input, len - 8);
957    }
958
959    // final2 (mum_ inlined)
960    let x = ((a ^ SECRET[1]) as u128).wrapping_mul((b ^ state[0]) as u128);
961    mix(
962        (x as u64) ^ SECRET[0] ^ (len as u64),
963        ((x >> 64) as u64) ^ SECRET[1],
964    )
965}
966
967/// `std.hash.int` — integer-to-integer hashing (same width in, same width out).
968/// Zig's version is `anytype`-generic; we cover the dedicated widths (16/32/64)
969/// via a sealed trait. All current callers pass `u32`.
970#[inline]
971pub fn hash_int<T: HashInt>(input: T) -> T {
972    T::hash_int(input)
973}
974
975pub trait HashInt: Copy {
976    fn hash_int(self) -> Self;
977}
978
979// Source: https://github.com/skeeto/hash-prospector
980impl HashInt for u16 {
981    #[inline]
982    fn hash_int(self) -> u16 {
983        let mut x = self;
984        x = (x ^ (x >> 7)).wrapping_mul(0x2993);
985        x = (x ^ (x >> 5)).wrapping_mul(0xe877);
986        x = (x ^ (x >> 9)).wrapping_mul(0x0235);
987        x ^ (x >> 10)
988    }
989}
990
991// Source: https://github.com/skeeto/hash-prospector
992impl HashInt for u32 {
993    #[inline]
994    fn hash_int(self) -> u32 {
995        let mut x = self;
996        x = (x ^ (x >> 17)).wrapping_mul(0xed5a_d4bb);
997        x = (x ^ (x >> 11)).wrapping_mul(0xac4c_1b51);
998        x = (x ^ (x >> 15)).wrapping_mul(0x3184_8bab);
999        x ^ (x >> 14)
1000    }
1001}
1002
1003// Source: https://github.com/jonmaiga/mx3
1004impl HashInt for u64 {
1005    #[inline]
1006    fn hash_int(self) -> u64 {
1007        const C: u64 = 0xbea2_25f9_eb34_556d;
1008        let mut x = self;
1009        x = (x ^ (x >> 32)).wrapping_mul(C);
1010        x = (x ^ (x >> 29)).wrapping_mul(C);
1011        x = (x ^ (x >> 32)).wrapping_mul(C);
1012        x ^ (x >> 29)
1013    }
1014}
1015
1016// ════════════════════════════════════════════════════════════════════════════
1017// Tests
1018// ════════════════════════════════════════════════════════════════════════════
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023
1024    // Run https://github.com/wangyi-fudan/wyhash/blob/77e50f267fbc7b8e2d09f2d455219adb70ad4749/test_vector.cpp directly.
1025    struct TestVector {
1026        seed: u64,
1027        expected: u64,
1028        input: &'static [u8],
1029    }
1030
1031    const VECTORS: &[TestVector] = &[
1032        TestVector {
1033            seed: 0,
1034            expected: 0x0409638ee2bde459,
1035            input: b"",
1036        },
1037        TestVector {
1038            seed: 1,
1039            expected: 0xa8412d091b5fe0a9,
1040            input: b"a",
1041        },
1042        TestVector {
1043            seed: 2,
1044            expected: 0x32dd92e4b2915153,
1045            input: b"abc",
1046        },
1047        TestVector {
1048            seed: 3,
1049            expected: 0x8619124089a3a16b,
1050            input: b"message digest",
1051        },
1052        TestVector {
1053            seed: 4,
1054            expected: 0x7a43afb61d7f5f40,
1055            input: b"abcdefghijklmnopqrstuvwxyz",
1056        },
1057        TestVector {
1058            seed: 5,
1059            expected: 0xff42329b90e50d58,
1060            input: b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
1061        },
1062        TestVector {
1063            seed: 6,
1064            expected: 0xc39cab13b115aad3,
1065            input:
1066                b"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
1067        },
1068    ];
1069
1070    #[test]
1071    fn test_vectors() {
1072        for e in VECTORS {
1073            assert_eq!(
1074                e.expected,
1075                Wyhash::hash(e.seed, e.input),
1076                "input={:?}",
1077                bstr::BStr::new(e.input)
1078            );
1079        }
1080    }
1081
1082    // Returns a verification code, the same as used by SMHasher.
1083    //
1084    // Hash keys of the form {0}, {0,1}, {0,1,2}... up to N=255, using 256-N as seed.
1085    // First four-bytes of the hash, interpreted as little-endian is the verification code.
1086    fn smhasher(hash_fn: impl Fn(u64, &[u8]) -> u64) -> u32 {
1087        const HASH_SIZE: usize = core::mem::size_of::<u64>();
1088        let mut buf = [0u8; 256];
1089        let mut buf_all = [0u8; 256 * HASH_SIZE];
1090
1091        for i in 0..256usize {
1092            buf[i] = i as u8;
1093            let h = hash_fn((256 - i) as u64, &buf[0..i]);
1094            buf_all[i * HASH_SIZE..(i + 1) * HASH_SIZE].copy_from_slice(&h.to_le_bytes());
1095        }
1096
1097        hash_fn(0, &buf_all[..]) as u32
1098    }
1099
1100    #[test]
1101    fn test_smhasher() {
1102        assert_eq!(smhasher(Wyhash::hash), 0xBD5E840C);
1103    }
1104
1105    #[test]
1106    fn hash_const_matches_runtime() {
1107        // `hash_const` is a parallel const-fn re-port of `Wyhash::hash`; it
1108        // produces compile-time symbol suffixes that must match Zig's
1109        // `comptime std.hash.Wyhash.hash(0, name)` byte-for-byte. Guard drift.
1110        for s in [
1111            &b""[..],
1112            b"a",
1113            b"abc",
1114            b"__require",
1115            b"0123456789abcdef0",
1116            b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
1117        ] {
1118            assert_eq!(hash_const(0, s), Wyhash::hash(0, s), "input: {s:?}");
1119        }
1120        // Also exercise the seeded init path + SMHasher's full range.
1121        assert_eq!(smhasher(hash_const), 0xBD5E840C);
1122    }
1123
1124    #[test]
1125    fn test_iterative_api() {
1126        // Sum(1..32) = 528
1127        let buf = [0u8; 528];
1128        let mut len: usize = 0;
1129        let seed = 0;
1130
1131        let mut hasher = Wyhash::init(seed);
1132        for i in 1..32usize {
1133            let r = Wyhash::hash(seed, &buf[0..len + i]);
1134            hasher.update(&buf[len..len + i]);
1135            let f1 = hasher.final_();
1136            let f2 = hasher.final_();
1137            assert_eq!(f1, f2, "iterative hash was not idempotent at i={i}");
1138            assert_eq!(f1, r, "iterative hash did not match direct at i={i}");
1139            len += i;
1140        }
1141    }
1142
1143    #[test]
1144    fn test_iterative_maintains_last_sixteen() {
1145        // "Z" ** 48 ++ "01234567890abcdefg"
1146        let mut input = [0u8; 48 + 18];
1147        for b in &mut input[..48] {
1148            *b = b'Z';
1149        }
1150        input[48..].copy_from_slice(b"01234567890abcdefg");
1151        let seed = 0;
1152
1153        for i in 0..17usize {
1154            let payload = &input[0..input.len() - i];
1155            let non_iterative_hash = Wyhash::hash(seed, payload);
1156
1157            let mut wh = Wyhash::init(seed);
1158            wh.update(payload);
1159            let iterative_hash = wh.final_();
1160
1161            assert_eq!(non_iterative_hash, iterative_hash, "i={i}");
1162        }
1163    }
1164
1165    #[test]
1166    fn test_iterative_chunked_matches_oneshot() {
1167        // Exercise the buf-carryover paths in update() across many split points.
1168        let mut data = [0u8; 200];
1169        for (i, b) in data.iter_mut().enumerate() {
1170            *b = (i as u8).wrapping_mul(31).wrapping_add(7);
1171        }
1172        for total in [0usize, 1, 3, 15, 16, 17, 47, 48, 49, 95, 96, 97, 150, 200] {
1173            let direct = Wyhash::hash(42, &data[..total]);
1174            for first in 0..=total {
1175                let mut h = Wyhash::init(42);
1176                h.update(&data[..first]);
1177                h.update(&data[first..total]);
1178                assert_eq!(direct, h.final_(), "total={total} first={first}");
1179            }
1180        }
1181    }
1182
1183    #[test]
1184    fn test_hash_ascii_lowercase() {
1185        // Chunked streaming path must equal one-shot over the fully-lowered
1186        // buffer (guards the "chunk-invariant" claim in the doc comment), and
1187        // mixed-case inputs must hash equal to their lowercase form.
1188        let mut data = [0u8; 200];
1189        for (i, b) in data.iter_mut().enumerate() {
1190            *b = b'A' + (i as u8 % 58); // mix of A..Z, punct, a..z
1191        }
1192        for total in [0usize, 1, 16, 47, 48, 49, 95, 96, 97, 200] {
1193            let lowered: Vec<u8> = data[..total].iter().map(u8::to_ascii_lowercase).collect();
1194            assert_eq!(
1195                hash_ascii_lowercase(0, &data[..total]),
1196                Wyhash::hash(0, &lowered),
1197                "total={total}"
1198            );
1199            assert_eq!(
1200                hash_ascii_lowercase(0, &data[..total]),
1201                hash_ascii_lowercase(0, &lowered),
1202                "case-fold total={total}"
1203            );
1204        }
1205    }
1206
1207    #[test]
1208    fn test_wyhash_is_not_wyhash11() {
1209        // Guard against the historical mix-up: these are different algorithms.
1210        assert_ne!(Wyhash::hash(0, b"abc"), Wyhash11::hash(0, b"abc"));
1211    }
1212}
1213
1214// ported from: src/wyhash/wyhash.zig