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