1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! Bytecode cache for compiled Shape modules
//!
//! Caches compiled bytecode programs on disk as `.shapec` files,
//! keyed by the SHA-256 hash of the source content + compiler version.
//! This avoids redundant recompilation when source files haven't changed.
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use crate::bytecode::BytecodeProgram;
/// Compiler version embedded in cache keys to invalidate on upgrades
const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// On-disk bytecode cache stored under `~/.shape/cache/bytecode/`
pub struct BytecodeCache {
cache_dir: PathBuf,
}
impl BytecodeCache {
/// Create a new bytecode cache, creating the cache directory if needed.
///
/// The cache lives at `~/.shape/cache/bytecode/`. Returns `None` if the
/// home directory cannot be determined or the directory cannot be created.
pub fn new() -> Option<Self> {
let home = dirs::home_dir()?;
let cache_dir = home.join(".shape").join("cache").join("bytecode");
std::fs::create_dir_all(&cache_dir).ok()?;
Some(Self { cache_dir })
}
/// Create a cache at a specific directory (for testing).
pub fn with_dir(cache_dir: PathBuf) -> std::io::Result<Self> {
std::fs::create_dir_all(&cache_dir)?;
Ok(Self { cache_dir })
}
/// Look up cached bytecode for the given source content.
///
/// Returns `Some(program)` on cache hit, `None` on miss or deserialization error.
pub fn get(&self, source: &str) -> Option<BytecodeProgram> {
let key = Self::cache_key(source);
let path = self.cache_path(&key);
let data = std::fs::read(&path).ok()?;
rmp_serde::from_slice(&data).ok()
}
/// Store compiled bytecode for the given source content.
///
/// **W17-make-closure note.** Programs whose compiler produced any
/// `closure_function_layouts` entries (i.e. the source contains at
/// least one closure literal) are not cached. The `ClosureLayout`
/// side-table is currently `#[serde(skip)]` on `BytecodeProgram`
/// and `ContentAddressedProgram` — a deserialized program would
/// load with an empty layouts vector, and `op_make_closure` would
/// then surface `no ClosureLayout registered for function N`
/// (`crates/shape-vm/src/executor/control_flow/mod.rs:447`). Until
/// `ClosureLayout` (+ `ConcreteType` / `FieldInfo` / `NativeKind` /
/// `CaptureKind`) grow `Serialize`/`Deserialize` derives, the safe
/// disposition is to skip caching for closure-bearing programs.
/// Caching for closure-free programs remains in force.
pub fn put(&self, source: &str, program: &BytecodeProgram) -> std::io::Result<()> {
let has_closure_layouts = program
.closure_function_layouts
.iter()
.any(|opt| opt.is_some());
let has_ca_closure_layouts = program
.content_addressed
.as_ref()
.map(|ca| !ca.closure_function_layouts_by_name.is_empty())
.unwrap_or(false);
// ADR-006 §2.7.24 Q25.C: same `#[serde(skip)]` concern applies
// to `trait_vtables` — vtables hold `Arc<VTable>` which is not
// a stable wire shape; a deserialized program would load with
// empty vtables and `op_box_trait_object` would fail with
// "no vtable registered". Skip caching for trait-bearing
// programs until a kinded vtable wire format lands.
let has_trait_vtables = !program.trait_vtables.is_empty();
let has_ca_trait_vtables = program
.content_addressed
.as_ref()
.map(|ca| !ca.trait_vtables.is_empty())
.unwrap_or(false);
if has_closure_layouts || has_ca_closure_layouts
|| has_trait_vtables || has_ca_trait_vtables
{
// Skip caching for closure-bearing OR trait-bearing programs.
return Ok(());
}
let key = Self::cache_key(source);
let path = self.cache_path(&key);
let data = rmp_serde::to_vec(program)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
std::fs::write(&path, data)
}
/// Remove all cached bytecode files.
pub fn clear(&self) -> std::io::Result<()> {
for entry in std::fs::read_dir(&self.cache_dir)? {
let entry = entry?;
if entry
.path()
.extension()
.map_or(false, |ext| ext == "shapec")
{
std::fs::remove_file(entry.path())?;
}
}
Ok(())
}
/// Compute the cache key for a source string.
///
/// Key = SHA-256(source_content + "\0" + compiler_version) as hex.
fn cache_key(source: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(source.as_bytes());
hasher.update(b"\0");
hasher.update(COMPILER_VERSION.as_bytes());
format!("{:x}", hasher.finalize())
}
/// Map a cache key to a file path: `<cache_dir>/<key>.shapec`
fn cache_path(&self, key: &str) -> PathBuf {
self.cache_dir.join(format!("{}.shapec", key))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_cache() -> (BytecodeCache, tempfile::TempDir) {
let tmp = tempfile::tempdir().unwrap();
let cache = BytecodeCache::with_dir(tmp.path().join("bytecode")).unwrap();
(cache, tmp)
}
#[test]
fn test_put_get_roundtrip() {
let (cache, _tmp) = temp_cache();
let program = BytecodeProgram::new();
cache.put("let x = 1", &program).unwrap();
let cached = cache.get("let x = 1");
assert!(cached.is_some(), "Cache hit expected after put");
}
#[test]
fn test_cache_miss() {
let (cache, _tmp) = temp_cache();
let result = cache.get("nonexistent source");
assert!(result.is_none(), "Cache miss expected for unknown source");
}
#[test]
fn test_different_source_different_key() {
let (cache, _tmp) = temp_cache();
let program = BytecodeProgram::new();
cache.put("let x = 1", &program).unwrap();
let result = cache.get("let x = 2");
assert!(result.is_none(), "Different source should miss cache");
}
#[test]
fn test_clear() {
let (cache, _tmp) = temp_cache();
let program = BytecodeProgram::new();
cache.put("source_a", &program).unwrap();
cache.put("source_b", &program).unwrap();
cache.clear().unwrap();
assert!(
cache.get("source_a").is_none(),
"Cache should be empty after clear"
);
assert!(
cache.get("source_b").is_none(),
"Cache should be empty after clear"
);
}
#[test]
fn test_cache_key_deterministic() {
let key1 = BytecodeCache::cache_key("hello");
let key2 = BytecodeCache::cache_key("hello");
assert_eq!(key1, key2, "Same source should produce same key");
}
#[test]
fn test_cache_key_different_for_different_source() {
let key1 = BytecodeCache::cache_key("hello");
let key2 = BytecodeCache::cache_key("world");
assert_ne!(key1, key2, "Different source should produce different key");
}
}