Skip to main content

geop_core_program/
cache.rs

1//! [`Cache`]: what the text of a file was made into — a STEP file's bodies —
2//! kept by the workspace, so that a file is made into the same thing once
3//! for every build that reads it (a step's preview, the program it is
4//! committed to, a part placed from it in another file), and, with a
5//! [`CacheStore`], once for every process. With a store, it keeps what
6//! the programs of the workspace built too, so that reopening one whose
7//! steps and inputs did not change builds nothing (see [`crate::kept`]).
8//!
9//! What a cache keeps is the answer to "what does this text read as", and
10//! nothing else: it is looked up by the file's path and checked against
11//! the file's whole text in memory, and against the text's digest and
12//! length, and the kernel build that made it, on disk. A value from
13//! another build of the kernel may have been made by other code, so it is
14//! made again rather than trusted.
15//!
16//! On disk, what the file `path` was made into is kept in `path.geopc`
17//! beside it: `AX-12A.STEP.geopc` (a shared library's file in a folder of
18//! the store's own, see [`cache_path`]). Its values are written exactly
19//! ([`geop_core_math::exact`]), so the value read back is the value that
20//! was made. A `.geopc` file is a cache: deleting it costs a make, and it
21//! belongs in no repository.
22
23use std::{
24    any::Any,
25    cell::{Cell, RefCell},
26    collections::BTreeMap,
27    rc::Rc,
28};
29
30#[cfg(test)]
31use geop_core_math::geop_error::GeopError;
32use geop_core_math::geop_error::GeopResult;
33use serde::{Deserialize, Serialize, de::DeserializeOwned};
34
35use crate::{kept::Builds, workspace::is_shared};
36
37/// The extension of a cache file: what the file it is beside was made
38/// into ([`Cache`]).
39pub const CACHE_EXTENSION: &str = "geopc";
40
41/// Where a [`Cache`] keeps what it made between processes: a folder on
42/// disk, for a kernel run by the command line or an editor's host process.
43pub trait CacheStore {
44    /// What tells this build of the kernel from every other: what it made
45    /// is only read back by the same build.
46    fn build(&self) -> &str;
47    /// The text of the cache file `path`, if there is one.
48    fn read(&self, path: &str) -> Option<String>;
49    /// Writes the cache file `path`. A cache that could not be written is
50    /// made again next time, nothing else, so a failure is the store's to
51    /// report, if anywhere.
52    fn write(&self, path: &str, text: &str);
53}
54
55/// What a [`Cache`] keeps in memory, by the kind of value and the file's
56/// path: the text it was made from, and what it was made into.
57type Kept = BTreeMap<(&'static str, String), (Rc<str>, Rc<dyn Any>)>;
58
59/// What the text of a file was made into, kept by the file's path (see
60/// the [module](self)).
61#[derive(Default)]
62pub struct Cache {
63    store: Option<Box<dyn CacheStore>>,
64    kept: RefCell<Kept>,
65    /// How many values it made: what a test of what is made again counts.
66    made: Cell<u64>,
67    /// What the program of each file built, as kept in the store, by the
68    /// file's path: read when a run first asks, written by [`Cache::flush`].
69    builds: RefCell<BTreeMap<String, Builds>>,
70}
71
72/// A cache file: what the file beside it was made into, and from what.
73#[derive(Serialize, Deserialize)]
74struct Entry<T> {
75    /// The build of the kernel that made it ([`CacheStore::build`]).
76    build: String,
77    /// What it was made into: [`Cache::made_of`]'s `kind`.
78    kind: String,
79    /// The text it was made from: its digest and length ([`source`]).
80    source: String,
81    value: T,
82}
83
84impl Cache {
85    /// A cache kept in memory only.
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// A cache kept in memory, and between processes in `store`.
91    pub fn stored(store: Box<dyn CacheStore>) -> Self {
92        Self {
93            store: Some(store),
94            ..Self::default()
95        }
96    }
97
98    /// What the text `text` of the file `path` reads as, as `kind`: made by
99    /// `make` once, for this text of this file, and kept. A value that
100    /// could not be made is not kept: it is made again when asked again.
101    pub fn made_of<T: Serialize + DeserializeOwned + 'static>(
102        &self,
103        kind: &'static str,
104        path: &str,
105        text: &Rc<str>,
106        make: impl FnOnce(&str) -> GeopResult<T>,
107    ) -> GeopResult<Rc<T>> {
108        let key = (kind, path.to_string());
109        if let Some((from, value)) = self.kept.borrow().get(&key)
110            && (Rc::ptr_eq(from, text) || **from == **text)
111            && let Ok(value) = value.clone().downcast::<T>()
112        {
113            return Ok(value);
114        }
115        let value = match self.read(kind, path, text) {
116            Some(value) => Rc::new(value),
117            None => {
118                self.made.set(self.made.get() + 1);
119                let value = Rc::new(make(text)?);
120                self.write(kind, path, text, &*value);
121                value
122            }
123        };
124        let kept: Rc<dyn Any> = value.clone();
125        self.kept.borrow_mut().insert(key, (text.clone(), kept));
126        Ok(value)
127    }
128
129    /// How many values it made, rather than found kept.
130    pub fn made(&self) -> u64 {
131        self.made.get()
132    }
133
134    /// `use_builds` of what the program in the file `path` built, as its
135    /// store keeps it — read from the store the first time — if it keeps
136    /// builds of that file: it has a store, and the program is in a file.
137    pub(crate) fn builds<R>(
138        &self,
139        path: &str,
140        use_builds: impl FnOnce(&mut Builds) -> R,
141    ) -> Option<R> {
142        let store = self.store.as_ref().filter(|_| !path.is_empty())?;
143        let mut builds = self.builds.borrow_mut();
144        let builds = (builds.entry(path.to_string())).or_insert_with(|| {
145            Builds::read(store.read(&cache_path(path)).as_deref(), store.build())
146        });
147        Some(use_builds(builds))
148    }
149
150    /// Writes what the programs built since it was last written, each into
151    /// its file's cache file: what an editor does when an edit is done, and
152    /// the command line when it has built. Nothing is written of a file
153    /// whose builds did not change.
154    pub fn flush(&self) {
155        let Some(store) = self.store.as_ref() else {
156            return;
157        };
158        for (path, builds) in self.builds.borrow_mut().iter_mut() {
159            if let Some(text) = builds.write(store.build()) {
160                store.write(&cache_path(path), &text);
161            }
162        }
163    }
164
165    /// What its store kept of `text` of `path` as `kind`, if it was made by
166    /// this build from this text. A file that does not read, of another
167    /// build or text or kind, is none.
168    fn read<T: DeserializeOwned>(&self, kind: &str, path: &str, text: &str) -> Option<T> {
169        let store = self.store.as_ref()?;
170        let kept = store.read(&cache_path(path))?;
171        let entry: Entry<T> = serde_json::from_str(&kept).ok()?;
172        (entry.build == store.build() && entry.kind == kind && entry.source == source(text))
173            .then_some(entry.value)
174    }
175
176    /// Keeps `value`, made of `text` of `path` as `kind`, in its store.
177    fn write<T: Serialize>(&self, kind: &str, path: &str, text: &str, value: &T) {
178        let Some(store) = self.store.as_ref() else {
179            return;
180        };
181        let entry = Entry {
182            build: store.build().to_string(),
183            kind: kind.to_string(),
184            source: source(text),
185            value,
186        };
187        // A value that does not serialize — an enclosure gone infinite — is
188        // only not kept.
189        if let Ok(written) = serde_json::to_string(&entry) {
190            store.write(&cache_path(path), &written);
191        }
192    }
193}
194
195/// The cache file of the file `path`: beside it, `path.geopc` — or, for a
196/// file of a library every workspace shares, which is not where the files
197/// of the workspace are (see [`is_shared`]), in a folder of the store's
198/// own, by the library's scheme: `std:hex_nut.geop` in
199/// `.geopc/std/hex_nut.geop.geopc`.
200pub fn cache_path(path: &str) -> String {
201    match path.split_once(':').filter(|_| is_shared(path)) {
202        Some((scheme, name)) => format!(".{CACHE_EXTENSION}/{scheme}/{name}.{CACHE_EXTENSION}"),
203        None => format!("{path}.{CACHE_EXTENSION}"),
204    }
205}
206
207/// What tells a text from others: its digest and its length.
208fn source(text: &str) -> String {
209    format!("{:032x}:{}", digest(text), text.len())
210}
211
212/// A 128-bit FNV-1a hash of `text`: written out here rather than the
213/// standard library's hasher, which may change between Rust versions, so
214/// that what is saved of a text keeps naming the same text.
215pub fn digest(text: &str) -> u128 {
216    const PRIME: u128 = 0x0000000001000000000000000000013B;
217    let mut hash: u128 = 0x6c62272e07bb014262b821756295c58d;
218    for byte in text.bytes() {
219        hash ^= u128::from(byte);
220        hash = hash.wrapping_mul(PRIME);
221    }
222    hash
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::testing::Shelf;
229
230    fn words(text: &str) -> GeopResult<Vec<String>> {
231        Ok(text.split_whitespace().map(str::to_string).collect())
232    }
233
234    /// A text is made into its value once; another text of the file is made
235    /// again; a text that cannot be made is not kept.
236    #[test]
237    fn a_text_is_made_once() {
238        let cache = Cache::new();
239        let text: Rc<str> = Rc::from("a b");
240        let first = cache.made_of("words", "f.txt", &text, words).unwrap();
241        let again = cache
242            .made_of("words", "f.txt", &Rc::from("a b"), words)
243            .unwrap();
244        assert!(Rc::ptr_eq(&first, &again));
245        assert_eq!(cache.made(), 1);
246        let other = cache
247            .made_of("words", "f.txt", &Rc::from("c"), words)
248            .unwrap();
249        assert_eq!(*other, ["c"]);
250        assert_eq!(cache.made(), 2);
251        let failing = |_: &str| -> GeopResult<Vec<String>> { Err(GeopError::new("no")) };
252        assert!(cache.made_of("words", "g.txt", &text, failing).is_err());
253        assert!(cache.made_of("words", "g.txt", &text, words).is_ok());
254        assert_eq!(cache.made(), 4);
255    }
256
257    /// What one process made is read back by the next of the same build,
258    /// from `path.geopc` — and made again by another build, or from another
259    /// text.
260    #[test]
261    fn a_store_keeps_what_was_made_between_processes() {
262        let shelf = Shelf::of("1");
263        let text: Rc<str> = Rc::from("a b");
264        let made = |shelf: &Shelf, text: &Rc<str>| {
265            let cache = shelf.cache();
266            let value = cache.made_of("words", "f.txt", text, words).unwrap();
267            (value, cache.made())
268        };
269        assert_eq!(made(&shelf, &text), (Rc::new(words("a b").unwrap()), 1));
270        assert!(shelf.files.borrow().contains_key("f.txt.geopc"));
271        assert_eq!(made(&shelf, &text), (Rc::new(words("a b").unwrap()), 0));
272        assert_eq!(made(&shelf, &Rc::from("a c")).1, 1);
273        let rebuilt = Shelf {
274            build: "2".into(),
275            ..shelf
276        };
277        assert_eq!(made(&rebuilt, &Rc::from("a c")).1, 1);
278    }
279}