probabilistic_machines 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ //! Frequency estimation for a stream nobody wants to store.
2
+
3
+ use crate::hash::{hash64, mix64, u32_low_u8, u64_low_u16, usize_low_u8};
4
+ use crate::{CodecError, TAG_COUNTMIN, read8};
5
+
6
+ const HEADER_LEN: usize = 12;
7
+ const COUNTER_LEN: usize = 4;
8
+
9
+ /// Counts how often each item appeared, in `W * D` counters.
10
+ ///
11
+ /// `W` is the row width, a power of two up to 65536; `D` is the number of rows.
12
+ /// An estimate is never below the true count while [`saturated`](Self::saturated)
13
+ /// is false, and overshoots it by more than
14
+ /// `e * total / W` with probability at most `e^-D`, where `total` is the sum of
15
+ /// every count added. 2048 columns over 5 rows hold the overshoot under 0.14%
16
+ /// of the total with probability 0.993.
17
+ ///
18
+ /// ```
19
+ /// use probabilistic_machines::CountMinSketch;
20
+ ///
21
+ /// let mut hits = CountMinSketch::<1024, 4>::new();
22
+ /// hits.add_count(b"/login", 900);
23
+ /// hits.add(b"/health");
24
+ ///
25
+ /// assert!(hits.estimate(b"/login") >= 900);
26
+ /// assert_eq!(hits.estimate(b"/never-seen"), 0);
27
+ /// assert_eq!(hits.total(), 901);
28
+ /// ```
29
+ #[derive(Debug, Clone, PartialEq, Eq)]
30
+ pub struct CountMinSketch<const W: usize, const D: usize> {
31
+ rows: [[u32; W]; D],
32
+ saturated: bool,
33
+ total: u64,
34
+ }
35
+
36
+ impl<const W: usize, const D: usize> CountMinSketch<W, D> {
37
+ const COLUMN_MASK: usize = {
38
+ assert!(W.is_power_of_two(), "row width must be a power of two");
39
+ assert!(W >= 2, "row width must be at least 2");
40
+ assert!(W <= 1 << 16, "row width must be at most 65536");
41
+ assert!(D >= 1, "a sketch needs at least one row");
42
+ assert!(D <= 64, "a sketch needs at most 64 rows");
43
+ W - 1
44
+ };
45
+
46
+ /// Length of the [`encode`](Self::encode) frame.
47
+ pub const ENCODED_LEN: usize = HEADER_LEN + W * D * COUNTER_LEN;
48
+
49
+ #[must_use]
50
+ pub const fn new() -> Self {
51
+ Self {
52
+ rows: [[0; W]; D],
53
+ total: 0,
54
+ saturated: false,
55
+ }
56
+ }
57
+
58
+ /// Sum of every count added, saturating at [`u64::MAX`].
59
+ #[must_use]
60
+ pub const fn total(&self) -> u64 {
61
+ self.total
62
+ }
63
+
64
+ /// True once a counter has hit [`u32::MAX`], after which an estimate can
65
+ /// fall below the true count.
66
+ #[must_use]
67
+ pub const fn saturated(&self) -> bool {
68
+ self.saturated
69
+ }
70
+
71
+ pub fn add(&mut self, item: &[u8]) {
72
+ self.add_count(item, 1);
73
+ }
74
+
75
+ pub fn add_count(&mut self, item: &[u8], count: u32) {
76
+ self.add_hash_count(hash64(item), count);
77
+ }
78
+
79
+ /// Adds to an item the caller already hashed.
80
+ pub fn add_hash_count(&mut self, hash: u64, count: u32) {
81
+ let mut cursor = hash;
82
+ for row in &mut self.rows {
83
+ cursor = mix64(cursor);
84
+ let column = usize::from(u64_low_u16(cursor)) & Self::COLUMN_MASK;
85
+ let (sum, overflowed) = row[column].overflowing_add(count);
86
+ self.saturated |= overflowed;
87
+ row[column] = if overflowed { u32::MAX } else { sum };
88
+ }
89
+ self.total = self.total.saturating_add(u64::from(count));
90
+ }
91
+
92
+ /// Upper bound on how often `item` was added.
93
+ #[must_use]
94
+ pub fn estimate(&self, item: &[u8]) -> u32 {
95
+ self.estimate_hash(hash64(item))
96
+ }
97
+
98
+ /// Upper bound for an item the caller already hashed.
99
+ #[must_use]
100
+ pub fn estimate_hash(&self, hash: u64) -> u32 {
101
+ let mut cursor = hash;
102
+ let mut smallest = u32::MAX;
103
+ for row in &self.rows {
104
+ cursor = mix64(cursor);
105
+ let column = usize::from(u64_low_u16(cursor)) & Self::COLUMN_MASK;
106
+ if row[column] < smallest {
107
+ smallest = row[column];
108
+ }
109
+ }
110
+ smallest
111
+ }
112
+
113
+ /// Adds another sketch's counters to this one, which is exact for sketches
114
+ /// of the same shape: the result equals one sketch fed both streams.
115
+ pub fn merge(&mut self, other: &Self) {
116
+ let mut saturated = self.saturated || other.saturated;
117
+ for (mine, theirs) in self.rows.iter_mut().zip(&other.rows) {
118
+ for (counter, addend) in mine.iter_mut().zip(theirs) {
119
+ let (sum, overflowed) = counter.overflowing_add(*addend);
120
+ saturated |= overflowed;
121
+ *counter = if overflowed { u32::MAX } else { sum };
122
+ }
123
+ }
124
+ self.saturated = saturated;
125
+ self.total = self.total.saturating_add(other.total);
126
+ }
127
+
128
+ /// Writes the sketch into `out`, returning the number of bytes written.
129
+ ///
130
+ /// # Errors
131
+ ///
132
+ /// [`CodecError::Truncated`] when `out` is shorter than
133
+ /// [`ENCODED_LEN`](Self::ENCODED_LEN).
134
+ pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
135
+ let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
136
+ return Err(CodecError::Truncated);
137
+ };
138
+ self.write_frame(frame);
139
+ Ok(Self::ENCODED_LEN)
140
+ }
141
+
142
+ /// The sketch's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
143
+ #[cfg(feature = "alloc")]
144
+ #[must_use]
145
+ pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
146
+ let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
147
+ self.write_frame(&mut frame);
148
+ frame
149
+ }
150
+
151
+ /// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
152
+ fn write_frame(&self, frame: &mut [u8]) {
153
+ frame[0] = TAG_COUNTMIN;
154
+ frame[1] = u32_low_u8(W.trailing_zeros());
155
+ frame[2] = usize_low_u8(D);
156
+ frame[3..11].copy_from_slice(&self.total.to_le_bytes());
157
+ frame[11] = u8::from(self.saturated);
158
+
159
+ let counters = self.rows.iter().flatten();
160
+ let (slots, _) = frame[HEADER_LEN..].as_chunks_mut::<COUNTER_LEN>();
161
+ for (slot, counter) in slots.iter_mut().zip(counters) {
162
+ *slot = counter.to_le_bytes();
163
+ }
164
+ }
165
+
166
+ /// Reads a sketch written by [`encode`](Self::encode).
167
+ ///
168
+ /// # Errors
169
+ ///
170
+ /// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
171
+ /// when it holds another structure, [`CodecError::Parameters`] when its
172
+ /// shape differs from `W` by `D` or its counters contradict its total.
173
+ pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
174
+ let mut sketch = Self::new();
175
+ sketch.decode_into(bytes)?;
176
+ Ok(sketch)
177
+ }
178
+
179
+ /// Reads a frame over this sketch, for a caller that owns one already and
180
+ /// has no room on its stack for a second.
181
+ ///
182
+ /// The sketch is untouched when the frame is rejected.
183
+ ///
184
+ /// # Errors
185
+ ///
186
+ /// The same as [`decode`](Self::decode).
187
+ pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
188
+ let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
189
+ return Err(CodecError::Truncated);
190
+ };
191
+ if frame[0] != TAG_COUNTMIN {
192
+ return Err(CodecError::Tag);
193
+ }
194
+ if frame[1] != u32_low_u8(W.trailing_zeros()) || frame[2] != usize_low_u8(D) {
195
+ return Err(CodecError::Parameters);
196
+ }
197
+
198
+ let total = u64::from_le_bytes(read8(&frame[3..11]));
199
+ let saturated = frame[11] != 0;
200
+ let (slots, _) = frame[HEADER_LEN..].as_chunks::<COUNTER_LEN>();
201
+ if !Self::rows_are_coherent(slots, total, saturated) {
202
+ return Err(CodecError::Parameters);
203
+ }
204
+
205
+ self.total = total;
206
+ self.saturated = saturated;
207
+ let counters = self.rows.iter_mut().flatten();
208
+ for (slot, counter) in slots.iter().zip(counters) {
209
+ *counter = u32::from_le_bytes(*slot);
210
+ }
211
+ Ok(())
212
+ }
213
+
214
+ /// Whether the counters carry the total the frame claims, as every sketch
215
+ /// built by adding items does: each row takes every count added, so it sums
216
+ /// to the total exactly. Only a counter stopped at [`u32::MAX`] can leave a
217
+ /// row short of it, and such a sketch says so in its saturation flag.
218
+ fn rows_are_coherent(slots: &[[u8; COUNTER_LEN]], total: u64, saturated: bool) -> bool {
219
+ slots.chunks(W).all(|row| {
220
+ let counted: u64 = row
221
+ .iter()
222
+ .map(|slot| u64::from(u32::from_le_bytes(*slot)))
223
+ .sum();
224
+ if saturated {
225
+ counted <= total
226
+ } else {
227
+ counted == total
228
+ }
229
+ })
230
+ }
231
+ }
232
+
233
+ impl<const W: usize, const D: usize> Default for CountMinSketch<W, D> {
234
+ fn default() -> Self {
235
+ Self::new()
236
+ }
237
+ }
238
+
239
+ #[cfg(test)]
240
+ mod tests;
@@ -0,0 +1,283 @@
1
+ //! Approximate membership with deletions, which a Bloom filter cannot do.
2
+
3
+ use crate::hash::{hash64, mix64, u32_low_u8, u64_low_u8, u64_low_u16};
4
+ use crate::{CodecError, TAG_CUCKOO};
5
+
6
+ const SLOTS: usize = 4;
7
+ const EMPTY: u8 = 0;
8
+ const MAX_KICKS: usize = 64;
9
+
10
+ /// The filter has no room left for this item.
11
+ ///
12
+ /// Cuckoo hashing gives up before the table is literally full; expect that
13
+ /// past about 95% of [`CuckooFilter::CAPACITY`]. The filter is unchanged when
14
+ /// an insert fails.
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
+ pub struct Full;
17
+
18
+ impl core::fmt::Display for Full {
19
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20
+ f.write_str("cuckoo filter full")
21
+ }
22
+ }
23
+
24
+ impl core::error::Error for Full {}
25
+
26
+ /// Answers "have I seen this?" in `B * 4` bytes, and unlike a Bloom filter it
27
+ /// can forget.
28
+ ///
29
+ /// `B` is the bucket count, a power of two up to 65536; each bucket holds four
30
+ /// 8-bit fingerprints, so the filter stores up to `4 * B` items in `4 * B`
31
+ /// bytes. [`contains`](Self::contains) never says no to an item that was
32
+ /// inserted; it says yes to an item that was not with probability at most
33
+ /// `2 * 4 / 255`, about 3%.
34
+ ///
35
+ /// [`remove`](Self::remove) must only be called for items that were inserted:
36
+ /// deleting an item that merely collides with one takes the other item's
37
+ /// fingerprint with it.
38
+ ///
39
+ /// ```
40
+ /// use probabilistic_machines::CuckooFilter;
41
+ ///
42
+ /// let mut seen = CuckooFilter::<1024>::new();
43
+ /// assert_eq!(seen.insert(b"device-9"), Ok(()));
44
+ /// assert!(seen.contains(b"device-9"));
45
+ ///
46
+ /// assert!(seen.remove(b"device-9"));
47
+ /// assert!(!seen.contains(b"device-9"));
48
+ /// ```
49
+ #[derive(Debug, Clone, PartialEq, Eq)]
50
+ pub struct CuckooFilter<const B: usize> {
51
+ buckets: [[u8; SLOTS]; B],
52
+ items: usize,
53
+ }
54
+
55
+ impl<const B: usize> CuckooFilter<B> {
56
+ const BUCKET_MASK: usize = {
57
+ assert!(B.is_power_of_two(), "bucket count must be a power of two");
58
+ assert!(B >= 2, "bucket count must be at least 2");
59
+ assert!(B <= 1 << 16, "bucket count must be at most 65536");
60
+ B - 1
61
+ };
62
+
63
+ /// Fingerprint slots in the table.
64
+ pub const CAPACITY: usize = B * SLOTS;
65
+
66
+ /// Length of the [`encode`](Self::encode) frame.
67
+ pub const ENCODED_LEN: usize = 2 + B * SLOTS;
68
+
69
+ #[must_use]
70
+ pub const fn new() -> Self {
71
+ Self {
72
+ buckets: [[EMPTY; SLOTS]; B],
73
+ items: 0,
74
+ }
75
+ }
76
+
77
+ /// Items currently stored.
78
+ #[must_use]
79
+ pub const fn len(&self) -> usize {
80
+ self.items
81
+ }
82
+
83
+ #[must_use]
84
+ pub const fn is_empty(&self) -> bool {
85
+ self.items == 0
86
+ }
87
+
88
+ /// # Errors
89
+ ///
90
+ /// [`Full`] when the item's two buckets and 64 evictions all failed to
91
+ /// find a slot. The filter is left as it was.
92
+ pub fn insert(&mut self, item: &[u8]) -> Result<(), Full> {
93
+ self.insert_hash(hash64(item))
94
+ }
95
+
96
+ /// Inserts an item the caller already hashed.
97
+ ///
98
+ /// # Errors
99
+ ///
100
+ /// [`Full`] when the item's two buckets and 64 evictions all failed to
101
+ /// find a slot. The filter is left as it was.
102
+ pub fn insert_hash(&mut self, hash: u64) -> Result<(), Full> {
103
+ let fingerprint = fingerprint(hash);
104
+ let primary = Self::bucket(hash);
105
+ if self.place(primary, fingerprint) {
106
+ return Ok(());
107
+ }
108
+ let alternate = Self::alternate(primary, fingerprint);
109
+ if self.place(alternate, fingerprint) {
110
+ return Ok(());
111
+ }
112
+
113
+ let mut path = [0_usize; MAX_KICKS];
114
+ let mut index = alternate;
115
+ let mut carried = fingerprint;
116
+ let mut walk = mix64(hash);
117
+
118
+ for step in &mut path {
119
+ walk = mix64(walk);
120
+ let slot = usize::from(u64_low_u16(walk)) % SLOTS;
121
+ core::mem::swap(&mut carried, &mut self.buckets[index][slot]);
122
+ *step = index * SLOTS + slot;
123
+ index = Self::alternate(index, carried);
124
+ if self.place(index, carried) {
125
+ return Ok(());
126
+ }
127
+ }
128
+
129
+ for step in path.iter().rev() {
130
+ core::mem::swap(&mut carried, &mut self.buckets[step / SLOTS][step % SLOTS]);
131
+ }
132
+ Err(Full)
133
+ }
134
+
135
+ #[must_use]
136
+ pub fn contains(&self, item: &[u8]) -> bool {
137
+ self.contains_hash(hash64(item))
138
+ }
139
+
140
+ /// Queries an item the caller already hashed.
141
+ #[must_use]
142
+ pub fn contains_hash(&self, hash: u64) -> bool {
143
+ let fingerprint = fingerprint(hash);
144
+ let primary = Self::bucket(hash);
145
+ let alternate = Self::alternate(primary, fingerprint);
146
+ self.buckets[primary].contains(&fingerprint)
147
+ || self.buckets[alternate].contains(&fingerprint)
148
+ }
149
+
150
+ /// Drops one copy of an item, reporting whether a fingerprint was cleared.
151
+ pub fn remove(&mut self, item: &[u8]) -> bool {
152
+ self.remove_hash(hash64(item))
153
+ }
154
+
155
+ /// Drops an item the caller already hashed.
156
+ pub fn remove_hash(&mut self, hash: u64) -> bool {
157
+ let fingerprint = fingerprint(hash);
158
+ let primary = Self::bucket(hash);
159
+ let alternate = Self::alternate(primary, fingerprint);
160
+ self.clear(primary, fingerprint) || self.clear(alternate, fingerprint)
161
+ }
162
+
163
+ /// Writes the filter into `out`, returning the number of bytes written.
164
+ ///
165
+ /// # Errors
166
+ ///
167
+ /// [`CodecError::Truncated`] when `out` is shorter than
168
+ /// [`ENCODED_LEN`](Self::ENCODED_LEN).
169
+ pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
170
+ let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
171
+ return Err(CodecError::Truncated);
172
+ };
173
+ self.write_frame(frame);
174
+ Ok(Self::ENCODED_LEN)
175
+ }
176
+
177
+ /// The filter's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
178
+ #[cfg(feature = "alloc")]
179
+ #[must_use]
180
+ pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
181
+ let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
182
+ self.write_frame(&mut frame);
183
+ frame
184
+ }
185
+
186
+ /// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
187
+ fn write_frame(&self, frame: &mut [u8]) {
188
+ frame[0] = TAG_CUCKOO;
189
+ frame[1] = u32_low_u8(B.trailing_zeros());
190
+
191
+ let fingerprints = self.buckets.iter().flatten();
192
+ for (slot, value) in frame[2..].iter_mut().zip(fingerprints) {
193
+ *slot = *value;
194
+ }
195
+ }
196
+
197
+ /// Reads a filter written by [`encode`](Self::encode).
198
+ ///
199
+ /// # Errors
200
+ ///
201
+ /// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
202
+ /// when it holds another structure, [`CodecError::Parameters`] when it was
203
+ /// written with a different bucket count.
204
+ pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
205
+ let mut filter = Self::new();
206
+ filter.decode_into(bytes)?;
207
+ Ok(filter)
208
+ }
209
+
210
+ /// Reads a frame over this filter, for a caller that owns one already and
211
+ /// has no room on its stack for a second.
212
+ ///
213
+ /// The filter is untouched when the frame is rejected.
214
+ ///
215
+ /// # Errors
216
+ ///
217
+ /// The same as [`decode`](Self::decode).
218
+ pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
219
+ let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
220
+ return Err(CodecError::Truncated);
221
+ };
222
+ if frame[0] != TAG_CUCKOO {
223
+ return Err(CodecError::Tag);
224
+ }
225
+ if frame[1] != u32_low_u8(B.trailing_zeros()) {
226
+ return Err(CodecError::Parameters);
227
+ }
228
+
229
+ self.items = 0;
230
+ let fingerprints = self.buckets.iter_mut().flatten();
231
+ for (value, byte) in fingerprints.zip(&frame[2..]) {
232
+ *value = *byte;
233
+ if *byte != EMPTY {
234
+ self.items += 1;
235
+ }
236
+ }
237
+ Ok(())
238
+ }
239
+
240
+ fn bucket(hash: u64) -> usize {
241
+ usize::from(u64_low_u16(hash)) & Self::BUCKET_MASK
242
+ }
243
+
244
+ fn alternate(index: usize, fingerprint: u8) -> usize {
245
+ let twist = usize::from(u64_low_u16(mix64(u64::from(fingerprint)))) & Self::BUCKET_MASK;
246
+ index ^ twist
247
+ }
248
+
249
+ fn place(&mut self, index: usize, fingerprint: u8) -> bool {
250
+ for slot in &mut self.buckets[index] {
251
+ if *slot == EMPTY {
252
+ *slot = fingerprint;
253
+ self.items += 1;
254
+ return true;
255
+ }
256
+ }
257
+ false
258
+ }
259
+
260
+ fn clear(&mut self, index: usize, fingerprint: u8) -> bool {
261
+ for slot in &mut self.buckets[index] {
262
+ if *slot == fingerprint {
263
+ *slot = EMPTY;
264
+ self.items -= 1;
265
+ return true;
266
+ }
267
+ }
268
+ false
269
+ }
270
+ }
271
+
272
+ impl<const B: usize> Default for CuckooFilter<B> {
273
+ fn default() -> Self {
274
+ Self::new()
275
+ }
276
+ }
277
+
278
+ fn fingerprint(hash: u64) -> u8 {
279
+ u64_low_u8(mix64(hash)).max(1)
280
+ }
281
+
282
+ #[cfg(test)]
283
+ mod tests;
@@ -0,0 +1,88 @@
1
+ //! The crate's only hash. Sketches index by bit slices of a 64-bit value, so
2
+ //! they need avalanche behaviour, not cryptography.
3
+
4
+ const PRIME: u64 = 0x9E37_79B9_7F4A_7C15;
5
+
6
+ /// `splitmix64`'s finalizer: a bijection on [`u64`].
7
+ ///
8
+ /// Every output bit depends on every input bit, and iterating it yields
9
+ /// decorrelated values, which is how the sketches derive one index per row
10
+ /// from a single hash.
11
+ #[must_use]
12
+ pub const fn mix64(value: u64) -> u64 {
13
+ let mut z = value.wrapping_add(PRIME);
14
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
15
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
16
+ z ^ (z >> 31)
17
+ }
18
+
19
+ /// Hashes bytes with the crate's default seed.
20
+ #[must_use]
21
+ pub const fn hash64(bytes: &[u8]) -> u64 {
22
+ hash64_with_seed(bytes, 0)
23
+ }
24
+
25
+ /// Hashes bytes, folding eight at a time through [`mix64`].
26
+ ///
27
+ /// Two seeds give two independent hash families, which is what a filter needs
28
+ /// when it wants fingerprints uncorrelated with bucket indices.
29
+ #[must_use]
30
+ pub const fn hash64_with_seed(bytes: &[u8], seed: u64) -> u64 {
31
+ let mut accumulator = seed ^ PRIME;
32
+ let mut consumed: u64 = 0;
33
+ let mut cursor = 0;
34
+
35
+ while cursor + 8 <= bytes.len() {
36
+ let lane = u64::from_le_bytes([
37
+ bytes[cursor],
38
+ bytes[cursor + 1],
39
+ bytes[cursor + 2],
40
+ bytes[cursor + 3],
41
+ bytes[cursor + 4],
42
+ bytes[cursor + 5],
43
+ bytes[cursor + 6],
44
+ bytes[cursor + 7],
45
+ ]);
46
+ accumulator = (accumulator ^ mix64(lane))
47
+ .rotate_left(27)
48
+ .wrapping_mul(PRIME);
49
+ cursor += 8;
50
+ consumed += 8;
51
+ }
52
+
53
+ let mut tail: u64 = 0;
54
+ let mut shift = 0;
55
+ while cursor < bytes.len() {
56
+ tail |= (bytes[cursor] as u64) << shift;
57
+ shift += 8;
58
+ cursor += 1;
59
+ consumed += 1;
60
+ }
61
+
62
+ mix64(mix64(accumulator ^ consumed.wrapping_mul(PRIME)) ^ mix64(tail))
63
+ }
64
+
65
+ pub(crate) const fn u64_low_u16(value: u64) -> u16 {
66
+ let [low, high, ..] = value.to_le_bytes();
67
+ u16::from_le_bytes([low, high])
68
+ }
69
+
70
+ pub(crate) const fn u64_low_u8(value: u64) -> u8 {
71
+ value.to_le_bytes()[0]
72
+ }
73
+
74
+ pub(crate) const fn u32_low_u8(value: u32) -> u8 {
75
+ value.to_le_bytes()[0]
76
+ }
77
+
78
+ pub(crate) const fn usize_low_u8(value: usize) -> u8 {
79
+ value.to_le_bytes()[0]
80
+ }
81
+
82
+ pub(crate) const fn usize_low_u16(value: usize) -> u16 {
83
+ let [low, high, ..] = value.to_le_bytes();
84
+ u16::from_le_bytes([low, high])
85
+ }
86
+
87
+ #[cfg(test)]
88
+ mod tests;