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