dmc/engine/cache.rs
1//! Persistent per-file compile cache. Skips lex/parse/transform/codegen
2//! (and the sidecar dispatch) for unchanged inputs by hashing the source
3//! together with the build config and stashing the resulting record on
4//! disk. Cache hits are O(read JSON + parse).
5//!
6//! Default location: `<output_dir>/.cache/dmc/`. One file per record,
7//! named `{16-hex blake3}.json`.
8//!
9//! Invalidation strategy: the key encodes the dmc version, the file
10//! source bytes, the source path, and a serialised view of the relevant
11//! `CompileConfig` fields. Any change to source or config produces a
12//! different hash; nothing is ever overwritten in place.
13
14use blake3::Hasher;
15use serde_json::Value;
16use std::path::{Path, PathBuf};
17
18const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20/// File-backed key/value store. One file per cached record.
21#[derive(Debug, Clone)]
22pub struct FileCache {
23 dir: PathBuf,
24}
25
26impl FileCache {
27 /// Open or create the cache at `dir`. Returns `None` (cache disabled)
28 /// if the directory could not be created.
29 pub fn open(dir: PathBuf) -> Option<Self> {
30 std::fs::create_dir_all(&dir).ok()?;
31 Some(Self { dir })
32 }
33
34 /// Compute a hex key for a cache entry. Inputs:
35 /// - dmc version (changes invalidate every entry)
36 /// - file source bytes
37 /// - file path (so two identical-content files at different paths
38 /// don't collide)
39 /// - opaque config fingerprint (caller-controlled)
40 pub fn key(source: &[u8], path: &Path, cfg_fingerprint: &[u8]) -> String {
41 let mut h = Hasher::new();
42 h.update(b"dmc/v1");
43 h.update(VERSION.as_bytes());
44 h.update(b"\0src\0");
45 h.update(source);
46 h.update(b"\0path\0");
47 h.update(path.to_string_lossy().as_bytes());
48 h.update(b"\0cfg\0");
49 h.update(cfg_fingerprint);
50 let hex = h.finalize().to_hex();
51 hex.as_str()[..16].to_string()
52 }
53
54 /// Load the record for `key`, returning `None` on miss or read error.
55 pub fn get(&self, key: &str) -> Option<Value> {
56 let p = self.path_for(key);
57 let s = std::fs::read_to_string(p).ok()?;
58 serde_json::from_str(&s).ok()
59 }
60
61 /// Write `value` under `key`. Errors are silently ignored; a cache
62 /// failure must never break the build.
63 pub fn put(&self, key: &str, value: &Value) {
64 let p = self.path_for(key);
65 if let Ok(json) = serde_json::to_string(value) {
66 let _ = std::fs::write(p, json);
67 }
68 }
69
70 fn path_for(&self, key: &str) -> PathBuf {
71 self.dir.join(format!("{key}.json"))
72 }
73}
74
75/// Hash any `Serialize`-able config snippet into an opaque fingerprint.
76/// Returns the empty vec on serialisation failure (cache still works,
77/// just collides across configs that fail to serialise).
78pub fn fingerprint<T: serde::Serialize>(cfg: &T) -> Vec<u8> {
79 let Ok(json) = serde_json::to_vec(cfg) else { return Vec::new() };
80 blake3::hash(&json).as_bytes().to_vec()
81}