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::context_manifest::{ContextManifest, ManifestCheck};
19use crate::module_artifact::{
20 compile_module_artifact_from_source_with_context,
21 compile_trusted_host_dispatch_module_artifact_from_source_with_context, ModuleArtifact,
22 ModuleCompilationContext, 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, InterfaceMemoEntry>>>,
205}
206
207#[derive(Clone, PartialEq, Eq, Hash)]
210struct InterfaceMemoKey {
211 canonical_path: PathBuf,
212 source_hash: [u8; 32],
213 provenance: ModuleProvenance,
214}
215
216#[derive(Clone)]
217struct InterfaceMemoEntry {
218 context: ModuleCompilationContext,
219 manifest: Option<Arc<PreparedModuleManifest>>,
223}
224
225struct PreparedModuleManifest {
231 manifest: Mutex<ContextManifest>,
232}
233
234impl PreparedModuleManifest {
235 fn new(manifest: ContextManifest) -> Self {
236 Self {
237 manifest: Mutex::new(manifest),
238 }
239 }
240
241 fn is_valid(&self) -> bool {
242 let mut manifest = self
243 .manifest
244 .lock()
245 .expect("prepared-module manifest lock poisoned");
246 let entry = manifest.entry.clone();
247 match manifest.check(&entry) {
248 ManifestCheck::Stale => false,
249 ManifestCheck::Valid => true,
250 ManifestCheck::ValidAfterRecheck { refreshed } => {
251 *manifest = refreshed;
252 true
253 }
254 }
255 }
256}
257
258#[derive(Clone, Default)]
265pub(crate) struct PreparedModuleValidation {
266 checked: Arc<Mutex<Vec<PreparedModuleCheck>>>,
267}
268
269struct PreparedModuleCheck {
270 manifest: Arc<PreparedModuleManifest>,
271 valid: bool,
272}
273
274impl PreparedModuleValidation {
275 fn is_valid(&self, manifest: &Arc<PreparedModuleManifest>) -> bool {
276 {
277 let checked = self
278 .checked
279 .lock()
280 .expect("prepared-module validation lock poisoned");
281 if let Some(check) = checked
282 .iter()
283 .find(|check| Arc::ptr_eq(&check.manifest, manifest))
284 {
285 return check.valid;
286 }
287 }
288 let valid = manifest.is_valid();
292 let mut checked = self
293 .checked
294 .lock()
295 .expect("prepared-module validation lock poisoned");
296 if let Some(check) = checked
297 .iter()
298 .find(|check| Arc::ptr_eq(&check.manifest, manifest))
299 {
300 return check.valid;
301 }
302 checked.push(PreparedModuleCheck {
303 manifest: Arc::clone(manifest),
304 valid,
305 });
306 valid
307 }
308
309 fn remember_fresh(&self, manifest: &Arc<PreparedModuleManifest>) {
310 self.checked
311 .lock()
312 .expect("prepared-module validation lock poisoned")
313 .push(PreparedModuleCheck {
314 manifest: Arc::clone(manifest),
315 valid: true,
316 });
317 }
318}
319
320impl Default for PreparedModuleCache {
321 fn default() -> Self {
322 Self::with_capacity(
323 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
324 )
325 }
326}
327
328impl PreparedModuleCache {
329 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
330 let counters = Arc::new(PreparedModuleCacheCounters::default());
331 let lifecycle = PreparedModuleCacheLifecycle {
332 counters: Arc::clone(&counters),
333 };
334 let capacity = max_entries.get();
335 Self {
336 entries: Arc::new(Cache::with(
337 capacity,
338 capacity as u64,
339 UnitWeighter,
340 DefaultHashBuilder::default(),
341 lifecycle,
342 )),
343 counters,
344 interfaces: Arc::new(Mutex::new(HashMap::new())),
345 }
346 }
347
348 fn remembered_interface(
349 &self,
350 key: &InterfaceMemoKey,
351 validation: &PreparedModuleValidation,
352 ) -> Option<ModuleCompilationContext> {
353 let entry = self
354 .interfaces
355 .lock()
356 .expect("interface memo lock poisoned")
357 .get(key)
358 .cloned()?;
359 if entry
360 .manifest
361 .as_ref()
362 .is_none_or(|manifest| validation.is_valid(manifest))
363 {
364 Some(entry.context)
365 } else {
366 None
367 }
368 }
369
370 fn remember_interface(
371 &self,
372 key: InterfaceMemoKey,
373 context: &ModuleCompilationContext,
374 manifest: Option<Arc<PreparedModuleManifest>>,
375 ) {
376 let mut interfaces = self
377 .interfaces
378 .lock()
379 .expect("interface memo lock poisoned");
380 if interfaces.len() >= MAX_REMEMBERED_INTERFACES {
387 interfaces.clear();
388 }
389 interfaces.insert(
390 key,
391 InterfaceMemoEntry {
392 context: context.clone(),
393 manifest,
394 },
395 );
396 }
397
398 fn remember_interface_graph(
399 &self,
400 root_key: InterfaceMemoKey,
401 root_context: &ModuleCompilationContext,
402 manifest: ContextManifest,
403 provenance: ModuleProvenance,
404 validation: &PreparedModuleValidation,
405 ) {
406 let files = manifest.files.clone();
407 let manifest = Arc::new(PreparedModuleManifest::new(manifest));
408 validation.remember_fresh(&manifest);
409 self.remember_interface(root_key, root_context, Some(Arc::clone(&manifest)));
410 for file in files {
411 self.remember_interface(
412 InterfaceMemoKey {
413 canonical_path: file.path,
414 source_hash: file.content_hash,
415 provenance,
416 },
417 &file.compilation_context,
418 Some(Arc::clone(&manifest)),
419 );
420 }
421 }
422
423 pub fn stats(&self) -> PreparedModuleCacheStats {
424 PreparedModuleCacheStats {
425 hits: self.counters.hits.load(Ordering::Relaxed),
426 misses: self.counters.misses.load(Ordering::Relaxed),
427 insertions: self.counters.insertions.load(Ordering::Relaxed),
428 evictions: self.counters.evictions.load(Ordering::Relaxed),
429 entries: self.entries.len(),
430 }
431 }
432
433 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
440 self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
441 }
442
443 pub fn prepare_trusted_host_dispatch_import_graph(
448 &self,
449 roots: &[PathBuf],
450 ) -> ModulePhaseStats {
451 self.prepare_import_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch)
452 }
453
454 fn prepare_import_graph_with_provenance(
455 &self,
456 roots: &[PathBuf],
457 provenance: ModuleProvenance,
458 ) -> ModulePhaseStats {
459 if roots.is_empty() {
460 return ModulePhaseStats::default();
461 }
462
463 self.interfaces
467 .lock()
468 .expect("interface memo lock poisoned")
469 .clear();
470 let graph = harn_modules::build(roots);
471 let root_paths = roots
472 .iter()
473 .map(|path| harn_modules::canonical_path(path))
474 .collect::<std::collections::HashSet<_>>();
475 let recorder = ModulePhaseRecorder::new();
476 let validation = PreparedModuleValidation::default();
477
478 for path in graph.module_paths() {
479 if root_paths.contains(&harn_modules::canonical_path(&path)) {
480 continue;
481 }
482 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
483 let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
484 continue;
485 }
486
487 let source = {
488 let _load_span = recorder.load_span();
489 match crate::module_source::read(&path) {
490 Ok(source) => source,
491 Err(_) => continue,
492 }
493 };
494 let Ok(compilation_context) =
495 ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
496 else {
497 continue;
498 };
499 let canonical = harn_modules::canonical_path(&path);
500 let _ = self.prepare(
501 &path,
502 &canonical,
503 &source,
504 Some(&compilation_context),
505 Some(&recorder),
506 provenance,
507 &validation,
508 );
509 }
510
511 recorder.snapshot()
512 }
513
514 #[cfg(test)]
515 pub(crate) fn get(
516 &self,
517 canonical_path: &Path,
518 source_hash: [u8; 32],
519 provenance: ModuleProvenance,
520 ) -> Option<Arc<PreparedModuleArtifact>> {
521 self.get_with_context(
522 canonical_path,
523 source_hash,
524 provenance,
525 &ModuleCompilationContext::default(),
526 )
527 }
528
529 pub(crate) fn get_with_context(
530 &self,
531 canonical_path: &Path,
532 source_hash: [u8; 32],
533 provenance: ModuleProvenance,
534 compilation_context: &ModuleCompilationContext,
535 ) -> Option<Arc<PreparedModuleArtifact>> {
536 let key = PreparedModuleCacheKey::with_context(
537 canonical_path.to_path_buf(),
538 source_hash,
539 provenance,
540 compilation_context,
541 );
542 let artifact = self.entries.get(&key);
543 if artifact.is_some() {
544 saturating_increment(&self.counters.hits);
545 } else {
546 saturating_increment(&self.counters.misses);
547 }
548 artifact
549 }
550
551 #[cfg(test)]
552 pub(crate) fn insert(
553 &self,
554 canonical_path: PathBuf,
555 source_hash: [u8; 32],
556 artifact: Arc<PreparedModuleArtifact>,
557 ) -> Arc<PreparedModuleArtifact> {
558 self.insert_with_context(
559 canonical_path,
560 source_hash,
561 &ModuleCompilationContext::default(),
562 artifact,
563 )
564 }
565
566 pub(crate) fn insert_with_context(
567 &self,
568 canonical_path: PathBuf,
569 source_hash: [u8; 32],
570 compilation_context: &ModuleCompilationContext,
571 artifact: Arc<PreparedModuleArtifact>,
572 ) -> Arc<PreparedModuleArtifact> {
573 let key = PreparedModuleCacheKey::with_context(
574 canonical_path,
575 source_hash,
576 artifact.provenance,
577 compilation_context,
578 );
579 match self.entries.get_value_or_guard(&key, None) {
580 GuardResult::Value(existing) => existing,
581 GuardResult::Guard(guard) => {
582 if guard.insert(Arc::clone(&artifact)).is_ok() {
583 saturating_increment(&self.counters.insertions);
584 }
585 artifact
586 }
587 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
588 }
589 }
590
591 fn prepare_exact_key(
592 &self,
593 key: &PreparedModuleCacheKey,
594 recorder: Option<&ModulePhaseRecorder>,
595 prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
596 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
597 let prepared = {
598 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
599 self.entries.get(key)
600 };
601 if let Some(prepared) = prepared {
602 saturating_increment(&self.counters.hits);
603 return Ok(prepared);
604 }
605 saturating_increment(&self.counters.misses);
606
607 let guarded = {
608 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
609 self.entries.get_value_or_guard(key, None)
610 };
611 match guarded {
612 GuardResult::Value(prepared) => Ok(prepared),
613 GuardResult::Guard(guard) => {
614 let prepared = prepare()?;
615 if guard.insert(Arc::clone(&prepared)).is_ok() {
616 saturating_increment(&self.counters.insertions);
617 }
618 Ok(prepared)
619 }
620 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
621 }
622 }
623
624 pub(crate) fn prepare(
625 &self,
626 source_path: &Path,
627 canonical_path: &Path,
628 source: &ModuleSource,
629 compilation_context: Option<&ModuleCompilationContext>,
630 recorder: Option<&ModulePhaseRecorder>,
631 provenance: ModuleProvenance,
632 validation: &PreparedModuleValidation,
633 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
634 let source_hash = {
635 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
636 source.sha256()
637 };
638 let memo_key = InterfaceMemoKey {
639 canonical_path: canonical_path.to_path_buf(),
640 source_hash,
641 provenance,
642 };
643 let compilation_context = match compilation_context {
644 Some(context) => {
645 self.remember_interface(memo_key, context, None);
646 context.clone()
647 }
648 None => match self.remembered_interface(&memo_key, validation) {
649 Some(context) => context,
650 None => {
651 let (context, manifest) =
652 crate::bytecode_cache::module_compilation_context_with_manifest(
653 source_path,
654 source.as_str(),
655 )?;
656 if let Some(manifest) = manifest {
657 self.remember_interface_graph(
658 memo_key, &context, manifest, provenance, validation,
659 );
660 }
661 context
662 }
663 },
664 };
665 let key = PreparedModuleCacheKey::with_context(
666 canonical_path.to_path_buf(),
667 source_hash,
668 provenance,
669 &compilation_context,
670 );
671 self.prepare_exact_key(&key, recorder, || {
672 let cached = {
684 let lookup = {
685 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
686 crate::bytecode_cache::load_module(
687 source_path,
688 source,
689 &compilation_context,
690 provenance,
691 )
692 };
693 if let Some(artifact) = lookup.artifact {
694 artifact
695 } else {
696 let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
697 let compiled = if provenance == ModuleProvenance::TrustedHostDispatch {
701 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
702 source_path,
703 source.as_str(),
704 &compilation_context,
705 )?
706 } else {
707 compile_module_artifact_from_source_with_context(
708 source_path,
709 source.as_str(),
710 &compilation_context,
711 )?
712 };
713 if let Some(span) = &mut compile_span {
714 span.mark_compile_succeeded();
715 }
716 drop(compile_span);
717 if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
718 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
719 eprintln!(
720 "[harn] module cache write skipped for {}: {err}",
721 source_path.display()
722 );
723 }
724 }
725 compiled
726 }
727 };
728 let prepared = {
729 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
730 Arc::new(PreparedModuleArtifact::from_cached(cached))
731 };
732 Ok(prepared)
733 })
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
741 use crate::module_source::ModuleSource;
742 use harn_parser::TypeExpr;
743 use std::sync::Barrier;
744
745 fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
746 match type_expr {
747 Some(TypeExpr::List(inner)) => match inner.as_ref() {
748 TypeExpr::Named(name) => name,
749 other => panic!("expected named list element, got {other:?}"),
750 },
751 other => panic!("expected list parameter type, got {other:?}"),
752 }
753 }
754
755 fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
756 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
757 provenance,
758 imports: Vec::new(),
759 type_schema_init_chunks: Vec::new(),
760 init_chunk: None,
761 functions: BTreeMap::new(),
762 public_exports: BTreeMap::new(),
763 public_value_names: Default::default(),
764 public_type_names: Default::default(),
765 }))
766 }
767
768 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
769 empty_artifact_with_provenance(ModuleProvenance::User)
770 }
771
772 #[test]
773 fn repeated_preparation_derives_one_modules_interface_once() {
774 let dir = tempfile::tempdir().expect("temp module dir");
779 let module = dir.path().join("library.harn");
780 std::fs::write(&module, "pub fn value() { return 1 }\n").expect("write module");
781 let source = crate::module_source::read(&module).expect("read module");
782 let canonical = harn_modules::canonical_path(&module);
783 let cache = PreparedModuleCache::default();
784 let validation = PreparedModuleValidation::default();
785
786 let resolutions = |prepare: &dyn Fn()| {
787 let before = crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get);
788 prepare();
789 crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get) - before
790 };
791 let prepare = || {
792 cache
793 .prepare(
794 &module,
795 &canonical,
796 &source,
797 None,
798 None,
799 ModuleProvenance::User,
800 &validation,
801 )
802 .expect("module prepares");
803 };
804
805 assert_eq!(
809 resolutions(&prepare),
810 1,
811 "the first preparation of a module must derive its interface"
812 );
813 assert_eq!(
814 resolutions(&prepare),
815 0,
816 "the same bytes must not be re-parsed to re-derive the same interface"
817 );
818 }
819
820 #[test]
821 fn fresh_run_revalidates_a_remembered_interface_dependency() {
822 let dir = tempfile::tempdir().expect("temp module dir");
823 let dependency = dir.path().join("dep.harn");
824 std::fs::write(&dependency, "pub enum Color { Ready(string) }\n")
825 .expect("write enum dependency");
826 let module = dir.path().join("library.harn");
827 std::fs::write(
828 &module,
829 r#"import "./dep"
830pub fn exercise(value: any) -> string {
831 match value {
832 Color.Ready(message) -> { return message }
833 _ -> { return "fallback" }
834 }
835}
836"#,
837 )
838 .expect("write dependent module");
839
840 let source = crate::module_source::read(&module).expect("read dependent module");
841 let canonical = harn_modules::canonical_path(&module);
842 let cache = PreparedModuleCache::default();
843 let first = cache
844 .prepare(
845 &module,
846 &canonical,
847 &source,
848 None,
849 None,
850 ModuleProvenance::User,
851 &PreparedModuleValidation::default(),
852 )
853 .expect("prepare with imported enum");
854
855 std::fs::write(&dependency, "pub fn replacement() { return 1 }\n")
856 .expect("replace dependency interface");
857
858 let second = cache
859 .prepare(
860 &module,
861 &canonical,
862 &source,
863 None,
864 None,
865 ModuleProvenance::User,
866 &PreparedModuleValidation::default(),
867 )
868 .expect("prepare after dependency edit");
869
870 assert!(
871 !Arc::ptr_eq(&first, &second),
872 "a fresh run must not reuse bytecode lowered against the old interface"
873 );
874 assert_ne!(
875 postcard::to_allocvec(&first.functions["exercise"].freeze_for_cache())
876 .expect("serialize first function"),
877 postcard::to_allocvec(&second.functions["exercise"].freeze_for_cache())
878 .expect("serialize second function"),
879 "the dependency edit must reach the context-sensitive bytecode"
880 );
881 }
882
883 #[test]
884 fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
885 let cache = PreparedModuleCache::default();
886 let source = ModuleSource::from_text("const value = 1");
887 let _ = cache.insert(
888 PathBuf::from("same.harn"),
889 source.sha256(),
890 empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
891 );
892 assert!(
893 cache
894 .get(
895 Path::new("same.harn"),
896 source.sha256(),
897 ModuleProvenance::User,
898 )
899 .is_none(),
900 "user module lookup must be provenance-separated"
901 );
902 assert!(cache
903 .get(
904 Path::new("same.harn"),
905 source.sha256(),
906 ModuleProvenance::PrivilegedWire,
907 )
908 .is_some());
909 }
910
911 #[test]
912 fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
913 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
914 let first_source = ModuleSource::from_text("pub fn first() { 1 }");
915 let second_source = ModuleSource::from_text("pub fn second() { 2 }");
916 let first = empty_artifact();
917 let first_weak = Arc::downgrade(&first);
918 drop(cache.insert(
919 PathBuf::from("first.harn"),
920 first_source.sha256(),
921 Arc::clone(&first),
922 ));
923 drop(first);
924
925 let scanned = empty_artifact();
928 let scanned_weak = Arc::downgrade(&scanned);
929 drop(cache.insert(
930 PathBuf::from("second.harn"),
931 second_source.sha256(),
932 Arc::clone(&scanned),
933 ));
934 drop(scanned);
935
936 assert!(cache
937 .get(
938 Path::new("first.harn"),
939 first_source.sha256(),
940 ModuleProvenance::User,
941 )
942 .is_some());
943 assert!(cache
944 .get(
945 Path::new("second.harn"),
946 second_source.sha256(),
947 ModuleProvenance::User,
948 )
949 .is_none());
950 assert!(first_weak.upgrade().is_some());
951 assert!(scanned_weak.upgrade().is_none());
952 assert_eq!(cache.stats().insertions, 2);
953 assert_eq!(cache.stats().evictions, 1);
954 assert_eq!(cache.stats().entries, 1);
955
956 drop(cache);
957 assert!(first_weak.upgrade().is_none());
958 }
959
960 #[test]
961 fn cache_key_separates_compiler_configuration() {
962 let path = PathBuf::from("module.harn");
963 let key = PreparedModuleCacheKey::new(
964 path,
965 ModuleSource::from_text("pub fn value() { 1 }").sha256(),
966 ModuleProvenance::User,
967 );
968 let mut other_compiler = key.clone();
969 other_compiler.optimizations_enabled = !key.optimizations_enabled;
970
971 assert_ne!(key, other_compiler);
972 }
973
974 #[test]
975 fn cache_counters_saturate_instead_of_wrapping() {
976 let counter = AtomicU64::new(u64::MAX);
977 saturating_increment(&counter);
978 assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
979 }
980
981 #[test]
982 fn cache_key_separates_imported_symbol_compilation_context() {
983 let source_path = PathBuf::from("context-sensitive.harn");
984 let source = ModuleSource::from_text(
985 r#"
986import "./library"
987
988pub fn exercise(value: any) -> string {
989 match value {
990 Color.Ready(message) -> { return message }
991 _ -> { return "fallback" }
992 }
993}
994"#,
995 );
996 let without_imported_enum = compile_module_artifact_from_source_with_context(
997 &source_path,
998 source.as_str(),
999 &ModuleCompilationContext::default(),
1000 )
1001 .expect("compile dynamically-resolved pattern");
1002 let imported_enum_context =
1003 ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
1004 let with_imported_enum = compile_module_artifact_from_source_with_context(
1005 &source_path,
1006 source.as_str(),
1007 &imported_enum_context,
1008 )
1009 .expect("compile imported-enum-resolved pattern");
1010 assert_ne!(
1011 postcard::to_allocvec(&without_imported_enum.functions["exercise"])
1012 .expect("serialize dynamically-resolved function"),
1013 postcard::to_allocvec(&with_imported_enum.functions["exercise"])
1014 .expect("serialize imported-enum-resolved function"),
1015 "the imported enum projection must demonstrably alter bytecode"
1016 );
1017
1018 let cache = PreparedModuleCache::default();
1019 let validation = PreparedModuleValidation::default();
1020 let without_imported_enum = cache
1021 .prepare(
1022 &source_path,
1023 &source_path,
1024 &source,
1025 Some(&ModuleCompilationContext::default()),
1026 None,
1027 ModuleProvenance::User,
1028 &validation,
1029 )
1030 .expect("prepare dynamically-resolved artifact");
1031 let with_imported_enum = cache
1032 .prepare(
1033 &source_path,
1034 &source_path,
1035 &source,
1036 Some(&imported_enum_context),
1037 None,
1038 ModuleProvenance::User,
1039 &validation,
1040 )
1041 .expect("prepare imported-enum-resolved artifact");
1042
1043 assert!(
1044 !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
1045 "one source/path/provenance with distinct imported projections must not alias"
1046 );
1047 assert_ne!(
1048 postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
1049 .expect("serialize cached dynamically-resolved function"),
1050 postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
1051 .expect("serialize cached imported-enum-resolved function")
1052 );
1053 assert_eq!(cache.stats().insertions, 2);
1054 }
1055
1056 #[test]
1057 fn dropping_last_cache_handle_releases_prepared_artifacts() {
1058 let cache = PreparedModuleCache::default();
1059 let path = PathBuf::from("module.harn");
1060 let source = ModuleSource::from_text("pub fn value() { 1 }");
1061 let artifact = empty_artifact();
1062 let weak = Arc::downgrade(&artifact);
1063 let _ = cache.insert(path, source.sha256(), artifact);
1064 let clone = cache.clone();
1065
1066 drop(cache);
1067 assert!(weak.upgrade().is_some());
1068 drop(clone);
1069 assert!(weak.upgrade().is_none());
1070 }
1071
1072 #[test]
1073 fn concurrent_identical_misses_compile_one_immutable_artifact() {
1074 const WORKERS: usize = 8;
1075
1076 let cache = PreparedModuleCache::default();
1077 let nonce_dir = tempfile::tempdir().expect("temp dir for a unique module identity");
1090 let nonce = nonce_dir
1091 .path()
1092 .file_name()
1093 .expect("temp dir has a final component")
1094 .to_string_lossy()
1095 .into_owned();
1096 let source = Arc::new(ModuleSource::from_text(
1097 std::iter::once(format!("// {nonce}\n"))
1098 .chain(
1099 (0..128).map(|index| format!("pub fn value_{index}() {{ return {index} }}\n")),
1100 )
1101 .collect::<String>(),
1102 ));
1103 let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
1104 let validation = PreparedModuleValidation::default();
1105 let start = Arc::new(Barrier::new(WORKERS + 1));
1106 let mut handles = Vec::with_capacity(WORKERS);
1107
1108 for _ in 0..WORKERS {
1109 let cache = cache.clone();
1110 let source = Arc::clone(&source);
1111 let source_path = Arc::clone(&source_path);
1112 let validation = validation.clone();
1113 let start = Arc::clone(&start);
1114 handles.push(std::thread::spawn(move || {
1115 let recorder = ModulePhaseRecorder::new();
1116 start.wait();
1117 let artifact = cache
1118 .prepare(
1119 &source_path,
1120 &source_path,
1121 &source,
1122 None,
1123 Some(&recorder),
1124 ModuleProvenance::TrustedHostDispatch,
1125 &validation,
1126 )
1127 .expect("compile shared immutable module");
1128 (artifact, recorder.snapshot())
1129 }));
1130 }
1131
1132 start.wait();
1133 let outcomes = handles
1134 .into_iter()
1135 .map(|handle| handle.join().expect("module compiler worker joins"))
1136 .collect::<Vec<_>>();
1137 let first = &outcomes[0].0;
1138
1139 assert!(
1140 outcomes
1141 .iter()
1142 .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
1143 "all workers must consume the same immutable prepared artifact"
1144 );
1145 assert_eq!(
1146 outcomes
1147 .iter()
1148 .map(|(_, phases)| phases.modules_compiled)
1149 .sum::<u64>(),
1150 1,
1151 "one exact cache key must have one compilation owner regardless of worker count"
1152 );
1153 assert_eq!(cache.stats().insertions, 1);
1154 }
1155
1156 #[test]
1157 fn failed_preparation_is_not_cached_or_poisoned() {
1158 let cache = PreparedModuleCache::default();
1159 let key = PreparedModuleCacheKey::new(
1160 PathBuf::from("recoverable.harn"),
1161 ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
1162 ModuleProvenance::TrustedHostDispatch,
1163 );
1164
1165 let failed = cache.prepare_exact_key(&key, None, || {
1166 Err(VmError::Runtime(
1167 "synthetic compilation failure".to_string(),
1168 ))
1169 });
1170 assert!(
1171 matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
1172 );
1173 assert_eq!(cache.stats().entries, 0);
1174 assert_eq!(cache.stats().insertions, 0);
1175
1176 let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
1177 let prepared = cache
1178 .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
1179 .expect("a failed owner must release the exact-key preparation slot");
1180
1181 assert!(Arc::ptr_eq(&prepared, &expected));
1182 assert_eq!(cache.stats().misses, 2);
1183 assert_eq!(cache.stats().insertions, 1);
1184 assert_eq!(cache.stats().entries, 1);
1185 }
1186
1187 #[test]
1188 fn hydration_moves_module_owned_storage() {
1189 let source = r#"
1190import { assert_eq } from "std/testing"
1191pub type Result = {value: int}
1192pub const value = 1
1193pub fn answer(items: list<string>) {
1194 fn nested() { return 42 }
1195 return items
1196}
1197"#;
1198 let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
1199 .expect("compile typed module artifact");
1200
1201 let imports = artifact.imports.as_ptr();
1202 let import_path = artifact.imports[0].path.as_ptr();
1203 let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
1204 panic!("expected selective import");
1205 };
1206 let selected_names = selected.as_ptr();
1207 let selected_name = selected[0].as_ptr();
1208 let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
1209 let schema_init_codes = artifact
1210 .type_schema_init_chunks
1211 .iter()
1212 .map(|chunk| chunk.code.as_ptr())
1213 .collect::<Vec<_>>();
1214 let (function_key, function) = artifact.functions.first_key_value().unwrap();
1215 let function_key = function_key.as_ptr();
1216 let function_name = function.name.clone();
1217 let function_code = function.chunk.code.as_ptr();
1218 let param_name = function.params[0].name.as_ptr();
1219 let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
1220 let nested_name = function.chunk.functions[0].name.clone();
1221 let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
1222 let public_export_name = artifact
1223 .public_exports
1224 .get_key_value("answer")
1225 .unwrap()
1226 .0
1227 .as_ptr();
1228 let public_export_kind = *artifact.public_exports.get("answer").unwrap();
1229 let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
1230 let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
1231 let hydrated = PreparedModuleArtifact::from_cached(artifact);
1232
1233 assert_eq!(hydrated.imports.as_ptr(), imports);
1234 assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
1235 let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
1236 panic!("expected selective import");
1237 };
1238 assert_eq!(selected.as_ptr(), selected_names);
1239 assert_eq!(selected[0].as_ptr(), selected_name);
1240 assert_eq!(
1241 hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
1242 init_code
1243 );
1244 assert_eq!(
1245 hydrated
1246 .type_schema_init_chunks
1247 .iter()
1248 .map(|chunk| chunk.code.as_ptr())
1249 .collect::<Vec<_>>(),
1250 schema_init_codes
1251 );
1252 let (hydrated_function_key, hydrated_function) =
1253 hydrated.functions.first_key_value().unwrap();
1254 assert_eq!(hydrated_function_key.as_ptr(), function_key);
1255 assert_eq!(hydrated_function.name.as_str(), function_name);
1258 assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
1259 assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
1260 assert_eq!(
1261 named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
1262 param_type_name
1263 );
1264 assert_eq!(
1265 hydrated_function.chunk.functions[0].name.as_str(),
1266 nested_name
1267 );
1268 assert_eq!(
1269 hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
1270 nested_code
1271 );
1272 assert_eq!(
1273 hydrated
1274 .public_exports
1275 .get_key_value("answer")
1276 .unwrap()
1277 .0
1278 .as_ptr(),
1279 public_export_name
1280 );
1281 assert_eq!(
1282 hydrated.public_exports.get("answer"),
1283 Some(&public_export_kind)
1284 );
1285 assert_eq!(
1286 hydrated.public_value_names.get("value").unwrap().as_ptr(),
1287 public_value_name
1288 );
1289 assert_eq!(
1290 hydrated.public_type_names.get("Result").unwrap().as_ptr(),
1291 public_type_name
1292 );
1293 }
1294}