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