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};
17
18const DEFAULT_MAX_ENTRIES: usize = 512;
19
20pub(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#[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#[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}