Skip to main content

harn_vm/
prepared_module.rs

1//! Scoped cache of immutable, hydrated module bytecode.
2//!
3//! Prepared modules deliberately stop before runtime instantiation. Each VM
4//! still receives fresh closures, function registries, module state, and init
5//! execution; only the serialized-to-runtime bytecode conversion is reused.
6
7use std::collections::{BTreeMap, VecDeque};
8use std::num::NonZeroUsize;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use parking_lot::Mutex;
13
14use crate::chunk::{Chunk, CompiledFunction};
15use crate::module_artifact::{ModuleArtifact, ModuleImportSpec};
16
17const DEFAULT_MAX_ENTRIES: usize = 512;
18
19/// Immutable runtime form of one compiled module artifact.
20pub(crate) struct PreparedModuleArtifact {
21    pub(crate) imports: Vec<ModuleImportSpec>,
22    pub(crate) init_chunk: Option<Arc<Chunk>>,
23    pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
24    pub(crate) public_names: std::collections::HashSet<String>,
25    pub(crate) public_value_names: std::collections::HashSet<String>,
26    pub(crate) public_type_names: std::collections::HashSet<String>,
27    pub(crate) public_type_schemas: BTreeMap<String, String>,
28}
29
30impl PreparedModuleArtifact {
31    pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
32        let ModuleArtifact {
33            imports,
34            init_chunk,
35            functions,
36            public_names,
37            public_value_names,
38            public_type_names,
39            public_type_schemas,
40        } = artifact;
41        let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
42        let functions = functions
43            .into_iter()
44            .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
45            .collect();
46        Self {
47            imports,
48            init_chunk,
49            functions,
50            public_names,
51            public_value_names,
52            public_type_names,
53            public_type_schemas,
54        }
55    }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
59struct PreparedModuleCacheKey {
60    canonical_path: PathBuf,
61    source_hash: [u8; 32],
62    harn_version: &'static str,
63    codegen_fingerprint: &'static str,
64    optimizations_enabled: bool,
65}
66
67impl PreparedModuleCacheKey {
68    fn new(canonical_path: PathBuf, source: &str) -> Self {
69        Self {
70            canonical_path,
71            source_hash: *blake3::hash(source.as_bytes()).as_bytes(),
72            harn_version: crate::bytecode_cache::HARN_VERSION,
73            codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
74            optimizations_enabled: crate::compiler::CompilerOptions::from_env()
75                .optimizations_enabled(),
76        }
77    }
78}
79
80#[derive(Default)]
81struct PreparedModuleCacheInner {
82    entries: BTreeMap<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>,
83    insertion_order: VecDeque<PreparedModuleCacheKey>,
84    hits: u64,
85    misses: u64,
86    insertions: u64,
87    evictions: u64,
88}
89
90/// Typed counters for a [`PreparedModuleCache`] lifetime.
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92#[non_exhaustive]
93pub struct PreparedModuleCacheStats {
94    pub hits: u64,
95    pub misses: u64,
96    pub insertions: u64,
97    pub evictions: u64,
98    pub entries: usize,
99}
100
101/// A bounded, shareable cache of immutable module bytecode templates.
102///
103/// The handle is explicit so embedders can scope reuse to one test suite,
104/// worker, watch generation, or VM baseline. Dropping the final handle releases
105/// every prepared artifact; historical user source never accumulates globally.
106#[derive(Clone)]
107pub struct PreparedModuleCache {
108    max_entries: NonZeroUsize,
109    inner: Arc<Mutex<PreparedModuleCacheInner>>,
110}
111
112impl Default for PreparedModuleCache {
113    fn default() -> Self {
114        Self::with_capacity(
115            NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
116        )
117    }
118}
119
120impl PreparedModuleCache {
121    pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
122        Self {
123            max_entries,
124            inner: Arc::new(Mutex::new(PreparedModuleCacheInner::default())),
125        }
126    }
127
128    pub fn stats(&self) -> PreparedModuleCacheStats {
129        let inner = self.inner.lock();
130        PreparedModuleCacheStats {
131            hits: inner.hits,
132            misses: inner.misses,
133            insertions: inner.insertions,
134            evictions: inner.evictions,
135            entries: inner.entries.len(),
136        }
137    }
138
139    pub(crate) fn get(
140        &self,
141        canonical_path: &Path,
142        source: &str,
143    ) -> Option<Arc<PreparedModuleArtifact>> {
144        let key = PreparedModuleCacheKey::new(canonical_path.to_path_buf(), source);
145        let mut inner = self.inner.lock();
146        let artifact = inner.entries.get(&key).cloned();
147        if artifact.is_some() {
148            inner.hits = inner.hits.saturating_add(1);
149        } else {
150            inner.misses = inner.misses.saturating_add(1);
151        }
152        artifact
153    }
154
155    pub(crate) fn insert(
156        &self,
157        canonical_path: PathBuf,
158        source: &str,
159        artifact: Arc<PreparedModuleArtifact>,
160    ) -> Arc<PreparedModuleArtifact> {
161        let key = PreparedModuleCacheKey::new(canonical_path, source);
162        let mut inner = self.inner.lock();
163        if let Some(existing) = inner.entries.get(&key) {
164            return Arc::clone(existing);
165        }
166        while inner.entries.len() >= self.max_entries.get() {
167            let Some(oldest) = inner.insertion_order.pop_front() else {
168                break;
169            };
170            if inner.entries.remove(&oldest).is_some() {
171                inner.evictions = inner.evictions.saturating_add(1);
172            }
173        }
174        inner.insertion_order.push_back(key.clone());
175        inner.entries.insert(key, Arc::clone(&artifact));
176        inner.insertions = inner.insertions.saturating_add(1);
177        artifact
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::module_artifact::compile_module_artifact_from_source;
185    use harn_parser::TypeExpr;
186
187    fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
188        match type_expr {
189            Some(TypeExpr::List(inner)) => match inner.as_ref() {
190                TypeExpr::Named(name) => name,
191                other => panic!("expected named list element, got {other:?}"),
192            },
193            other => panic!("expected list parameter type, got {other:?}"),
194        }
195    }
196
197    fn empty_artifact() -> Arc<PreparedModuleArtifact> {
198        Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
199            imports: Vec::new(),
200            init_chunk: None,
201            functions: BTreeMap::new(),
202            public_names: Default::default(),
203            public_value_names: Default::default(),
204            public_type_names: Default::default(),
205            public_type_schemas: BTreeMap::new(),
206        }))
207    }
208
209    #[test]
210    fn bounded_cache_evicts_oldest_exact_key() {
211        let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
212        let first_source = "pub fn first() { 1 }";
213        let second_source = "pub fn second() { 2 }";
214        let first = empty_artifact();
215        let _ = cache.insert(PathBuf::from("first.harn"), first_source, first);
216        let _ = cache.insert(
217            PathBuf::from("second.harn"),
218            second_source,
219            empty_artifact(),
220        );
221
222        assert!(cache.get(Path::new("first.harn"), first_source).is_none());
223        assert!(cache.get(Path::new("second.harn"), second_source).is_some());
224        assert_eq!(cache.stats().evictions, 1);
225        assert_eq!(cache.stats().entries, 1);
226    }
227
228    #[test]
229    fn cache_key_separates_compiler_configuration() {
230        let path = PathBuf::from("module.harn");
231        let key = PreparedModuleCacheKey::new(path, "pub fn value() { 1 }");
232        let mut other_compiler = key.clone();
233        other_compiler.optimizations_enabled = !key.optimizations_enabled;
234
235        assert_ne!(key, other_compiler);
236    }
237
238    #[test]
239    fn dropping_last_cache_handle_releases_prepared_artifacts() {
240        let cache = PreparedModuleCache::default();
241        let path = PathBuf::from("module.harn");
242        let source = "pub fn value() { 1 }";
243        let artifact = empty_artifact();
244        let weak = Arc::downgrade(&artifact);
245        let _ = cache.insert(path, source, artifact);
246        let clone = cache.clone();
247
248        drop(cache);
249        assert!(weak.upgrade().is_some());
250        drop(clone);
251        assert!(weak.upgrade().is_none());
252    }
253
254    #[test]
255    fn hydration_moves_module_owned_storage() {
256        let source = r#"
257import { assert_eq } from "std/testing"
258pub type Result = {value: int}
259pub const value = 1
260pub fn answer(items: list<string>) {
261  fn nested() { return 42 }
262  return items
263}
264"#;
265        let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
266            .expect("compile typed module artifact");
267
268        let imports = artifact.imports.as_ptr();
269        let import_path = artifact.imports[0].path.as_ptr();
270        let selected_names = artifact.imports[0]
271            .selected_names
272            .as_ref()
273            .unwrap()
274            .as_ptr();
275        let selected_name = artifact.imports[0].selected_names.as_ref().unwrap()[0].as_ptr();
276        let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
277        let (function_key, function) = artifact.functions.first_key_value().unwrap();
278        let function_key = function_key.as_ptr();
279        let function_name = function.name.as_ptr();
280        let function_code = function.chunk.code.as_ptr();
281        let param_name = function.params[0].name.as_ptr();
282        let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
283        let nested_name = function.chunk.functions[0].name.as_ptr();
284        let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
285        let public_name = artifact.public_names.get("answer").unwrap().as_ptr();
286        let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
287        let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
288        let (schema_name, schema) = artifact.public_type_schemas.first_key_value().unwrap();
289        let schema_name = schema_name.as_ptr();
290        let schema = schema.as_ptr();
291
292        let hydrated = PreparedModuleArtifact::from_cached(artifact);
293
294        assert_eq!(hydrated.imports.as_ptr(), imports);
295        assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
296        assert_eq!(
297            hydrated.imports[0]
298                .selected_names
299                .as_ref()
300                .unwrap()
301                .as_ptr(),
302            selected_names
303        );
304        assert_eq!(
305            hydrated.imports[0].selected_names.as_ref().unwrap()[0].as_ptr(),
306            selected_name
307        );
308        assert_eq!(
309            hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
310            init_code
311        );
312        let (hydrated_function_key, hydrated_function) =
313            hydrated.functions.first_key_value().unwrap();
314        assert_eq!(hydrated_function_key.as_ptr(), function_key);
315        assert_eq!(hydrated_function.name.as_ptr(), function_name);
316        assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
317        assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
318        assert_eq!(
319            named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
320            param_type_name
321        );
322        assert_eq!(
323            hydrated_function.chunk.functions[0].name.as_ptr(),
324            nested_name
325        );
326        assert_eq!(
327            hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
328            nested_code
329        );
330        assert_eq!(
331            hydrated.public_names.get("answer").unwrap().as_ptr(),
332            public_name
333        );
334        assert_eq!(
335            hydrated.public_value_names.get("value").unwrap().as_ptr(),
336            public_value_name
337        );
338        assert_eq!(
339            hydrated.public_type_names.get("Result").unwrap().as_ptr(),
340            public_type_name
341        );
342        let (hydrated_schema_name, hydrated_schema) =
343            hydrated.public_type_schemas.first_key_value().unwrap();
344        assert_eq!(hydrated_schema_name.as_ptr(), schema_name);
345        assert_eq!(hydrated_schema.as_ptr(), schema);
346    }
347}