Skip to main content

sui_intern/
memo.rs

1//! `ContentMemo` — a byte-neutral, content-keyed memo of a PURE function.
2//!
3//! # The load-bearing invariant (why this is safe on a byte-parity path)
4//!
5//! The memoized value is a **pure function of its content-key**, so a cache
6//! hit is **byte-identical** to a recompute. That is exactly what makes
7//! memoization safe on sui's byte-parity-critical eval path: a memoized nix
8//! evaluation must produce the identical `drvPath` whether the value was
9//! freshly computed or served from the memo. The key IS the content address;
10//! the value is fully determined by it.
11//!
12//! **RISK — do not violate:** never memoize a value that depends on anything
13//! other than its key — wall-clock (`currentTime`), environment (`getEnv`),
14//! mutable filesystem reads, or eval-order-sensitive identity. Those are not
15//! pure functions of the key, so a hit would NOT equal a recompute and could
16//! change a `drvPath`. If the value can vary for a fixed key, it is not a
17//! `ContentMemo` candidate.
18//!
19//! # Why it's a primitive
20//!
21//! This exact shape — a thread-local `Map<ContentKey, Rc<Value>>` of a pure
22//! function, cleared at a natural boundary — was hand-rolled three times
23//! before this module (the sui-compat NAR-hash memo, the sui-eval
24//! referenced-idents memo, the sui-eval overlay-flatten cache). This is the
25//! extracted, packaged shape: declare one with [`thread_local_content_memo!`]
26//! and get a memoized accessor + a clear fn for free.
27//!
28//! Single-threaded by construction (`Rc` + `RefCell`), matching sui's
29//! single-threaded evaluator.
30
31use rustc_hash::FxHashMap;
32use std::cell::RefCell;
33use std::hash::{Hash, Hasher};
34use std::marker::PhantomData;
35use std::rc::Rc;
36
37/// A content address derived **by construction** from a value of type `T`.
38///
39/// # The type-level seal (M3 — the stale-key/decoupling axis)
40///
41/// The general [`ContentMemo::get_or_compute`] lets the `key` and the
42/// `compute` closure **disagree**: a caller can pass a key that is *not* a
43/// function of what `compute` actually reads (the stale-key footgun — the
44/// `Sharing::PerSite`/libxcrypt divergence class). `ContentKey<T>` removes
45/// that footgun structurally: its **sole constructor** is [`ContentKey::of`],
46/// which hashes `T`'s structural read-set. So "the key IS the content of the
47/// input" is not a caller obligation — it holds **by construction**, and a
48/// key decoupled from its input **has no way to be built**.
49///
50/// Paired with [`ContentMemo::get_or_compute_keyed`], which derives the key
51/// from the same `&T` it hands to `compute`, the key↔content decoupling axis
52/// is **parse-time-rejected** (there is no expressible program that memoizes
53/// under a key not derived from the computed input).
54///
55/// # Honest ceiling — what this does NOT seal
56///
57/// This seals the KEY↔CONTENT structural axis only. It does **not** seal the
58/// **purity** of `T → V`: `compute` could still read wall-clock, `getEnv`, or a
59/// mutable filesystem — those are opaque to the type system. There is no
60/// `PureFn` in safe Rust and there cannot be, so the purity axis stays
61/// **only-mitigated (C1) forever** (the module invariant + the CI byte gate are
62/// the correct terminal enforcement). Do not read `ContentKey<T>` as a purity
63/// proof — it is a decoupling proof.
64///
65/// The digest is a 32-byte BLAKE3 over `T`'s `Hash` serialization, so the key
66/// is a stable, collision-resistant content address of the value's structural
67/// fields.
68///
69/// The trait impls below are hand-written (not `#[derive]`d) so they hold for
70/// **any** `T` — the standard derive would demand `T: Clone + Eq + Hash + …`
71/// even though the only real field is the 32-byte digest (`T` lives only in a
72/// zero-size `PhantomData`).
73pub struct ContentKey<T> {
74    digest: [u8; 32],
75    _marker: PhantomData<fn() -> T>,
76}
77
78impl<T> Clone for ContentKey<T> {
79    fn clone(&self) -> Self {
80        *self
81    }
82}
83impl<T> Copy for ContentKey<T> {}
84impl<T> PartialEq for ContentKey<T> {
85    fn eq(&self, other: &Self) -> bool {
86        self.digest == other.digest
87    }
88}
89impl<T> Eq for ContentKey<T> {}
90impl<T> Hash for ContentKey<T> {
91    fn hash<H: Hasher>(&self, state: &mut H) {
92        self.digest.hash(state);
93    }
94}
95impl<T> std::fmt::Debug for ContentKey<T> {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        // Short hex prefix of the digest — enough to distinguish keys in logs.
98        write!(f, "ContentKey(")?;
99        for b in &self.digest[..4] {
100            write!(f, "{b:02x}")?;
101        }
102        write!(f, "…)")
103    }
104}
105
106/// A `std::hash::Hasher` that streams into a BLAKE3 hasher, so any `T: Hash`
107/// can be content-addressed generically (its structural read-set = exactly the
108/// bytes its `Hash` impl writes).
109struct Blake3Hasher(blake3::Hasher);
110
111impl Hasher for Blake3Hasher {
112    fn finish(&self) -> u64 {
113        // Not the content address — `ContentKey::of` uses the full 32-byte
114        // digest via `finalize()`. This exists only to satisfy the trait for
115        // the streaming `write` path; a fold of the first 8 digest bytes.
116        let bytes = self.0.finalize();
117        let mut buf = [0u8; 8];
118        buf.copy_from_slice(&bytes.as_bytes()[..8]);
119        u64::from_le_bytes(buf)
120    }
121
122    fn write(&mut self, bytes: &[u8]) {
123        self.0.update(bytes);
124    }
125}
126
127impl<T: Hash> ContentKey<T> {
128    /// Derive the content key from `input` — the **sole** constructor.
129    ///
130    /// Hashes `input`'s structural read-set (everything its `Hash` impl writes)
131    /// through BLAKE3, so the returned key is a pure, deterministic function of
132    /// `input`'s content. Two structurally-equal `T`s produce the identical key;
133    /// there is no way to construct a `ContentKey<T>` that is *not* the content
134    /// address of some `&T`.
135    #[must_use]
136    pub fn of(input: &T) -> Self {
137        let mut hasher = Blake3Hasher(blake3::Hasher::new());
138        input.hash(&mut hasher);
139        Self {
140            digest: *hasher.0.finalize().as_bytes(),
141            _marker: PhantomData,
142        }
143    }
144
145    /// The raw 32-byte BLAKE3 digest (for debugging / cross-checking).
146    #[must_use]
147    pub fn digest(&self) -> &[u8; 32] {
148        &self.digest
149    }
150}
151
152/// A single-threaded content-keyed memo. `K` is the content address; the
153/// value `Rc<V>` is a **pure function of `K`** (see the module invariant).
154/// A hit is a cheap `Rc::clone`.
155#[derive(Debug)]
156pub struct ContentMemo<K: Eq + Hash, V> {
157    map: RefCell<FxHashMap<K, Rc<V>>>,
158}
159
160impl<K: Eq + Hash, V> Default for ContentMemo<K, V> {
161    fn default() -> Self {
162        Self {
163            map: RefCell::new(FxHashMap::default()),
164        }
165    }
166}
167
168impl<K: Eq + Hash + Clone, V> ContentMemo<K, V> {
169    /// A fresh empty memo.
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Return the memoized value for `key`, computing + storing it via
176    /// `compute` on a miss. A hit is a cheap `Rc::clone` and is byte-identical
177    /// to a recompute **by the module's purity invariant** (the caller's
178    /// responsibility — `compute` must be a pure function of `key`).
179    pub fn get_or_compute(&self, key: K, compute: impl FnOnce() -> V) -> Rc<V> {
180        if let Some(hit) = self.map.borrow().get(&key).cloned() {
181            return hit;
182        }
183        // Compute OUTSIDE the borrow: `compute` may itself touch other memos.
184        let rc = Rc::new(compute());
185        self.map.borrow_mut().insert(key, rc.clone());
186        rc
187    }
188
189    /// Drop all entries. Call at a natural boundary (e.g. per top-level eval)
190    /// so stale keys from a prior computation cannot persist and collide.
191    pub fn clear(&self) {
192        self.map.borrow_mut().clear();
193    }
194
195    /// Number of memoized entries.
196    #[must_use]
197    pub fn len(&self) -> usize {
198        self.map.borrow().len()
199    }
200
201    /// Whether the memo is empty.
202    #[must_use]
203    pub fn is_empty(&self) -> bool {
204        self.map.borrow().is_empty()
205    }
206}
207
208impl<T: Hash, V> ContentMemo<ContentKey<T>, V> {
209    /// The **keyed** memo API — the M3 seal for the key↔content decoupling axis.
210    ///
211    /// Derives the [`ContentKey`] from `input` internally (so the key is
212    /// *provably* the content address of `input`, not some ambient value the
213    /// caller passed alongside it) and hands the **same `&input`** to
214    /// `compute`. A caller therefore **cannot** memoize under a key decoupled
215    /// from the computed input — the decoupling has no constructor.
216    ///
217    /// A hit is a cheap `Rc::clone` and is byte-identical to a recompute **by
218    /// the module's purity invariant** — `compute` must still be a pure
219    /// function of its `&T` argument (the C1-ceiling purity axis this seal does
220    /// NOT close; see [`ContentKey`]).
221    pub fn get_or_compute_keyed(&self, input: &T, compute: impl FnOnce(&T) -> V) -> Rc<V> {
222        let key = ContentKey::of(input);
223        if let Some(hit) = self.map.borrow().get(&key).cloned() {
224            return hit;
225        }
226        // Compute OUTSIDE the borrow: `compute` may itself touch other memos.
227        let rc = Rc::new(compute(input));
228        self.map.borrow_mut().insert(key, rc.clone());
229        rc
230    }
231}
232
233/// Declare a thread-local [`ContentMemo`] plus a memoized accessor and a clear
234/// fn — the boilerplate that was hand-rolled at every memo site. Real-easy
235/// front: one declaration replaces the `thread_local!` + `get_or_compute`
236/// wrapper + `clear` wrapper trio.
237///
238/// ```ignore
239/// use sui_intern::thread_local_content_memo;
240/// use std::collections::HashSet;
241///
242/// thread_local_content_memo! {
243///     /// referenced idents per (source-id, text-range)
244///     REFERENCED_IDENTS: (u32, rnix::TextRange) => HashSet<smol_str::SmolStr>;
245///     fn referenced_idents_memo;   // accessor: (key, || compute) -> Rc<V>
246///     fn clear_referenced_idents;  // clear the memo (per top-level eval)
247/// }
248///
249/// // hit-or-compute (byte-neutral: value is a pure fn of the key):
250/// let set = referenced_idents_memo(key, || walk_and_collect(expr));
251/// // reset at the eval boundary:
252/// clear_referenced_idents();
253/// ```
254#[macro_export]
255macro_rules! thread_local_content_memo {
256    (
257        $(#[$meta:meta])*
258        $STATIC:ident : $K:ty => $V:ty ;
259        fn $get:ident ;
260        fn $clear:ident ;
261    ) => {
262        thread_local! {
263            $(#[$meta])*
264            static $STATIC: $crate::memo::ContentMemo<$K, $V> =
265                $crate::memo::ContentMemo::new();
266        }
267
268        /// Memoized accessor — returns the cached `Rc` on a hit, else computes,
269        /// stores, and returns. `compute` MUST be a pure function of `key`
270        /// (the memo's byte-neutrality invariant).
271        #[allow(dead_code)]
272        fn $get(key: $K, compute: impl FnOnce() -> $V) -> ::std::rc::Rc<$V> {
273            $STATIC.with(|m| m.get_or_compute(key, compute))
274        }
275
276        /// Clear the thread-local memo (call at a natural boundary).
277        #[allow(dead_code)]
278        fn $clear() {
279            $STATIC.with(|m| m.clear());
280        }
281    };
282}
283
284/// `ContentKey<T>` has no public constructor other than `of(&T)`, and its
285/// fields are private — so you cannot build one from a value of the wrong type
286/// (or from raw bytes). This doctest proves the type-level seal: passing a `&B`
287/// where a `&A` is expected is a type error, not a runtime footgun.
288///
289/// ```compile_fail
290/// use sui_intern::memo::ContentKey;
291/// struct A(u32);
292/// struct B(u32);
293/// let b = B(1);
294/// // A key over A cannot be constructed from a &B — the decoupling has no path.
295/// let _k: ContentKey<A> = ContentKey::<A>::of(&b);
296/// ```
297///
298/// And the private fields cannot be hand-forged from an arbitrary digest:
299///
300/// ```compile_fail
301/// use sui_intern::memo::ContentKey;
302/// struct A(u32);
303/// // No public constructor from raw bytes; fields are private.
304/// let _k: ContentKey<A> = ContentKey { digest: [0u8; 32], _marker: Default::default() };
305/// ```
306#[allow(dead_code)]
307fn _content_key_type_seal_doc() {}
308
309#[cfg(test)]
310mod tests {
311    use super::{ContentKey, ContentMemo};
312
313    #[test]
314    fn hit_returns_same_rc_and_does_not_recompute() {
315        let memo: ContentMemo<u32, String> = ContentMemo::new();
316        let mut calls = 0;
317        let a = memo.get_or_compute(7, || {
318            calls += 1;
319            "seven".to_string()
320        });
321        let b = memo.get_or_compute(7, || {
322            calls += 1;
323            "SHOULD-NOT-RUN".to_string()
324        });
325        assert_eq!(calls, 1, "second get for the same key must not recompute");
326        assert!(std::rc::Rc::ptr_eq(&a, &b), "same key returns the same Rc");
327        assert_eq!(&*a, "seven");
328    }
329
330    #[test]
331    fn distinct_keys_are_independent() {
332        let memo: ContentMemo<u32, u32> = ContentMemo::new();
333        let a = memo.get_or_compute(1, || 10);
334        let b = memo.get_or_compute(2, || 20);
335        assert_eq!((*a, *b), (10, 20));
336        assert_eq!(memo.len(), 2);
337        assert!(!std::rc::Rc::ptr_eq(&a, &b));
338    }
339
340    #[test]
341    fn clear_drops_entries_so_recompute_happens() {
342        let memo: ContentMemo<u32, u32> = ContentMemo::new();
343        let mut calls = 0;
344        let _ = memo.get_or_compute(1, || {
345            calls += 1;
346            1
347        });
348        assert_eq!(memo.len(), 1);
349        memo.clear();
350        assert!(memo.is_empty());
351        let _ = memo.get_or_compute(1, || {
352            calls += 1;
353            1
354        });
355        assert_eq!(calls, 2, "after clear, the key recomputes");
356    }
357
358    // The macro declares fn items at module scope.
359    crate::thread_local_content_memo! {
360        /// test memo
361        TEST_MEMO: u32 => String;
362        fn tl_get;
363        fn tl_clear;
364    }
365
366    #[test]
367    fn macro_accessor_memoizes_and_clears() {
368        tl_clear();
369        let a = tl_get(3, || "three".to_string());
370        let b = tl_get(3, || "nope".to_string());
371        assert!(std::rc::Rc::ptr_eq(&a, &b));
372        assert_eq!(&*a, "three");
373        tl_clear();
374        let c = tl_get(3, || "again".to_string());
375        assert_eq!(&*c, "again", "after clear the key recomputes");
376    }
377
378    // --- M3: ContentKey<T> (the key↔content decoupling seal) ---
379
380    #[derive(Hash)]
381    struct Input {
382        name: String,
383        n: u32,
384    }
385
386    #[test]
387    fn content_key_of_is_deterministic() {
388        let a = Input {
389            name: "x".into(),
390            n: 5,
391        };
392        let b = Input {
393            name: "x".into(),
394            n: 5,
395        };
396        // Same structural content → identical key (the "key IS the content"
397        // invariant, holding by construction).
398        assert_eq!(ContentKey::of(&a), ContentKey::of(&b));
399        assert_eq!(ContentKey::of(&a).digest(), ContentKey::of(&b).digest());
400        // Re-deriving the same value is stable across calls.
401        assert_eq!(ContentKey::of(&a), ContentKey::of(&a));
402    }
403
404    #[test]
405    fn content_key_differs_on_different_content() {
406        let a = Input {
407            name: "x".into(),
408            n: 5,
409        };
410        let differ_n = Input {
411            name: "x".into(),
412            n: 6,
413        };
414        let differ_name = Input {
415            name: "y".into(),
416            n: 5,
417        };
418        assert_ne!(ContentKey::of(&a), ContentKey::of(&differ_n));
419        assert_ne!(ContentKey::of(&a), ContentKey::of(&differ_name));
420    }
421
422    #[test]
423    fn keyed_memo_round_trips_and_does_not_recompute_on_hit() {
424        let memo: ContentMemo<ContentKey<Input>, String> = ContentMemo::new();
425        let mut calls = 0;
426        let input = Input {
427            name: "hello".into(),
428            n: 2,
429        };
430
431        // Miss: compute runs, receives the SAME &input the key was derived from.
432        let a = memo.get_or_compute_keyed(&input, |i| {
433            calls += 1;
434            assert_eq!(i.name, "hello");
435            assert_eq!(i.n, 2);
436            format!("{}-{}", i.name, i.n)
437        });
438        assert_eq!(&*a, "hello-2");
439
440        // Hit on a structurally-equal but DISTINCT value: same content key, no
441        // recompute — the key is the content, not the object identity.
442        let same_content = Input {
443            name: "hello".into(),
444            n: 2,
445        };
446        let b = memo.get_or_compute_keyed(&same_content, |_| {
447            calls += 1;
448            "SHOULD-NOT-RUN".to_string()
449        });
450        assert_eq!(calls, 1, "structurally-equal input must hit, not recompute");
451        assert!(std::rc::Rc::ptr_eq(&a, &b), "hit returns the same Rc");
452
453        // Different content → miss → recompute.
454        let other = Input {
455            name: "world".into(),
456            n: 9,
457        };
458        let c = memo.get_or_compute_keyed(&other, |i| {
459            calls += 1;
460            format!("{}-{}", i.name, i.n)
461        });
462        assert_eq!(calls, 2);
463        assert_eq!(&*c, "world-9");
464        assert_eq!(memo.len(), 2);
465    }
466}