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<()> {
63 let has_closure_layouts = program
64 .closure_function_layouts
65 .iter()
66 .any(|opt| opt.is_some());
67 let has_ca_closure_layouts = program
68 .content_addressed
69 .as_ref()
70 .map(|ca| !ca.closure_function_layouts_by_name.is_empty())
71 .unwrap_or(false);
72 let has_trait_vtables = !program.trait_vtables.is_empty();
79 let has_ca_trait_vtables = program
80 .content_addressed
81 .as_ref()
82 .map(|ca| !ca.trait_vtables.is_empty())
83 .unwrap_or(false);
84 if has_closure_layouts || has_ca_closure_layouts
85 || has_trait_vtables || has_ca_trait_vtables
86 {
87 return Ok(());
89 }
90 let key = Self::cache_key(source);
91 let path = self.cache_path(&key);
92 let data = rmp_serde::to_vec(program)
93 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
94 std::fs::write(&path, data)
95 }
96
97 pub fn clear(&self) -> std::io::Result<()> {
99 for entry in std::fs::read_dir(&self.cache_dir)? {
100 let entry = entry?;
101 if entry
102 .path()
103 .extension()
104 .map_or(false, |ext| ext == "shapec")
105 {
106 std::fs::remove_file(entry.path())?;
107 }
108 }
109 Ok(())
110 }
111
112 fn cache_key(source: &str) -> String {
116 let mut hasher = Sha256::new();
117 hasher.update(source.as_bytes());
118 hasher.update(b"\0");
119 hasher.update(COMPILER_VERSION.as_bytes());
120 format!("{:x}", hasher.finalize())
121 }
122
123 fn cache_path(&self, key: &str) -> PathBuf {
125 self.cache_dir.join(format!("{}.shapec", key))
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn temp_cache() -> (BytecodeCache, tempfile::TempDir) {
134 let tmp = tempfile::tempdir().unwrap();
135 let cache = BytecodeCache::with_dir(tmp.path().join("bytecode")).unwrap();
136 (cache, tmp)
137 }
138
139 #[test]
140 fn test_put_get_roundtrip() {
141 let (cache, _tmp) = temp_cache();
142 let program = BytecodeProgram::new();
143 cache.put("let x = 1", &program).unwrap();
144 let cached = cache.get("let x = 1");
145 assert!(cached.is_some(), "Cache hit expected after put");
146 }
147
148 #[test]
149 fn test_cache_miss() {
150 let (cache, _tmp) = temp_cache();
151 let result = cache.get("nonexistent source");
152 assert!(result.is_none(), "Cache miss expected for unknown source");
153 }
154
155 #[test]
156 fn test_different_source_different_key() {
157 let (cache, _tmp) = temp_cache();
158 let program = BytecodeProgram::new();
159 cache.put("let x = 1", &program).unwrap();
160 let result = cache.get("let x = 2");
161 assert!(result.is_none(), "Different source should miss cache");
162 }
163
164 #[test]
165 fn test_clear() {
166 let (cache, _tmp) = temp_cache();
167 let program = BytecodeProgram::new();
168 cache.put("source_a", &program).unwrap();
169 cache.put("source_b", &program).unwrap();
170
171 cache.clear().unwrap();
172
173 assert!(
174 cache.get("source_a").is_none(),
175 "Cache should be empty after clear"
176 );
177 assert!(
178 cache.get("source_b").is_none(),
179 "Cache should be empty after clear"
180 );
181 }
182
183 #[test]
184 fn test_cache_key_deterministic() {
185 let key1 = BytecodeCache::cache_key("hello");
186 let key2 = BytecodeCache::cache_key("hello");
187 assert_eq!(key1, key2, "Same source should produce same key");
188 }
189
190 #[test]
191 fn test_cache_key_different_for_different_source() {
192 let key1 = BytecodeCache::cache_key("hello");
193 let key2 = BytecodeCache::cache_key("world");
194 assert_ne!(key1, key2, "Different source should produce different key");
195 }
196}