metrale_grammar/support/
hash.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Hash-combining helpers — port of the genuinely-reusable part of
4// `cpp/support/utils.h`.
5//
6// Skipped from `utils.h` (C++-template machinery with no Rust need):
7//   - `HashByMembers` / `XGRAMMAR_HASH_BY_MEMBERS`: replaced by
8//     `#[derive(Hash)]`.
9//   - `std::hash` specializations for pair/tuple/vector: Rust derives
10//     these.
11//   - `PartialResult` / `Result` / `ResultOk` / `ResultErr` /
12//     `Convert`: replaced by the standard library `Result`.
13//   - `TypedError`, `ThrowVariantError`, `GetMessageFromVariantError`:
14//     replaced by `thiserror`-based error enums.
15//   - `XGRAMMAR_UNREACHABLE`, `EqualByMembers` / `XGRAMMAR_EQUAL_BY_MEMBERS`:
16//     replaced by `unreachable!()` and `#[derive(PartialEq, Eq)]`.
17//
18// Only `HashCombineBinary` / `HashCombine` carry over: they implement
19// the specific boost-derived mixing function xgrammar relies on for
20// structural hashes, which `#[derive(Hash)]` does not reproduce.
21
22/// Mix `value` into `seed` in place.
23///
24/// Faithful to C++ `HashCombineBinary` — the boost `hash_combine`
25/// 64-bit mixing step. Uses wrapping arithmetic to match C++ unsigned
26/// overflow semantics.
27pub fn hash_combine_binary(seed: &mut u64, value: u64) {
28    *seed ^= value
29        .wrapping_add(0x9e3779b97f4a7c15)
30        .wrapping_add(*seed << 6)
31        .wrapping_add(*seed >> 2);
32}
33
34/// Fold a slice of values into a single combined hash, starting from
35/// seed `0`. Faithful to the variadic C++ `HashCombine`.
36pub fn hash_combine(values: &[u64]) -> u64 {
37    let mut seed = 0u64;
38    for &v in values {
39        hash_combine_binary(&mut seed, v);
40    }
41    seed
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn combine_binary_is_deterministic() {
50        let mut a = 0u64;
51        let mut b = 0u64;
52        hash_combine_binary(&mut a, 42);
53        hash_combine_binary(&mut b, 42);
54        assert_eq!(a, b);
55        assert_ne!(a, 0);
56    }
57
58    #[test]
59    fn combine_binary_order_sensitive() {
60        let mut a = 0u64;
61        hash_combine_binary(&mut a, 1);
62        hash_combine_binary(&mut a, 2);
63
64        let mut b = 0u64;
65        hash_combine_binary(&mut b, 2);
66        hash_combine_binary(&mut b, 1);
67
68        assert_ne!(a, b);
69    }
70
71    #[test]
72    fn combine_matches_manual_folding() {
73        let mut seed = 0u64;
74        hash_combine_binary(&mut seed, 7);
75        hash_combine_binary(&mut seed, 8);
76        hash_combine_binary(&mut seed, 9);
77        assert_eq!(hash_combine(&[7, 8, 9]), seed);
78    }
79
80    #[test]
81    fn combine_empty_is_zero() {
82        assert_eq!(hash_combine(&[]), 0);
83    }
84
85    #[test]
86    fn combine_distinguishes_inputs() {
87        assert_ne!(hash_combine(&[1, 2, 3]), hash_combine(&[1, 2, 4]));
88        assert_ne!(hash_combine(&[1, 2]), hash_combine(&[2, 1]));
89    }
90
91    #[test]
92    fn combine_no_overflow_panic() {
93        // Large values must not panic (wrapping arithmetic).
94        let mut seed = u64::MAX;
95        hash_combine_binary(&mut seed, u64::MAX);
96        let _ = hash_combine(&[u64::MAX, u64::MAX, u64::MAX]);
97    }
98}