memo_cache/lib.rs
1#![no_std]
2
3use core::borrow::Borrow;
4
5/// Key equivalence trait, to support `Borrow` types as keys.
6trait Equivalent<K: ?Sized> {
7 /// Returns `true` if two values are equivalent, `false` if otherwise.
8 fn equivalent(&self, k: &K) -> bool;
9}
10
11impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
12where
13 Q: Eq,
14 K: Borrow<Q>,
15{
16 fn equivalent(&self, k: &K) -> bool {
17 self == k.borrow()
18 }
19}
20
21/// A single key/value slot used in the cache.
22#[derive(Clone, PartialEq)]
23enum KeyValueSlot<K, V> {
24 Used((K, V)),
25 Empty,
26}
27
28impl<K, V> KeyValueSlot<K, V> {
29 /// Check a used slot key for equivalence.
30 #[cfg_attr(feature = "inline-more", inline)]
31 fn is_key<Q>(&self, k: &Q) -> bool
32 where
33 Q: Equivalent<K> + ?Sized,
34 {
35 if let KeyValueSlot::Used(kv) = self {
36 k.equivalent(&kv.0)
37 } else {
38 false
39 }
40 }
41
42 /// Get the value of a used slot.
43 #[cfg_attr(feature = "inline-more", inline)]
44 fn get_value(&self) -> Option<&V> {
45 if let KeyValueSlot::Used(kv) = self {
46 Some(&kv.1)
47 } else {
48 None
49 }
50 }
51
52 /// Get the value of a used slot (for mutation).
53 #[cfg_attr(feature = "inline-more", inline)]
54 fn get_value_mut(&mut self) -> Option<&mut V> {
55 if let KeyValueSlot::Used(kv) = self {
56 Some(&mut kv.1)
57 } else {
58 None
59 }
60 }
61
62 /// Update the value of a used slot.
63 #[cfg_attr(feature = "inline-more", inline)]
64 fn update_value(&mut self, v: V) {
65 if let KeyValueSlot::Used(kv) = self {
66 kv.1 = v
67 }
68 }
69}
70
71/// A small, fixed-size, heap-allocated key/value cache with retention management.
72pub struct MemoCache<K, V, const SIZE: usize> {
73 buffer: [KeyValueSlot<K, V>; SIZE],
74 cursor: usize,
75}
76
77impl<K, V, const SIZE: usize> MemoCache<K, V, SIZE>
78where
79 K: Clone + Eq,
80 V: Clone,
81{
82 /// Create a new cache.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// use memo_cache::MemoCache;
88 ///
89 /// let c = MemoCache::<u32, String, 4>::new();
90 /// ```
91 #[cfg_attr(feature = "inline-more", inline)]
92 pub fn new() -> Self {
93 Self {
94 buffer: [const { KeyValueSlot::Empty }; SIZE],
95 cursor: 0,
96 }
97 }
98
99 /// Get the (fixed) capacity of the cache.
100 ///
101 /// # Examples
102 ///
103 /// ```
104 /// use memo_cache::MemoCache;
105 ///
106 /// let c = MemoCache::<u32, String, 8>::new();
107 ///
108 /// assert_eq!(c.capacity(), 8);
109 /// ```
110 #[cfg_attr(feature = "inline-more", inline)]
111 pub const fn capacity(&self) -> usize {
112 SIZE
113 }
114
115 /// Replace slot under cursor and shift cursor position. Returns a reference to the replaced slot value.
116 #[cfg_attr(feature = "inline-more", inline)]
117 fn replace_and_shift(&mut self, k: K, v: V) -> &V {
118 // SAFETY: The cursor value is assumed to be correct.
119 let s = unsafe { self.buffer.get_unchecked_mut(self.cursor) };
120
121 *s = KeyValueSlot::Used((k, v));
122
123 // Move the cursor over the buffer elements sequentially, creating FIFO behavior.
124 self.cursor = (self.cursor + 1) % SIZE;
125
126 // SAFETY: The slot was filled with a key/value above.
127 unsafe { s.get_value().unwrap_unchecked() }
128 }
129
130 /// Insert a key/value pair.
131 ///
132 /// # Examples
133 ///
134 /// ```
135 /// use memo_cache::MemoCache;
136 ///
137 /// let mut c = MemoCache::<u32, &str, 4>::new();
138 ///
139 /// assert_eq!(c.get(&42), None);
140 ///
141 /// c.insert(42, "The Answer");
142 ///
143 /// assert_eq!(c.get(&42), Some(&"The Answer"));
144 /// ```
145 #[cfg_attr(feature = "inline-more", inline)]
146 pub fn insert(&mut self, k: K, v: V) {
147 match self.buffer.iter_mut().find(|e| e.is_key(&k)) {
148 Some(s) => s.update_value(v),
149 None => {
150 self.replace_and_shift(k, v);
151 }
152 }
153 }
154
155 /// Returns `true` if the cache contains a value for the specified key.
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use memo_cache::MemoCache;
161 ///
162 /// let mut c = MemoCache::<u32, &str, 4>::new();
163 ///
164 /// assert_eq!(c.contains_key(&42), false);
165 ///
166 /// c.insert(42, "The Answer");
167 ///
168 /// assert_eq!(c.contains_key(&42), true);
169 /// ```
170 #[cfg_attr(feature = "inline-more", inline)]
171 pub fn contains_key<Q>(&self, k: &Q) -> bool
172 where
173 K: Borrow<Q>,
174 Q: Eq + ?Sized,
175 {
176 self.buffer.iter().any(|e| e.is_key(k))
177 }
178
179 /// Lookup a cache entry by key.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use memo_cache::MemoCache;
185 ///
186 /// let mut c = MemoCache::<u32, &str, 4>::new();
187 ///
188 /// assert_eq!(c.get(&42), None);
189 ///
190 /// c.insert(42, "The Answer");
191 ///
192 /// assert_eq!(c.get(&42), Some(&"The Answer"));
193 /// ```
194 #[cfg_attr(feature = "inline-more", inline)]
195 pub fn get<Q>(&self, k: &Q) -> Option<&V>
196 where
197 K: Borrow<Q>,
198 Q: Eq + ?Sized,
199 {
200 self.buffer
201 .iter()
202 .find(|e| e.is_key(k))
203 .map(|e| e.get_value().unwrap())
204 }
205
206 /// Lookup a cache entry by key (for mutation).
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use memo_cache::MemoCache;
212 ///
213 /// let mut c = MemoCache::<u32, &str, 4>::new();
214 ///
215 /// c.insert(42, "The Answer");
216 ///
217 /// if let Some(v) = c.get_mut(&42) {
218 /// *v = "Another Answer";
219 /// }
220 ///
221 /// assert_eq!(c.get(&42), Some(&"Another Answer"));
222 /// ```
223 #[cfg_attr(feature = "inline-more", inline)]
224 pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
225 where
226 K: Borrow<Q>,
227 Q: Eq + ?Sized,
228 {
229 self.buffer
230 .iter_mut()
231 .find(|e| e.is_key(k))
232 .map(|e| e.get_value_mut().unwrap())
233 }
234
235 /// Get the index for a given key, if found.
236 #[cfg_attr(feature = "inline-more", inline)]
237 fn get_key_index<Q>(&self, k: &Q) -> Option<usize>
238 where
239 K: Borrow<Q>,
240 Q: Eq + ?Sized,
241 {
242 self.buffer.iter().position(|e| e.is_key(k))
243 }
244
245 /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
246 /// Returns a reference to the found, or newly inserted value associated with the given key.
247 /// If a value is inserted, the key is cloned.
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use memo_cache::MemoCache;
253 ///
254 /// let mut c = MemoCache::<u32, &str, 4>::new();
255 ///
256 /// assert_eq!(c.get(&42), None);
257 ///
258 /// let v = c.get_or_insert_with(&42, |_| "The Answer");
259 ///
260 /// assert_eq!(v, &"The Answer");
261 /// assert_eq!(c.get(&42), Some(&"The Answer"));
262 /// ```
263 ///
264 /// # Notes
265 ///
266 /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
267 /// version of `get_or_insert_with` that can create an owned value from a borrowed key.
268 ///
269 #[cfg_attr(feature = "inline-more", inline)]
270 pub fn get_or_insert_with<F>(&mut self, k: &K, f: F) -> &V
271 where
272 F: FnOnce(&K) -> V,
273 {
274 if let Some(i) = self.get_key_index(k) {
275 // SAFETY: The key index was retrieved from a found key.
276 unsafe { self.buffer[i].get_value().unwrap_unchecked() }
277 } else {
278 self.replace_and_shift(k.clone(), f(k))
279 }
280 }
281
282 /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
283 /// Returns a result with a reference to the found, or newly inserted value associated with the given key.
284 /// If `f` fails, the error is returned.
285 /// If a value is inserted, the key is cloned.
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// use memo_cache::MemoCache;
291 ///
292 /// let mut c = MemoCache::<u32, &str, 4>::new();
293 ///
294 /// assert_eq!(c.get(&42), None);
295 ///
296 /// let answer : Result<_, &str> = Ok("The Answer");
297 /// let v = c.get_or_try_insert_with(&42, |_| answer);
298 ///
299 /// assert_eq!(v, Ok(&"The Answer"));
300 /// assert_eq!(c.get(&42), Some(&"The Answer"));
301 ///
302 /// let v = c.get_or_try_insert_with(&17, |_| Err("Dunno"));
303 ///
304 /// assert_eq!(v, Err("Dunno"));
305 /// assert_eq!(c.get(&17), None);
306 /// ```
307 ///
308 /// # Notes
309 ///
310 /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
311 /// version of `get_or_try_insert_with` that can create an owned value from a borrowed key.
312 ///
313 #[cfg_attr(feature = "inline-more", inline)]
314 pub fn get_or_try_insert_with<F, E>(&mut self, k: &K, f: F) -> Result<&V, E>
315 where
316 F: FnOnce(&K) -> Result<V, E>,
317 {
318 if let Some(i) = self.get_key_index(k) {
319 // SAFETY: The key index was retrieved from a found key.
320 Ok(unsafe { self.buffer[i].get_value().unwrap_unchecked() })
321 } else {
322 f(k).map(|v| self.replace_and_shift(k.clone(), v))
323 }
324 }
325
326 /// Clear the cache.
327 ///
328 /// # Examples
329 ///
330 /// ```
331 /// use memo_cache::MemoCache;
332 ///
333 /// let mut c = MemoCache::<u32, &str, 4>::new();
334 ///
335 /// assert_eq!(c.get(&42), None);
336 ///
337 /// c.insert(42, "The Answer");
338 ///
339 /// assert_eq!(c.get(&42), Some(&"The Answer"));
340 ///
341 /// c.clear();
342 ///
343 /// assert_eq!(c.get(&42), None);
344 ///
345 #[cfg_attr(feature = "inline-more", inline)]
346 pub fn clear(&mut self) {
347 self.buffer
348 .iter_mut()
349 .for_each(|e| *e = KeyValueSlot::Empty);
350 self.cursor = 0;
351 }
352}
353
354impl<K, V, const SIZE: usize> Default for MemoCache<K, V, SIZE>
355where
356 K: Clone + Eq,
357 V: Clone,
358{
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364#[cfg(test)]
365mod tests_internal {
366 use super::*;
367
368 #[test]
369 fn test_new_state() {
370 const SIZE: usize = 8;
371
372 let c = MemoCache::<i32, i32, SIZE>::new();
373
374 // Verify cache size.
375 assert_eq!(c.buffer.len(), SIZE);
376 assert_eq!(c.capacity(), SIZE);
377
378 // All slots should be empty.
379 assert!(c.buffer.iter().all(|s| s == &KeyValueSlot::Empty));
380 }
381
382 #[test]
383 fn test_cursor_state() {
384 let mut c = MemoCache::<i32, i32, 2>::new();
385
386 assert_eq!(c.cursor, 0);
387
388 c.insert(1, 2);
389
390 assert_eq!(c.cursor, 1);
391
392 c.insert(3, 4);
393
394 assert_eq!(c.cursor, 0);
395
396 c.insert(5, 6);
397
398 assert_eq!(c.cursor, 1);
399
400 c.insert(7, 8);
401
402 assert_eq!(c.cursor, 0);
403 }
404}