Skip to main content

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}