Skip to main content

rustpython_common/
hash.rs

1use core::hash::{BuildHasher, Hash, Hasher};
2use malachite_bigint::BigInt;
3use num_traits::ToPrimitive;
4use siphasher::sip::SipHasher24;
5
6pub type PyHash = i64;
7pub type PyUHash = u64;
8
9/// A PyHash value used to represent a missing hash value, e.g. means "not yet computed" for
10/// `str`'s hash cache
11pub const SENTINEL: PyHash = -1;
12
13/// Prime multiplier used in string and various other hashes.
14pub const MULTIPLIER: PyHash = 1_000_003; // 0xf4243
15/// Numeric hashes are based on reduction modulo the prime 2**_BITS - 1
16pub const BITS: usize = 61;
17pub const MODULUS: PyUHash = (1 << BITS) - 1;
18pub const INF: PyHash = 314_159;
19pub const NAN: PyHash = 0;
20pub const IMAG: PyHash = MULTIPLIER;
21pub const ALGO: &str = "siphash24";
22pub const HASH_BITS: usize = core::mem::size_of::<PyHash>() * 8;
23// SipHasher24 takes 2 u64s as a seed
24pub const SEED_BITS: usize = core::mem::size_of::<u64>() * 2 * 8;
25
26// pub const CUTOFF: usize = 7;
27
28#[derive(Clone, Copy)]
29pub struct HashSecret {
30    k0: u64,
31    k1: u64,
32}
33
34impl BuildHasher for HashSecret {
35    type Hasher = SipHasher24;
36
37    fn build_hasher(&self) -> Self::Hasher {
38        SipHasher24::new_with_keys(self.k0, self.k1)
39    }
40}
41
42impl HashSecret {
43    #[must_use]
44    pub fn new(seed: u32) -> Self {
45        let mut buf = [0u8; 16];
46        lcg_urandom(seed, &mut buf);
47        let (left, right) = buf.split_at(8);
48        let k0 = u64::from_le_bytes(left.try_into().unwrap());
49        let k1 = u64::from_le_bytes(right.try_into().unwrap());
50        Self { k0, k1 }
51    }
52
53    /// Build a secret from explicit SipHash keys, bypassing seed derivation.
54    /// Lets an embedder reproduce a fixed keying (e.g. a deterministic run) that
55    /// [`new`](Self::new) cannot express through its `u32` seed.
56    #[must_use]
57    pub const fn from_keys(k0: u64, k1: u64) -> Self {
58        Self { k0, k1 }
59    }
60
61    pub fn hash_value<T: Hash + ?Sized>(&self, data: &T) -> PyHash {
62        fix_sentinel(mod_int(self.hash_one(data) as _))
63    }
64
65    pub fn hash_iter<'a, T: 'a, I, F, E>(&self, iter: I, hash_func: F) -> Result<PyHash, E>
66    where
67        I: IntoIterator<Item = &'a T>,
68        F: Fn(&'a T) -> Result<PyHash, E>,
69    {
70        let mut hasher = self.build_hasher();
71        for element in iter {
72            let item_hash = hash_func(element)?;
73            item_hash.hash(&mut hasher);
74        }
75        Ok(fix_sentinel(mod_int(hasher.finish() as PyHash)))
76    }
77
78    #[must_use]
79    pub fn hash_bytes(&self, value: &[u8]) -> PyHash {
80        if value.is_empty() {
81            0
82        } else {
83            self.hash_value(value)
84        }
85    }
86
87    #[must_use]
88    pub fn hash_str(&self, value: &str) -> PyHash {
89        self.hash_bytes(value.as_bytes())
90    }
91}
92
93#[inline]
94#[must_use]
95pub const fn hash_pointer(value: usize) -> PyHash {
96    // TODO: 32bit?
97    let hash = (value >> 4) | value;
98    hash as _
99}
100
101#[inline]
102#[must_use]
103pub const fn hash_float(value: f64) -> Option<PyHash> {
104    // cpython _Py_HashDouble
105    if !value.is_finite() {
106        return if value.is_infinite() {
107            Some(if value > 0.0 { INF } else { -INF })
108        } else {
109            None
110        };
111    }
112
113    let frexp = super::float_ops::decompose_float(value);
114
115    // process 28 bits at a time;  this should work well both for binary
116    // and hexadecimal floating point.
117    let mut m = frexp.0;
118    let mut e = frexp.1;
119    let mut x: PyUHash = 0;
120
121    #[expect(clippy::while_float, reason = "keep this loop like CPython does it")]
122    while m != 0.0 {
123        x = ((x << 28) & MODULUS) | (x >> (BITS - 28));
124        m *= 268_435_456.0; // 2**28
125        e -= 28;
126        let y = m as PyUHash; // pull out integer part
127        m -= y as f64;
128        x += y;
129        if x >= MODULUS {
130            x -= MODULUS;
131        }
132    }
133
134    // adjust for the exponent;  first reduce it modulo BITS
135    const BITS32: i32 = BITS as i32;
136    e = if e >= 0 {
137        e % BITS32
138    } else {
139        BITS32 - 1 - ((-1 - e) % BITS32)
140    };
141    x = ((x << e) & MODULUS) | (x >> (BITS32 - e));
142
143    Some(fix_sentinel(x as PyHash * value.signum() as PyHash))
144}
145
146#[must_use]
147pub fn hash_bigint(value: &BigInt) -> PyHash {
148    let ret = if let Some(v) = value.to_i64() {
149        mod_int(v)
150    } else {
151        // SAFETY:
152        // MODULUS < i64::MAX, so value % MODULUS is guaranteed to be in the range of i64
153        unsafe { (value % MODULUS).to_i64().unwrap_unchecked() }
154    };
155
156    fix_sentinel(ret)
157}
158
159#[inline]
160#[must_use]
161pub const fn hash_usize(data: usize) -> PyHash {
162    fix_sentinel(mod_int(data as i64))
163}
164
165#[inline(always)]
166#[must_use]
167pub const fn fix_sentinel(x: PyHash) -> PyHash {
168    if x == SENTINEL { -2 } else { x }
169}
170
171#[inline]
172#[must_use]
173pub const fn mod_int(value: i64) -> PyHash {
174    value % MODULUS as i64
175}
176
177pub fn lcg_urandom(mut x: u32, buf: &mut [u8]) {
178    for b in buf {
179        x = x.wrapping_mul(214013);
180        x = x.wrapping_add(2531011);
181        *b = ((x >> 16) & 0xff) as u8;
182    }
183}
184
185#[inline]
186#[must_use]
187pub const fn hash_object_id_raw(p: usize) -> PyHash {
188    // TODO: Use commented logic when below issue resolved.
189    // Ref: https://github.com/RustPython/RustPython/pull/3951#issuecomment-1193108966
190
191    /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid
192    excessive hash collisions for dicts and sets */
193    // p.rotate_right(4) as PyHash
194    p as PyHash
195}
196
197#[inline]
198#[must_use]
199pub const fn hash_object_id(p: usize) -> PyHash {
200    fix_sentinel(hash_object_id_raw(p))
201}
202
203#[must_use]
204pub fn keyed_hash(key: u64, buf: &[u8]) -> u64 {
205    let mut hasher = SipHasher24::new_with_keys(key, 0);
206    buf.hash(&mut hasher);
207    hasher.finish()
208}
209
210/// tuplehash: fold the element hashes of a tuple (xxHash-based).
211///
212/// The caller supplies each element's hash lazily; a hash computation may fail,
213/// in which case the error short-circuits the fold.
214pub fn hash_tuple<E>(
215    element_hashes: impl IntoIterator<Item = Result<PyHash, E>>,
216) -> Result<PyHash, E> {
217    const PRIME1: PyUHash = cfg_select! {
218        target_pointer_width = "64" => 11400714785074694791,
219        target_pointer_width = "32" => 2654435761,
220        _ => unreachable!(),
221    };
222
223    const PRIME2: PyUHash = cfg_select! {
224        target_pointer_width = "64" => 14029467366897019727,
225        target_pointer_width = "32" => 2246822519,
226        _ => unreachable!(),
227    };
228
229    const PRIME5: PyUHash = cfg_select! {
230        target_pointer_width = "64" => 2870177450012600261,
231        target_pointer_width = "32" => 374761393,
232        _ => unreachable!(),
233    };
234
235    const ROTATE: u32 = cfg_select! {
236        target_pointer_width = "64" => 31,
237        target_pointer_width = "32" => 13,
238        _ => unreachable!(),
239    };
240
241    let mut acc = PRIME5;
242    let mut len: PyUHash = 0;
243
244    for element_hash in element_hashes {
245        let lane = element_hash? as PyUHash;
246        acc = acc.wrapping_add(lane.wrapping_mul(PRIME2));
247        acc = acc.rotate_left(ROTATE);
248        acc = acc.wrapping_mul(PRIME1);
249        len += 1;
250    }
251
252    acc = acc.wrapping_add(len ^ (PRIME5 ^ 3527539));
253
254    let acc_py_hash = acc as PyHash;
255    if acc_py_hash == -1 {
256        return Ok(1546275796);
257    }
258
259    Ok(acc_py_hash)
260}
261
262/// frozenset_hash: order-independent XOR-fold of a frozenset's element hashes.
263///
264/// The entry hashes are fed in one at a time via [`FrozenSetHash::add`], so the
265/// caller keeps ownership of the iteration (which may hold a lock and compute
266/// each element hash fallibly). The fold is commutative, so element order does
267/// not affect the result.
268pub struct FrozenSetHash {
269    hash: u64,
270}
271
272impl FrozenSetHash {
273    #[must_use]
274    pub fn new(len: usize) -> Self {
275        // Factor in the number of active entries
276        Self {
277            hash: (len as u64 + 1).wrapping_mul(1927868237),
278        }
279    }
280
281    pub fn add(&mut self, element_hash: PyHash) {
282        // Work to increase the bit dispersion for closely spaced hash values.
283        // This is important because some use cases have many combinations of a
284        // small number of elements with nearby hashes so that many distinct
285        // combinations collapse to only a handful of distinct hash values.
286        const fn shuffle_bits(h: u64) -> u64 {
287            ((h ^ 89869747) ^ (h.wrapping_shl(16))).wrapping_mul(3644798167)
288        }
289        // Xor-in shuffled bits from every entry's hash field because xor is
290        // commutative and a frozenset hash should be independent of order.
291        self.hash ^= shuffle_bits(element_hash as u64);
292    }
293
294    #[must_use]
295    pub fn finish(self) -> PyHash {
296        let mut hash = self.hash;
297        // Disperse patterns arising in nested frozen-sets
298        hash ^= (hash >> 11) ^ (hash >> 25);
299        hash = hash.wrapping_mul(69069).wrapping_add(907133923);
300        // -1 is reserved as an error code
301        if hash == u64::MAX {
302            hash = 590923713;
303        }
304        hash as PyHash
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn from_keys_is_stable_and_seed_independent() {
314        const K0: u64 = 0x0706_0504_0302_0100;
315        const K1: u64 = 0x0f0e_0d0c_0b0a_0908;
316        const LOCKED_DIGEST: PyHash = -1862661396243998188;
317
318        // Two secrets built from the same explicit keys hash identically, and
319        // the digest does not depend on the seed-derivation path.
320        let a = HashSecret::from_keys(K0, K1);
321        let b = HashSecret::from_keys(K0, K1);
322        assert_eq!(a.hash_str("hello"), b.hash_str("hello"));
323        assert_eq!(
324            a.hash_bytes(b"a fixed message"),
325            b.hash_bytes(b"a fixed message")
326        );
327
328        // Explicit keys drive the SipHasher-2-4 directly. `keyed_hash` pins
329        // k1 = 0, so a secret built with the same k0 and k1 = 0 must reproduce
330        // its raw digest.
331        let zero_k1 = HashSecret::from_keys(K0, 0);
332        assert_eq!(keyed_hash(K0, b"payload"), zero_k1.hash_one(b"payload"));
333
334        // Locked digest so an accidental keying change is caught.
335        assert_eq!(a.hash_str("determinism"), LOCKED_DIGEST);
336    }
337}