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}