Skip to main content

gglib_core/domain/agent/
fnv1a.rs

1//! FNV-1a 64-bit hash utility backing the loop and stagnation detectors.
2//!
3//! # Why inline rather than using the `fnv` crate?
4//!
5//! The full implementation is 10 lines with no unsafe code and no trait
6//! machinery.  Vendoring it keeps the `gglib-core` dependency graph minimal
7//! and avoids pulling in an additional crate for a single trivial function.
8
9// =============================================================================
10// FNV-1a 64-bit hash
11// =============================================================================
12
13/// FNV-1a 64-bit hash of `s`.
14///
15/// Parameters:
16/// - Offset basis: `14_695_981_039_346_656_037`
17/// - Prime: `1_099_511_628_211`
18/// - Wrapping 64-bit multiplication
19///
20/// The Rust implementation hashes UTF-8 bytes.
21pub(crate) fn fnv1a_64(s: &str) -> u64 {
22    const OFFSET: u64 = 14_695_981_039_346_656_037;
23    const PRIME: u64 = 1_099_511_628_211;
24    let mut hash = OFFSET;
25    for byte in s.bytes() {
26        hash ^= u64::from(byte);
27        hash = hash.wrapping_mul(PRIME);
28    }
29    hash
30}
31
32// =============================================================================
33// Tests
34// =============================================================================
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn fnv1a_64_empty_string_is_offset_basis() {
42        // FNV-1a of "" is the offset basis unchanged.
43        assert_eq!(fnv1a_64(""), 14_695_981_039_346_656_037);
44    }
45
46    #[test]
47    fn fnv1a_64_is_deterministic_and_distinct() {
48        // Hashing the same string twice must yield the same value.
49        assert_eq!(fnv1a_64("hello"), fnv1a_64("hello"));
50        // Different inputs must produce different hashes.
51        assert_ne!(fnv1a_64("hello"), fnv1a_64("world"));
52    }
53
54    #[test]
55    fn fnv1a_64_differs_from_32_bit_basis() {
56        // Sanity check: the 64-bit offset basis is different from the 32-bit one.
57        #[allow(clippy::cast_possible_truncation)]
58        let low32 = fnv1a_64("") as u32;
59        assert_ne!(low32, 2_166_136_261_u32);
60    }
61}