1use std::collections::{BTreeMap, HashMap};
8use std::num::NonZeroUsize;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex};
12
13use harn_modules::DefKind;
14use quick_cache::sync::{Cache, GuardResult};
15use quick_cache::{DefaultHashBuilder, Lifecycle, UnitWeighter};
16
17use crate::chunk::{Chunk, CompiledFunction};
18use crate::module_artifact::{
19 compile_module_artifact_from_source_with_context,
20 compile_trusted_host_dispatch_module_artifact_from_source_with_context,
21 module_compilation_context_for_source, ModuleArtifact, ModuleCompilationContext,
22 ModuleImportSpec, ModuleProvenance,
23};
24use crate::module_source::ModuleSource;
25use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
26const DEFAULT_MAX_ENTRIES: usize = 512;
27const MAX_REMEMBERED_INTERFACES: usize = 8192;
32
33pub(crate) struct PreparedModuleArtifact {
35 pub(crate) provenance: ModuleProvenance,
36 pub(crate) imports: Vec<ModuleImportSpec>,
37 pub(crate) type_schema_init_chunks: Vec<Arc<Chunk>>,
38 pub(crate) init_chunk: Option<Arc<Chunk>>,
39 pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
40 pub(crate) public_exports: BTreeMap<String, DefKind>,
41 pub(crate) public_value_names: std::collections::HashSet<String>,
42 pub(crate) public_type_names: std::collections::HashSet<String>,
43}
44
45impl PreparedModuleArtifact {
46 pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
47 let ModuleArtifact {
48 provenance,
49 imports,
50 type_schema_init_chunks,
51 init_chunk,
52 functions,
53 public_exports,
54 public_value_names,
55 public_type_names,
56 } = artifact;
57 let type_schema_init_chunks = type_schema_init_chunks
58 .into_iter()
59 .map(|chunk| Arc::new(Chunk::from_cached(chunk)))
60 .collect();
61 let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
62 let functions = functions
63 .into_iter()
64 .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
65 .collect();
66 Self {
67 provenance,
68 imports,
69 type_schema_init_chunks,
70 init_chunk,
71 functions,
72 public_exports,
73 public_value_names,
74 public_type_names,
75 }
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
80struct PreparedModuleCacheKey {
81 canonical_path: PathBuf,
82 source_hash: [u8; 32],
83 provenance: ModuleProvenance,
84 harn_version: &'static str,
85 codegen_fingerprint: &'static str,
86 optimizations_enabled: bool,
87 compilation_context_digest: [u8; 32],
88}
89
90impl PreparedModuleCacheKey {
91 #[cfg(test)]
96 fn new(canonical_path: PathBuf, source_hash: [u8; 32], provenance: ModuleProvenance) -> Self {
97 Self::with_context(
98 canonical_path,
99 source_hash,
100 provenance,
101 &ModuleCompilationContext::default(),
102 )
103 }
104
105 fn with_context(
106 canonical_path: PathBuf,
107 source_hash: [u8; 32],
108 provenance: ModuleProvenance,
109 compilation_context: &ModuleCompilationContext,
110 ) -> Self {
111 Self {
112 canonical_path,
113 source_hash,
114 provenance,
115 harn_version: crate::bytecode_cache::HARN_VERSION,
116 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
117 optimizations_enabled: crate::compiler::CompilerOptions::from_env()
118 .optimizations_enabled(),
119 compilation_context_digest: compilation_context.digest(),
120 }
121 }
122}
123
124#[derive(Default)]
125struct PreparedModuleCacheCounters {
126 hits: AtomicU64,
127 misses: AtomicU64,
128 insertions: AtomicU64,
129 evictions: AtomicU64,
130}
131
132fn saturating_increment(counter: &AtomicU64) {
133 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
134 Some(value.saturating_add(1))
135 });
136}
137
138#[derive(Clone)]
139struct PreparedModuleCacheLifecycle {
140 counters: Arc<PreparedModuleCacheCounters>,
141}
142
143impl Lifecycle<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>
144 for PreparedModuleCacheLifecycle
145{
146 type RequestState = ();
147
148 fn on_evict(
149 &self,
150 _state: &mut Self::RequestState,
151 _key: PreparedModuleCacheKey,
152 _artifact: Arc<PreparedModuleArtifact>,
153 ) {
154 saturating_increment(&self.counters.evictions);
155 }
156}
157
158type PreparedArtifactCache = Cache<
159 PreparedModuleCacheKey,
160 Arc<PreparedModuleArtifact>,
161 UnitWeighter,
162 DefaultHashBuilder,
163 PreparedModuleCacheLifecycle,
164>;
165
166#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct PreparedModuleCacheStats {
170 pub hits: u64,
171 pub misses: u64,
172 pub insertions: u64,
173 pub evictions: u64,
176 pub entries: usize,
177}
178
179#[derive(Clone)]
187pub struct PreparedModuleCache {
188 entries: Arc<PreparedArtifactCache>,
189 counters: Arc<PreparedModuleCacheCounters>,
190 interfaces: Arc<Mutex<HashMap<InterfaceMemoKey, ModuleCompilationContext>>>,
205}
206
207#[derive(Clone, PartialEq, Eq, Hash)]
210struct InterfaceMemoKey {
211 canonical_path: PathBuf,
212 source_hash: [u8; 32],
213 provenance: ModuleProvenance,
214}
215
216impl Default for PreparedModuleCache {
217 fn default() -> Self {
218 Self::with_capacity(
219 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
220 )
221 }
222}
223
224impl PreparedModuleCache {
225 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
226 let counters = Arc::new(PreparedModuleCacheCounters::default());
227 let lifecycle = PreparedModuleCacheLifecycle {
228 counters: Arc::clone(&counters),
229 };
230 let capacity = max_entries.get();
231 Self {
232 entries: Arc::new(Cache::with(
233 capacity,
234 capacity as u64,
235 UnitWeighter,
236 DefaultHashBuilder::default(),
237 lifecycle,
238 )),
239 counters,
240 interfaces: Arc::new(Mutex::new(HashMap::new())),
241 }
242 }
243
244 fn remembered_interface(&self, key: &InterfaceMemoKey) -> Option<ModuleCompilationContext> {
245 self.interfaces
246 .lock()
247 .expect("interface memo lock poisoned")
248 .get(key)
249 .cloned()
250 }
251
252 fn remember_interface(&self, key: InterfaceMemoKey, context: &ModuleCompilationContext) {
253 let mut interfaces = self
254 .interfaces
255 .lock()
256 .expect("interface memo lock poisoned");
257 if interfaces.len() >= MAX_REMEMBERED_INTERFACES {
264 interfaces.clear();
265 }
266 interfaces.insert(key, context.clone());
267 }
268
269 pub fn stats(&self) -> PreparedModuleCacheStats {
270 PreparedModuleCacheStats {
271 hits: self.counters.hits.load(Ordering::Relaxed),
272 misses: self.counters.misses.load(Ordering::Relaxed),
273 insertions: self.counters.insertions.load(Ordering::Relaxed),
274 evictions: self.counters.evictions.load(Ordering::Relaxed),
275 entries: self.entries.len(),
276 }
277 }
278
279 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
286 self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
287 }
288
289 pub fn prepare_trusted_host_dispatch_import_graph(
294 &self,
295 roots: &[PathBuf],
296 ) -> ModulePhaseStats {
297 self.prepare_import_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch)
298 }
299
300 fn prepare_import_graph_with_provenance(
301 &self,
302 roots: &[PathBuf],
303 provenance: ModuleProvenance,
304 ) -> ModulePhaseStats {
305 if roots.is_empty() {
306 return ModulePhaseStats::default();
307 }
308
309 self.interfaces
313 .lock()
314 .expect("interface memo lock poisoned")
315 .clear();
316 let graph = harn_modules::build(roots);
317 let root_paths = roots
318 .iter()
319 .map(|path| harn_modules::canonical_path(path))
320 .collect::<std::collections::HashSet<_>>();
321 let recorder = ModulePhaseRecorder::new();
322
323 for path in graph.module_paths() {
324 if root_paths.contains(&harn_modules::canonical_path(&path)) {
325 continue;
326 }
327 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
328 let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
329 continue;
330 }
331
332 let source = {
333 let _load_span = recorder.load_span();
334 match crate::module_source::read(&path) {
335 Ok(source) => source,
336 Err(_) => continue,
337 }
338 };
339 let Ok(compilation_context) =
340 ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
341 else {
342 continue;
343 };
344 let canonical = harn_modules::canonical_path(&path);
345 let _ = self.prepare(
346 &path,
347 &canonical,
348 &source,
349 Some(&compilation_context),
350 Some(&recorder),
351 provenance,
352 );
353 }
354
355 recorder.snapshot()
356 }
357
358 #[cfg(test)]
359 pub(crate) fn get(
360 &self,
361 canonical_path: &Path,
362 source_hash: [u8; 32],
363 provenance: ModuleProvenance,
364 ) -> Option<Arc<PreparedModuleArtifact>> {
365 self.get_with_context(
366 canonical_path,
367 source_hash,
368 provenance,
369 &ModuleCompilationContext::default(),
370 )
371 }
372
373 pub(crate) fn get_with_context(
374 &self,
375 canonical_path: &Path,
376 source_hash: [u8; 32],
377 provenance: ModuleProvenance,
378 compilation_context: &ModuleCompilationContext,
379 ) -> Option<Arc<PreparedModuleArtifact>> {
380 let key = PreparedModuleCacheKey::with_context(
381 canonical_path.to_path_buf(),
382 source_hash,
383 provenance,
384 compilation_context,
385 );
386 let artifact = self.entries.get(&key);
387 if artifact.is_some() {
388 saturating_increment(&self.counters.hits);
389 } else {
390 saturating_increment(&self.counters.misses);
391 }
392 artifact
393 }
394
395 #[cfg(test)]
396 pub(crate) fn insert(
397 &self,
398 canonical_path: PathBuf,
399 source_hash: [u8; 32],
400 artifact: Arc<PreparedModuleArtifact>,
401 ) -> Arc<PreparedModuleArtifact> {
402 self.insert_with_context(
403 canonical_path,
404 source_hash,
405 &ModuleCompilationContext::default(),
406 artifact,
407 )
408 }
409
410 pub(crate) fn insert_with_context(
411 &self,
412 canonical_path: PathBuf,
413 source_hash: [u8; 32],
414 compilation_context: &ModuleCompilationContext,
415 artifact: Arc<PreparedModuleArtifact>,
416 ) -> Arc<PreparedModuleArtifact> {
417 let key = PreparedModuleCacheKey::with_context(
418 canonical_path,
419 source_hash,
420 artifact.provenance,
421 compilation_context,
422 );
423 match self.entries.get_value_or_guard(&key, None) {
424 GuardResult::Value(existing) => existing,
425 GuardResult::Guard(guard) => {
426 if guard.insert(Arc::clone(&artifact)).is_ok() {
427 saturating_increment(&self.counters.insertions);
428 }
429 artifact
430 }
431 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
432 }
433 }
434
435 fn prepare_exact_key(
436 &self,
437 key: &PreparedModuleCacheKey,
438 recorder: Option<&ModulePhaseRecorder>,
439 prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
440 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
441 let prepared = {
442 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
443 self.entries.get(key)
444 };
445 if let Some(prepared) = prepared {
446 saturating_increment(&self.counters.hits);
447 return Ok(prepared);
448 }
449 saturating_increment(&self.counters.misses);
450
451 let guarded = {
452 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
453 self.entries.get_value_or_guard(key, None)
454 };
455 match guarded {
456 GuardResult::Value(prepared) => Ok(prepared),
457 GuardResult::Guard(guard) => {
458 let prepared = prepare()?;
459 if guard.insert(Arc::clone(&prepared)).is_ok() {
460 saturating_increment(&self.counters.insertions);
461 }
462 Ok(prepared)
463 }
464 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
465 }
466 }
467
468 pub(crate) fn prepare(
469 &self,
470 source_path: &Path,
471 canonical_path: &Path,
472 source: &ModuleSource,
473 compilation_context: Option<&ModuleCompilationContext>,
474 recorder: Option<&ModulePhaseRecorder>,
475 provenance: ModuleProvenance,
476 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
477 let source_hash = {
478 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
479 source.sha256()
480 };
481 let memo_key = InterfaceMemoKey {
482 canonical_path: canonical_path.to_path_buf(),
483 source_hash,
484 provenance,
485 };
486 let compilation_context = match compilation_context {
487 Some(context) => {
488 self.remember_interface(memo_key, context);
489 context.clone()
490 }
491 None => match self.remembered_interface(&memo_key) {
492 Some(context) => context,
493 None => {
494 let context =
495 module_compilation_context_for_source(source_path, source.as_str())?;
496 self.remember_interface(memo_key, &context);
497 context
498 }
499 },
500 };
501 let key = PreparedModuleCacheKey::with_context(
502 canonical_path.to_path_buf(),
503 source_hash,
504 provenance,
505 &compilation_context,
506 );
507 self.prepare_exact_key(&key, recorder, || {
508 let cached = {
520 let lookup = {
521 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
522 crate::bytecode_cache::load_module(
523 source_path,
524 source,
525 &compilation_context,
526 provenance,
527 )
528 };
529 if let Some(artifact) = lookup.artifact {
530 artifact
531 } else {
532 let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
533 let compiled = if provenance == ModuleProvenance::TrustedHostDispatch {
537 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
538 source_path,
539 source.as_str(),
540 &compilation_context,
541 )?
542 } else {
543 compile_module_artifact_from_source_with_context(
544 source_path,
545 source.as_str(),
546 &compilation_context,
547 )?
548 };
549 if let Some(span) = &mut compile_span {
550 span.mark_compile_succeeded();
551 }
552 drop(compile_span);
553 if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
554 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
555 eprintln!(
556 "[harn] module cache write skipped for {}: {err}",
557 source_path.display()
558 );
559 }
560 }
561 compiled
562 }
563 };
564 let prepared = {
565 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
566 Arc::new(PreparedModuleArtifact::from_cached(cached))
567 };
568 Ok(prepared)
569 })
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
577 use crate::module_source::ModuleSource;
578 use harn_parser::TypeExpr;
579 use std::sync::Barrier;
580
581 fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
582 match type_expr {
583 Some(TypeExpr::List(inner)) => match inner.as_ref() {
584 TypeExpr::Named(name) => name,
585 other => panic!("expected named list element, got {other:?}"),
586 },
587 other => panic!("expected list parameter type, got {other:?}"),
588 }
589 }
590
591 fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
592 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
593 provenance,
594 imports: Vec::new(),
595 type_schema_init_chunks: Vec::new(),
596 init_chunk: None,
597 functions: BTreeMap::new(),
598 public_exports: BTreeMap::new(),
599 public_value_names: Default::default(),
600 public_type_names: Default::default(),
601 }))
602 }
603
604 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
605 empty_artifact_with_provenance(ModuleProvenance::User)
606 }
607
608 #[test]
609 fn repeated_preparation_derives_one_modules_interface_once() {
610 let dir = tempfile::tempdir().expect("temp module dir");
615 let module = dir.path().join("library.harn");
616 std::fs::write(&module, "pub fn value() { return 1 }\n").expect("write module");
617 let source = crate::module_source::read(&module).expect("read module");
618 let canonical = harn_modules::canonical_path(&module);
619 let cache = PreparedModuleCache::default();
620
621 let resolutions = |prepare: &dyn Fn()| {
622 let before = crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get);
623 prepare();
624 crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get) - before
625 };
626 let prepare = || {
627 cache
628 .prepare(
629 &module,
630 &canonical,
631 &source,
632 None,
633 None,
634 ModuleProvenance::User,
635 )
636 .expect("module prepares");
637 };
638
639 assert_eq!(
643 resolutions(&prepare),
644 1,
645 "the first preparation of a module must derive its interface"
646 );
647 assert_eq!(
648 resolutions(&prepare),
649 0,
650 "the same bytes must not be re-parsed to re-derive the same interface"
651 );
652 }
653
654 #[test]
655 fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
656 let cache = PreparedModuleCache::default();
657 let source = ModuleSource::from_text("const value = 1");
658 let _ = cache.insert(
659 PathBuf::from("same.harn"),
660 source.sha256(),
661 empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
662 );
663 assert!(
664 cache
665 .get(
666 Path::new("same.harn"),
667 source.sha256(),
668 ModuleProvenance::User,
669 )
670 .is_none(),
671 "user module lookup must be provenance-separated"
672 );
673 assert!(cache
674 .get(
675 Path::new("same.harn"),
676 source.sha256(),
677 ModuleProvenance::PrivilegedWire,
678 )
679 .is_some());
680 }
681
682 #[test]
683 fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
684 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
685 let first_source = ModuleSource::from_text("pub fn first() { 1 }");
686 let second_source = ModuleSource::from_text("pub fn second() { 2 }");
687 let first = empty_artifact();
688 let first_weak = Arc::downgrade(&first);
689 drop(cache.insert(
690 PathBuf::from("first.harn"),
691 first_source.sha256(),
692 Arc::clone(&first),
693 ));
694 drop(first);
695
696 let scanned = empty_artifact();
699 let scanned_weak = Arc::downgrade(&scanned);
700 drop(cache.insert(
701 PathBuf::from("second.harn"),
702 second_source.sha256(),
703 Arc::clone(&scanned),
704 ));
705 drop(scanned);
706
707 assert!(cache
708 .get(
709 Path::new("first.harn"),
710 first_source.sha256(),
711 ModuleProvenance::User,
712 )
713 .is_some());
714 assert!(cache
715 .get(
716 Path::new("second.harn"),
717 second_source.sha256(),
718 ModuleProvenance::User,
719 )
720 .is_none());
721 assert!(first_weak.upgrade().is_some());
722 assert!(scanned_weak.upgrade().is_none());
723 assert_eq!(cache.stats().insertions, 2);
724 assert_eq!(cache.stats().evictions, 1);
725 assert_eq!(cache.stats().entries, 1);
726
727 drop(cache);
728 assert!(first_weak.upgrade().is_none());
729 }
730
731 #[test]
732 fn cache_key_separates_compiler_configuration() {
733 let path = PathBuf::from("module.harn");
734 let key = PreparedModuleCacheKey::new(
735 path,
736 ModuleSource::from_text("pub fn value() { 1 }").sha256(),
737 ModuleProvenance::User,
738 );
739 let mut other_compiler = key.clone();
740 other_compiler.optimizations_enabled = !key.optimizations_enabled;
741
742 assert_ne!(key, other_compiler);
743 }
744
745 #[test]
746 fn cache_counters_saturate_instead_of_wrapping() {
747 let counter = AtomicU64::new(u64::MAX);
748 saturating_increment(&counter);
749 assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
750 }
751
752 #[test]
753 fn cache_key_separates_imported_symbol_compilation_context() {
754 let source_path = PathBuf::from("context-sensitive.harn");
755 let source = ModuleSource::from_text(
756 r#"
757import "./library"
758
759pub fn exercise(value: any) -> string {
760 match value {
761 Color.Ready(message) -> { return message }
762 _ -> { return "fallback" }
763 }
764}
765"#,
766 );
767 let without_imported_enum = compile_module_artifact_from_source_with_context(
768 &source_path,
769 source.as_str(),
770 &ModuleCompilationContext::default(),
771 )
772 .expect("compile dynamically-resolved pattern");
773 let imported_enum_context =
774 ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
775 let with_imported_enum = compile_module_artifact_from_source_with_context(
776 &source_path,
777 source.as_str(),
778 &imported_enum_context,
779 )
780 .expect("compile imported-enum-resolved pattern");
781 assert_ne!(
782 postcard::to_allocvec(&without_imported_enum.functions["exercise"])
783 .expect("serialize dynamically-resolved function"),
784 postcard::to_allocvec(&with_imported_enum.functions["exercise"])
785 .expect("serialize imported-enum-resolved function"),
786 "the imported enum projection must demonstrably alter bytecode"
787 );
788
789 let cache = PreparedModuleCache::default();
790 let without_imported_enum = cache
791 .prepare(
792 &source_path,
793 &source_path,
794 &source,
795 Some(&ModuleCompilationContext::default()),
796 None,
797 ModuleProvenance::User,
798 )
799 .expect("prepare dynamically-resolved artifact");
800 let with_imported_enum = cache
801 .prepare(
802 &source_path,
803 &source_path,
804 &source,
805 Some(&imported_enum_context),
806 None,
807 ModuleProvenance::User,
808 )
809 .expect("prepare imported-enum-resolved artifact");
810
811 assert!(
812 !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
813 "one source/path/provenance with distinct imported projections must not alias"
814 );
815 assert_ne!(
816 postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
817 .expect("serialize cached dynamically-resolved function"),
818 postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
819 .expect("serialize cached imported-enum-resolved function")
820 );
821 assert_eq!(cache.stats().insertions, 2);
822 }
823
824 #[test]
825 fn dropping_last_cache_handle_releases_prepared_artifacts() {
826 let cache = PreparedModuleCache::default();
827 let path = PathBuf::from("module.harn");
828 let source = ModuleSource::from_text("pub fn value() { 1 }");
829 let artifact = empty_artifact();
830 let weak = Arc::downgrade(&artifact);
831 let _ = cache.insert(path, source.sha256(), artifact);
832 let clone = cache.clone();
833
834 drop(cache);
835 assert!(weak.upgrade().is_some());
836 drop(clone);
837 assert!(weak.upgrade().is_none());
838 }
839
840 #[test]
841 fn concurrent_identical_misses_compile_one_immutable_artifact() {
842 const WORKERS: usize = 8;
843
844 let cache = PreparedModuleCache::default();
845 let nonce_dir = tempfile::tempdir().expect("temp dir for a unique module identity");
858 let nonce = nonce_dir
859 .path()
860 .file_name()
861 .expect("temp dir has a final component")
862 .to_string_lossy()
863 .into_owned();
864 let source = Arc::new(ModuleSource::from_text(
865 std::iter::once(format!("// {nonce}\n"))
866 .chain(
867 (0..128).map(|index| format!("pub fn value_{index}() {{ return {index} }}\n")),
868 )
869 .collect::<String>(),
870 ));
871 let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
872 let start = Arc::new(Barrier::new(WORKERS + 1));
873 let mut handles = Vec::with_capacity(WORKERS);
874
875 for _ in 0..WORKERS {
876 let cache = cache.clone();
877 let source = Arc::clone(&source);
878 let source_path = Arc::clone(&source_path);
879 let start = Arc::clone(&start);
880 handles.push(std::thread::spawn(move || {
881 let recorder = ModulePhaseRecorder::new();
882 start.wait();
883 let artifact = cache
884 .prepare(
885 &source_path,
886 &source_path,
887 &source,
888 None,
889 Some(&recorder),
890 ModuleProvenance::TrustedHostDispatch,
891 )
892 .expect("compile shared immutable module");
893 (artifact, recorder.snapshot())
894 }));
895 }
896
897 start.wait();
898 let outcomes = handles
899 .into_iter()
900 .map(|handle| handle.join().expect("module compiler worker joins"))
901 .collect::<Vec<_>>();
902 let first = &outcomes[0].0;
903
904 assert!(
905 outcomes
906 .iter()
907 .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
908 "all workers must consume the same immutable prepared artifact"
909 );
910 assert_eq!(
911 outcomes
912 .iter()
913 .map(|(_, phases)| phases.modules_compiled)
914 .sum::<u64>(),
915 1,
916 "one exact cache key must have one compilation owner regardless of worker count"
917 );
918 assert_eq!(cache.stats().insertions, 1);
919 }
920
921 #[test]
922 fn failed_preparation_is_not_cached_or_poisoned() {
923 let cache = PreparedModuleCache::default();
924 let key = PreparedModuleCacheKey::new(
925 PathBuf::from("recoverable.harn"),
926 ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
927 ModuleProvenance::TrustedHostDispatch,
928 );
929
930 let failed = cache.prepare_exact_key(&key, None, || {
931 Err(VmError::Runtime(
932 "synthetic compilation failure".to_string(),
933 ))
934 });
935 assert!(
936 matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
937 );
938 assert_eq!(cache.stats().entries, 0);
939 assert_eq!(cache.stats().insertions, 0);
940
941 let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
942 let prepared = cache
943 .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
944 .expect("a failed owner must release the exact-key preparation slot");
945
946 assert!(Arc::ptr_eq(&prepared, &expected));
947 assert_eq!(cache.stats().misses, 2);
948 assert_eq!(cache.stats().insertions, 1);
949 assert_eq!(cache.stats().entries, 1);
950 }
951
952 #[test]
953 fn hydration_moves_module_owned_storage() {
954 let source = r#"
955import { assert_eq } from "std/testing"
956pub type Result = {value: int}
957pub const value = 1
958pub fn answer(items: list<string>) {
959 fn nested() { return 42 }
960 return items
961}
962"#;
963 let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
964 .expect("compile typed module artifact");
965
966 let imports = artifact.imports.as_ptr();
967 let import_path = artifact.imports[0].path.as_ptr();
968 let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
969 panic!("expected selective import");
970 };
971 let selected_names = selected.as_ptr();
972 let selected_name = selected[0].as_ptr();
973 let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
974 let schema_init_codes = artifact
975 .type_schema_init_chunks
976 .iter()
977 .map(|chunk| chunk.code.as_ptr())
978 .collect::<Vec<_>>();
979 let (function_key, function) = artifact.functions.first_key_value().unwrap();
980 let function_key = function_key.as_ptr();
981 let function_name = function.name.clone();
982 let function_code = function.chunk.code.as_ptr();
983 let param_name = function.params[0].name.as_ptr();
984 let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
985 let nested_name = function.chunk.functions[0].name.clone();
986 let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
987 let public_export_name = artifact
988 .public_exports
989 .get_key_value("answer")
990 .unwrap()
991 .0
992 .as_ptr();
993 let public_export_kind = *artifact.public_exports.get("answer").unwrap();
994 let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
995 let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
996 let hydrated = PreparedModuleArtifact::from_cached(artifact);
997
998 assert_eq!(hydrated.imports.as_ptr(), imports);
999 assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
1000 let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
1001 panic!("expected selective import");
1002 };
1003 assert_eq!(selected.as_ptr(), selected_names);
1004 assert_eq!(selected[0].as_ptr(), selected_name);
1005 assert_eq!(
1006 hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
1007 init_code
1008 );
1009 assert_eq!(
1010 hydrated
1011 .type_schema_init_chunks
1012 .iter()
1013 .map(|chunk| chunk.code.as_ptr())
1014 .collect::<Vec<_>>(),
1015 schema_init_codes
1016 );
1017 let (hydrated_function_key, hydrated_function) =
1018 hydrated.functions.first_key_value().unwrap();
1019 assert_eq!(hydrated_function_key.as_ptr(), function_key);
1020 assert_eq!(hydrated_function.name.as_str(), function_name);
1023 assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
1024 assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
1025 assert_eq!(
1026 named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
1027 param_type_name
1028 );
1029 assert_eq!(
1030 hydrated_function.chunk.functions[0].name.as_str(),
1031 nested_name
1032 );
1033 assert_eq!(
1034 hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
1035 nested_code
1036 );
1037 assert_eq!(
1038 hydrated
1039 .public_exports
1040 .get_key_value("answer")
1041 .unwrap()
1042 .0
1043 .as_ptr(),
1044 public_export_name
1045 );
1046 assert_eq!(
1047 hydrated.public_exports.get("answer"),
1048 Some(&public_export_kind)
1049 );
1050 assert_eq!(
1051 hydrated.public_value_names.get("value").unwrap().as_ptr(),
1052 public_value_name
1053 );
1054 assert_eq!(
1055 hydrated.public_type_names.get("Result").unwrap().as_ptr(),
1056 public_type_name
1057 );
1058 }
1059}