shape_vm/
bytecode_cache.rs1use sha2::{Digest, Sha256};
8use std::path::PathBuf;
9
10use crate::bytecode::BytecodeProgram;
11
12const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
14
15pub struct BytecodeCache {
17 cache_dir: PathBuf,
18}
19
20impl BytecodeCache {
21 pub fn new() -> Option<Self> {
26 let home = dirs::home_dir()?;
27 let cache_dir = home.join(".shape").join("cache").join("bytecode");
28 std::fs::create_dir_all(&cache_dir).ok()?;
29 Some(Self { cache_dir })
30 }
31
32 pub fn with_dir(cache_dir: PathBuf) -> std::io::Result<Self> {
34 std::fs::create_dir_all(&cache_dir)?;
35 Ok(Self { cache_dir })
36 }
37
38 pub fn get(&self, source: &str) -> Option<BytecodeProgram> {
42 let key = Self::cache_key(source);
43 let path = self.cache_path(&key);
44 let data = std::fs::read(&path).ok()?;
45 rmp_serde::from_slice(&data).ok()
46 }
47
48 pub fn put(&self, source: &str, program: &BytecodeProgram) -> std::io::Result<()> {
50 let key = Self::cache_key(source);
51 let path = self.cache_path(&key);
52 let data = rmp_serde::to_vec(program)
53 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
54 std::fs::write(&path, data)
55 }
56
57 pub fn clear(&self) -> std::io::Result<()> {
59 for entry in std::fs::read_dir(&self.cache_dir)? {
60 let entry = entry?;
61 if entry
62 .path()
63 .extension()
64 .map_or(false, |ext| ext == "shapec")
65 {
66 std::fs::remove_file(entry.path())?;
67 }
68 }
69 Ok(())
70 }
71
72 fn cache_key(source: &str) -> String {
76 let mut hasher = Sha256::new();
77 hasher.update(source.as_bytes());
78 hasher.update(b"\0");
79 hasher.update(COMPILER_VERSION.as_bytes());
80 format!("{:x}", hasher.finalize())
81 }
82
83 fn cache_path(&self, key: &str) -> PathBuf {
85 self.cache_dir.join(format!("{}.shapec", key))
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn temp_cache() -> (BytecodeCache, tempfile::TempDir) {
94 let tmp = tempfile::tempdir().unwrap();
95 let cache = BytecodeCache::with_dir(tmp.path().join("bytecode")).unwrap();
96 (cache, tmp)
97 }
98
99 #[test]
100 fn test_put_get_roundtrip() {
101 let (cache, _tmp) = temp_cache();
102 let program = BytecodeProgram::new();
103 cache.put("let x = 1", &program).unwrap();
104 let cached = cache.get("let x = 1");
105 assert!(cached.is_some(), "Cache hit expected after put");
106 }
107
108 #[test]
109 fn test_cache_miss() {
110 let (cache, _tmp) = temp_cache();
111 let result = cache.get("nonexistent source");
112 assert!(result.is_none(), "Cache miss expected for unknown source");
113 }
114
115 #[test]
116 fn test_different_source_different_key() {
117 let (cache, _tmp) = temp_cache();
118 let program = BytecodeProgram::new();
119 cache.put("let x = 1", &program).unwrap();
120 let result = cache.get("let x = 2");
121 assert!(result.is_none(), "Different source should miss cache");
122 }
123
124 #[test]
125 fn test_clear() {
126 let (cache, _tmp) = temp_cache();
127 let program = BytecodeProgram::new();
128 cache.put("source_a", &program).unwrap();
129 cache.put("source_b", &program).unwrap();
130
131 cache.clear().unwrap();
132
133 assert!(
134 cache.get("source_a").is_none(),
135 "Cache should be empty after clear"
136 );
137 assert!(
138 cache.get("source_b").is_none(),
139 "Cache should be empty after clear"
140 );
141 }
142
143 #[test]
144 fn test_cache_key_deterministic() {
145 let key1 = BytecodeCache::cache_key("hello");
146 let key2 = BytecodeCache::cache_key("hello");
147 assert_eq!(key1, key2, "Same source should produce same key");
148 }
149
150 #[test]
151 fn test_cache_key_different_for_different_source() {
152 let key1 = BytecodeCache::cache_key("hello");
153 let key2 = BytecodeCache::cache_key("world");
154 assert_ne!(key1, key2, "Different source should produce different key");
155 }
156}