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