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::{
17 compile_module_artifact_from_source, compile_module_artifact_from_source_with_imported_enums,
18 ModuleArtifact, ModuleImportSpec,
19};
20use crate::module_source::ModuleSource;
21use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
22const DEFAULT_MAX_ENTRIES: usize = 512;
23
24pub(crate) struct PreparedModuleArtifact {
26 pub(crate) imports: Vec<ModuleImportSpec>,
27 pub(crate) type_schema_init_chunk: Option<Arc<Chunk>>,
28 pub(crate) init_chunk: Option<Arc<Chunk>>,
29 pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
30 pub(crate) public_exports: BTreeMap<String, DefKind>,
31 pub(crate) public_value_names: std::collections::HashSet<String>,
32 pub(crate) public_type_names: std::collections::HashSet<String>,
33}
34
35impl PreparedModuleArtifact {
36 pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
37 let ModuleArtifact {
38 imports,
39 type_schema_init_chunk,
40 init_chunk,
41 functions,
42 public_exports,
43 public_value_names,
44 public_type_names,
45 } = artifact;
46 let type_schema_init_chunk =
47 type_schema_init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
48 let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
49 let functions = functions
50 .into_iter()
51 .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
52 .collect();
53 Self {
54 imports,
55 type_schema_init_chunk,
56 init_chunk,
57 functions,
58 public_exports,
59 public_value_names,
60 public_type_names,
61 }
62 }
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
66struct PreparedModuleCacheKey {
67 canonical_path: PathBuf,
68 source_hash: [u8; 32],
69 harn_version: &'static str,
70 codegen_fingerprint: &'static str,
71 optimizations_enabled: bool,
72}
73
74impl PreparedModuleCacheKey {
75 fn new(canonical_path: PathBuf, source_hash: [u8; 32]) -> Self {
80 Self {
81 canonical_path,
82 source_hash,
83 harn_version: crate::bytecode_cache::HARN_VERSION,
84 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
85 optimizations_enabled: crate::compiler::CompilerOptions::from_env()
86 .optimizations_enabled(),
87 }
88 }
89}
90
91#[derive(Default)]
92struct PreparedModuleCacheInner {
93 entries: BTreeMap<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>,
94 insertion_order: VecDeque<PreparedModuleCacheKey>,
95 hits: u64,
96 misses: u64,
97 insertions: u64,
98 evictions: u64,
99}
100
101#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
103#[non_exhaustive]
104pub struct PreparedModuleCacheStats {
105 pub hits: u64,
106 pub misses: u64,
107 pub insertions: u64,
108 pub evictions: u64,
109 pub entries: usize,
110}
111
112#[derive(Clone)]
118pub struct PreparedModuleCache {
119 max_entries: NonZeroUsize,
120 inner: Arc<Mutex<PreparedModuleCacheInner>>,
121}
122
123impl Default for PreparedModuleCache {
124 fn default() -> Self {
125 Self::with_capacity(
126 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
127 )
128 }
129}
130
131impl PreparedModuleCache {
132 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
133 Self {
134 max_entries,
135 inner: Arc::new(Mutex::new(PreparedModuleCacheInner::default())),
136 }
137 }
138
139 pub fn stats(&self) -> PreparedModuleCacheStats {
140 let inner = self.inner.lock();
141 PreparedModuleCacheStats {
142 hits: inner.hits,
143 misses: inner.misses,
144 insertions: inner.insertions,
145 evictions: inner.evictions,
146 entries: inner.entries.len(),
147 }
148 }
149
150 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
157 if roots.is_empty() {
158 return ModulePhaseStats::default();
159 }
160
161 let graph = harn_modules::build(roots);
162 let root_paths = roots
163 .iter()
164 .map(|path| harn_modules::canonical_path(path))
165 .collect::<std::collections::HashSet<_>>();
166 let recorder = ModulePhaseRecorder::new();
167
168 for path in graph.module_paths() {
169 if root_paths.contains(&harn_modules::canonical_path(&path)) {
170 continue;
171 }
172 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
173 let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
174 continue;
175 }
176
177 let source = {
178 let _load_span = recorder.load_span();
179 match crate::module_source::read(&path) {
180 Ok(source) => source,
181 Err(_) => continue,
182 }
183 };
184 let mut imported_enum_candidates = graph
185 .imported_names_by_kind_for_file(&path, DefKind::Enum)
186 .unwrap_or_default()
187 .into_iter()
188 .collect::<Vec<_>>();
189 imported_enum_candidates.sort_unstable();
190 let canonical = harn_modules::canonical_path(&path);
191 let _ = self.prepare(
192 &path,
193 &canonical,
194 &source,
195 Some(&imported_enum_candidates),
196 Some(&recorder),
197 );
198 }
199
200 recorder.snapshot()
201 }
202
203 pub(crate) fn get(
204 &self,
205 canonical_path: &Path,
206 source_hash: [u8; 32],
207 ) -> Option<Arc<PreparedModuleArtifact>> {
208 let key = PreparedModuleCacheKey::new(canonical_path.to_path_buf(), source_hash);
209 let mut inner = self.inner.lock();
210 let artifact = inner.entries.get(&key).cloned();
211 if artifact.is_some() {
212 inner.hits = inner.hits.saturating_add(1);
213 } else {
214 inner.misses = inner.misses.saturating_add(1);
215 }
216 artifact
217 }
218
219 pub(crate) fn insert(
220 &self,
221 canonical_path: PathBuf,
222 source_hash: [u8; 32],
223 artifact: Arc<PreparedModuleArtifact>,
224 ) -> Arc<PreparedModuleArtifact> {
225 let key = PreparedModuleCacheKey::new(canonical_path, source_hash);
226 let mut inner = self.inner.lock();
227 if let Some(existing) = inner.entries.get(&key) {
228 return Arc::clone(existing);
229 }
230 while inner.entries.len() >= self.max_entries.get() {
231 let Some(oldest) = inner.insertion_order.pop_front() else {
232 break;
233 };
234 if inner.entries.remove(&oldest).is_some() {
235 inner.evictions = inner.evictions.saturating_add(1);
236 }
237 }
238 inner.insertion_order.push_back(key.clone());
239 inner.entries.insert(key, Arc::clone(&artifact));
240 inner.insertions = inner.insertions.saturating_add(1);
241 artifact
242 }
243
244 pub(crate) fn prepare(
245 &self,
246 source_path: &Path,
247 canonical_path: &Path,
248 source: &ModuleSource,
249 imported_enum_candidates: Option<&[String]>,
250 recorder: Option<&ModulePhaseRecorder>,
251 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
252 let prepared = {
253 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
254 self.get(canonical_path, source.sha256())
255 };
256 if let Some(prepared) = prepared {
257 return Ok(prepared);
258 }
259
260 let lookup = {
264 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
265 crate::bytecode_cache::load_module(source_path, source)
266 };
267 let cached = if let Some(artifact) = lookup.artifact {
268 artifact
269 } else {
270 let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
271 let compiled = match imported_enum_candidates {
272 Some(candidates) => compile_module_artifact_from_source_with_imported_enums(
273 source_path,
274 source.as_str(),
275 candidates.iter().cloned(),
276 )?,
277 None => compile_module_artifact_from_source(source_path, source.as_str())?,
278 };
279 if let Some(span) = &mut compile_span {
280 span.mark_compile_succeeded();
281 }
282 drop(compile_span);
283 if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
284 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
285 eprintln!(
286 "[harn] module cache write skipped for {}: {err}",
287 source_path.display()
288 );
289 }
290 }
291 compiled
292 };
293 let prepared = {
294 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
295 Arc::new(PreparedModuleArtifact::from_cached(cached))
296 };
297 Ok(self.insert(canonical_path.to_path_buf(), source.sha256(), prepared))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::module_artifact::compile_module_artifact_from_source;
305 use crate::module_source::ModuleSource;
306 use harn_parser::TypeExpr;
307
308 fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
309 match type_expr {
310 Some(TypeExpr::List(inner)) => match inner.as_ref() {
311 TypeExpr::Named(name) => name,
312 other => panic!("expected named list element, got {other:?}"),
313 },
314 other => panic!("expected list parameter type, got {other:?}"),
315 }
316 }
317
318 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
319 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
320 imports: Vec::new(),
321 type_schema_init_chunk: None,
322 init_chunk: None,
323 functions: BTreeMap::new(),
324 public_exports: BTreeMap::new(),
325 public_value_names: Default::default(),
326 public_type_names: Default::default(),
327 }))
328 }
329
330 #[test]
331 fn bounded_cache_evicts_oldest_exact_key() {
332 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
333 let first_source = ModuleSource::from_text("pub fn first() { 1 }");
334 let second_source = ModuleSource::from_text("pub fn second() { 2 }");
335 let first = empty_artifact();
336 let _ = cache.insert(PathBuf::from("first.harn"), first_source.sha256(), first);
337 let _ = cache.insert(
338 PathBuf::from("second.harn"),
339 second_source.sha256(),
340 empty_artifact(),
341 );
342
343 assert!(cache
344 .get(Path::new("first.harn"), first_source.sha256())
345 .is_none());
346 assert!(cache
347 .get(Path::new("second.harn"), second_source.sha256())
348 .is_some());
349 assert_eq!(cache.stats().evictions, 1);
350 assert_eq!(cache.stats().entries, 1);
351 }
352
353 #[test]
354 fn cache_key_separates_compiler_configuration() {
355 let path = PathBuf::from("module.harn");
356 let key = PreparedModuleCacheKey::new(
357 path,
358 ModuleSource::from_text("pub fn value() { 1 }").sha256(),
359 );
360 let mut other_compiler = key.clone();
361 other_compiler.optimizations_enabled = !key.optimizations_enabled;
362
363 assert_ne!(key, other_compiler);
364 }
365
366 #[test]
367 fn dropping_last_cache_handle_releases_prepared_artifacts() {
368 let cache = PreparedModuleCache::default();
369 let path = PathBuf::from("module.harn");
370 let source = ModuleSource::from_text("pub fn value() { 1 }");
371 let artifact = empty_artifact();
372 let weak = Arc::downgrade(&artifact);
373 let _ = cache.insert(path, source.sha256(), artifact);
374 let clone = cache.clone();
375
376 drop(cache);
377 assert!(weak.upgrade().is_some());
378 drop(clone);
379 assert!(weak.upgrade().is_none());
380 }
381
382 #[test]
383 fn hydration_moves_module_owned_storage() {
384 let source = r#"
385import { assert_eq } from "std/testing"
386pub type Result = {value: int}
387pub const value = 1
388pub fn answer(items: list<string>) {
389 fn nested() { return 42 }
390 return items
391}
392"#;
393 let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
394 .expect("compile typed module artifact");
395
396 let imports = artifact.imports.as_ptr();
397 let import_path = artifact.imports[0].path.as_ptr();
398 let selected_names = artifact.imports[0]
399 .selected_names
400 .as_ref()
401 .unwrap()
402 .as_ptr();
403 let selected_name = artifact.imports[0].selected_names.as_ref().unwrap()[0].as_ptr();
404 let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
405 let schema_init_code = artifact
406 .type_schema_init_chunk
407 .as_ref()
408 .unwrap()
409 .code
410 .as_ptr();
411 let (function_key, function) = artifact.functions.first_key_value().unwrap();
412 let function_key = function_key.as_ptr();
413 let function_name = function.name.as_ptr();
414 let function_code = function.chunk.code.as_ptr();
415 let param_name = function.params[0].name.as_ptr();
416 let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
417 let nested_name = function.chunk.functions[0].name.as_ptr();
418 let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
419 let public_export_name = artifact
420 .public_exports
421 .get_key_value("answer")
422 .unwrap()
423 .0
424 .as_ptr();
425 let public_export_kind = *artifact.public_exports.get("answer").unwrap();
426 let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
427 let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
428 let hydrated = PreparedModuleArtifact::from_cached(artifact);
429
430 assert_eq!(hydrated.imports.as_ptr(), imports);
431 assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
432 assert_eq!(
433 hydrated.imports[0]
434 .selected_names
435 .as_ref()
436 .unwrap()
437 .as_ptr(),
438 selected_names
439 );
440 assert_eq!(
441 hydrated.imports[0].selected_names.as_ref().unwrap()[0].as_ptr(),
442 selected_name
443 );
444 assert_eq!(
445 hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
446 init_code
447 );
448 assert_eq!(
449 hydrated
450 .type_schema_init_chunk
451 .as_ref()
452 .unwrap()
453 .code
454 .as_ptr(),
455 schema_init_code
456 );
457 let (hydrated_function_key, hydrated_function) =
458 hydrated.functions.first_key_value().unwrap();
459 assert_eq!(hydrated_function_key.as_ptr(), function_key);
460 assert_eq!(hydrated_function.name.as_ptr(), function_name);
461 assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
462 assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
463 assert_eq!(
464 named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
465 param_type_name
466 );
467 assert_eq!(
468 hydrated_function.chunk.functions[0].name.as_ptr(),
469 nested_name
470 );
471 assert_eq!(
472 hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
473 nested_code
474 );
475 assert_eq!(
476 hydrated
477 .public_exports
478 .get_key_value("answer")
479 .unwrap()
480 .0
481 .as_ptr(),
482 public_export_name
483 );
484 assert_eq!(
485 hydrated.public_exports.get("answer"),
486 Some(&public_export_kind)
487 );
488 assert_eq!(
489 hydrated.public_value_names.get("value").unwrap().as_ptr(),
490 public_value_name
491 );
492 assert_eq!(
493 hydrated.public_type_names.get("Result").unwrap().as_ptr(),
494 public_type_name
495 );
496 }
497}