1use crate::bytecode::{FunctionBlob, FunctionHash};
7use std::collections::{HashMap, HashSet};
8use std::path::PathBuf;
9
10pub struct HotReloader {
12 current_mappings: HashMap<String, FunctionHash>,
14 blob_store: HashMap<FunctionHash, FunctionBlob>,
16 active_references: HashSet<FunctionHash>,
18 update_history: Vec<ReloadEvent>,
20 watch_paths: Vec<PathBuf>,
22}
23
24#[derive(Debug, Clone)]
26pub struct ReloadEvent {
27 pub timestamp: std::time::Instant,
28 pub old_hash: FunctionHash,
29 pub new_hash: FunctionHash,
30 pub function_name: String,
31}
32
33#[derive(Debug)]
35pub struct ReloadResult {
36 pub functions_updated: Vec<String>,
37 pub functions_unchanged: Vec<String>,
38 pub old_blobs_retained: usize,
39}
40
41#[derive(Debug, Clone)]
43pub struct FunctionPatch {
44 pub function_name: String,
45 pub old_hash: Option<FunctionHash>,
46 pub new_blob: FunctionBlob,
47}
48
49impl HotReloader {
50 pub fn new() -> Self {
51 Self {
52 current_mappings: HashMap::new(),
53 blob_store: HashMap::new(),
54 active_references: HashSet::new(),
55 update_history: Vec::new(),
56 watch_paths: Vec::new(),
57 }
58 }
59
60 pub fn register_function(&mut self, name: String, blob: FunctionBlob) {
62 let hash = blob.content_hash;
63 self.current_mappings.insert(name, hash);
64 self.blob_store.insert(hash, blob);
65 }
66
67 pub fn add_active_reference(&mut self, hash: FunctionHash) {
69 self.active_references.insert(hash);
70 }
71
72 pub fn remove_active_reference(&mut self, hash: FunctionHash) {
74 self.active_references.remove(&hash);
75 }
76
77 pub fn apply_patches(&mut self, patches: Vec<FunctionPatch>) -> ReloadResult {
79 let mut functions_updated = Vec::new();
80 let mut functions_unchanged = Vec::new();
81 let mut old_blobs_retained: usize = 0;
82
83 for patch in patches {
84 let new_hash = patch.new_blob.content_hash;
85
86 if let Some(&existing_hash) = self.current_mappings.get(&patch.function_name) {
88 if existing_hash == new_hash {
89 functions_unchanged.push(patch.function_name);
90 continue;
91 }
92
93 self.update_history.push(ReloadEvent {
95 timestamp: std::time::Instant::now(),
96 old_hash: existing_hash,
97 new_hash,
98 function_name: patch.function_name.clone(),
99 });
100
101 if self.active_references.contains(&existing_hash) {
103 old_blobs_retained += 1;
104 }
105 }
106
107 self.current_mappings
109 .insert(patch.function_name.clone(), new_hash);
110 self.blob_store.insert(new_hash, patch.new_blob);
111 functions_updated.push(patch.function_name);
112 }
113
114 ReloadResult {
115 functions_updated,
116 functions_unchanged,
117 old_blobs_retained,
118 }
119 }
120
121 pub fn current_hash(&self, name: &str) -> Option<&FunctionHash> {
123 self.current_mappings.get(name)
124 }
125
126 pub fn get_blob(&self, hash: &FunctionHash) -> Option<&FunctionBlob> {
128 self.blob_store.get(hash)
129 }
130
131 pub fn gc(&mut self) -> usize {
135 let live_hashes: HashSet<FunctionHash> = self.current_mappings.values().copied().collect();
137
138 let to_remove: Vec<FunctionHash> = self
140 .blob_store
141 .keys()
142 .filter(|hash| !live_hashes.contains(hash) && !self.active_references.contains(hash))
143 .copied()
144 .collect();
145
146 let removed = to_remove.len();
147 for hash in to_remove {
148 self.blob_store.remove(&hash);
149 }
150 removed
151 }
152
153 pub fn history(&self) -> &[ReloadEvent] {
155 &self.update_history
156 }
157
158 pub fn watch_path(&mut self, path: PathBuf) {
160 self.watch_paths.push(path);
161 }
162
163 pub fn watched_paths(&self) -> &[PathBuf] {
165 &self.watch_paths
166 }
167
168 pub fn compute_affected_functions(&self, _changed_files: &[PathBuf]) -> Vec<String> {
172 self.current_mappings.keys().cloned().collect()
173 }
174}
175
176impl Default for HotReloader {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::bytecode::{FunctionBlob, FunctionHash};
186
187 fn make_blob(name: &str, seed: u8) -> FunctionBlob {
189 let mut hash_bytes = [0u8; 32];
190 hash_bytes[0] = seed;
191 FunctionBlob {
192 content_hash: FunctionHash(hash_bytes),
193 name: name.to_string(),
194 arity: 0,
195 param_names: Vec::new(),
196 locals_count: 0,
197 is_closure: false,
198 captures_count: 0,
199 is_async: false,
200 ref_params: Vec::new(),
201 ref_mutates: Vec::new(),
202 mutable_captures: Vec::new(),
203 frame_descriptor: None,
204 instructions: Vec::new(),
205 constants: Vec::new(),
206 strings: Vec::new(),
207 required_permissions: Default::default(),
208 dependencies: Vec::new(),
209 callee_names: Vec::new(),
210 type_schemas: Vec::new(),
211 source_map: Vec::new(),
212 foreign_dependencies: Vec::new(),
213 }
214 }
215
216 #[test]
217 fn test_register_and_lookup() {
218 let mut hr = HotReloader::new();
219 let blob = make_blob("foo", 1);
220 let hash = blob.content_hash;
221 hr.register_function("foo".into(), blob);
222
223 assert_eq!(hr.current_hash("foo"), Some(&hash));
224 assert!(hr.get_blob(&hash).is_some());
225 assert_eq!(hr.get_blob(&hash).unwrap().name, "foo");
226 }
227
228 #[test]
229 fn test_apply_patches_updates_mapping() {
230 let mut hr = HotReloader::new();
231 let blob_v1 = make_blob("bar", 10);
232 hr.register_function("bar".into(), blob_v1);
233
234 let blob_v2 = make_blob("bar", 20);
235 let new_hash = blob_v2.content_hash;
236 let result = hr.apply_patches(vec![FunctionPatch {
237 function_name: "bar".into(),
238 old_hash: Some(FunctionHash([10; 32])),
239 new_blob: blob_v2,
240 }]);
241
242 assert_eq!(result.functions_updated, vec!["bar"]);
243 assert!(result.functions_unchanged.is_empty());
244 assert_eq!(hr.current_hash("bar"), Some(&new_hash));
245 assert_eq!(hr.history().len(), 1);
246 }
247
248 #[test]
249 fn test_apply_patches_unchanged_when_same_hash() {
250 let mut hr = HotReloader::new();
251 let blob = make_blob("baz", 5);
252 hr.register_function("baz".into(), blob.clone());
253
254 let result = hr.apply_patches(vec![FunctionPatch {
255 function_name: "baz".into(),
256 old_hash: None,
257 new_blob: blob,
258 }]);
259
260 assert!(result.functions_updated.is_empty());
261 assert_eq!(result.functions_unchanged, vec!["baz"]);
262 }
263
264 #[test]
265 fn test_gc_removes_unreferenced_old_blobs() {
266 let mut hr = HotReloader::new();
267 let blob_v1 = make_blob("fn1", 1);
268 let old_hash = blob_v1.content_hash;
269 hr.register_function("fn1".into(), blob_v1);
270
271 let blob_v2 = make_blob("fn1", 2);
273 hr.apply_patches(vec![FunctionPatch {
274 function_name: "fn1".into(),
275 old_hash: Some(old_hash),
276 new_blob: blob_v2,
277 }]);
278
279 assert!(hr.get_blob(&old_hash).is_some());
281
282 let removed = hr.gc();
284 assert_eq!(removed, 1);
285 assert!(hr.get_blob(&old_hash).is_none());
286 }
287
288 #[test]
289 fn test_gc_retains_actively_referenced_blobs() {
290 let mut hr = HotReloader::new();
291 let blob_v1 = make_blob("fn2", 10);
292 let old_hash = blob_v1.content_hash;
293 hr.register_function("fn2".into(), blob_v1);
294
295 hr.add_active_reference(old_hash);
297
298 let blob_v2 = make_blob("fn2", 20);
300 hr.apply_patches(vec![FunctionPatch {
301 function_name: "fn2".into(),
302 old_hash: Some(old_hash),
303 new_blob: blob_v2,
304 }]);
305
306 let removed = hr.gc();
308 assert_eq!(removed, 0);
309 assert!(hr.get_blob(&old_hash).is_some());
310
311 hr.remove_active_reference(old_hash);
313
314 let removed = hr.gc();
316 assert_eq!(removed, 1);
317 assert!(hr.get_blob(&old_hash).is_none());
318 }
319
320 #[test]
321 fn test_watch_paths() {
322 let mut hr = HotReloader::new();
323 assert!(hr.watched_paths().is_empty());
324
325 hr.watch_path(PathBuf::from("/src/main.shape"));
326 hr.watch_path(PathBuf::from("/src/lib.shape"));
327
328 assert_eq!(hr.watched_paths().len(), 2);
329 }
330
331 #[test]
332 fn test_compute_affected_functions_conservative() {
333 let mut hr = HotReloader::new();
334 hr.register_function("a".into(), make_blob("a", 1));
335 hr.register_function("b".into(), make_blob("b", 2));
336 hr.register_function("c".into(), make_blob("c", 3));
337
338 let affected = hr.compute_affected_functions(&[PathBuf::from("/src/something.shape")]);
339 assert_eq!(affected.len(), 3);
341 }
342
343 #[test]
344 fn test_old_blobs_retained_count() {
345 let mut hr = HotReloader::new();
346 let blob_v1 = make_blob("f", 1);
347 let old_hash = blob_v1.content_hash;
348 hr.register_function("f".into(), blob_v1);
349
350 hr.add_active_reference(old_hash);
352
353 let blob_v2 = make_blob("f", 2);
354 let result = hr.apply_patches(vec![FunctionPatch {
355 function_name: "f".into(),
356 old_hash: Some(old_hash),
357 new_blob: blob_v2,
358 }]);
359
360 assert_eq!(result.old_blobs_retained, 1);
361 }
362}