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