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