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_embedded_stdlib_module_artifact_from_source_with_context,
21 compile_module_artifact_from_source_with_context,
22 compile_trusted_host_dispatch_module_artifact_from_source_with_context, ModuleArtifact,
23 ModuleCompilationContext, ModuleImportSpec, ModuleProvenance,
24};
25use crate::module_source::ModuleSource;
26use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
27const DEFAULT_MAX_ENTRIES: usize = 512;
28const MAX_REMEMBERED_INTERFACES: usize = 8192;
33
34mod generation;
35pub use generation::PreparedModuleGenerationStats;
36
37pub(crate) struct PreparedModuleArtifact {
39 pub(crate) provenance: ModuleProvenance,
40 pub(crate) imports: Vec<ModuleImportSpec>,
41 pub(crate) type_schema_init_chunks: Vec<Arc<Chunk>>,
42 pub(crate) init_chunk: Option<Arc<Chunk>>,
43 pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
44 pub(crate) public_exports: BTreeMap<String, DefKind>,
45 pub(crate) public_value_names: std::collections::HashSet<String>,
46 pub(crate) public_type_names: std::collections::HashSet<String>,
47}
48
49impl PreparedModuleArtifact {
50 pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
51 let ModuleArtifact {
52 provenance,
53 imports,
54 type_schema_init_chunks,
55 init_chunk,
56 functions,
57 public_exports,
58 public_value_names,
59 public_type_names,
60 } = artifact;
61 let type_schema_init_chunks = type_schema_init_chunks
62 .into_iter()
63 .map(|chunk| Arc::new(Chunk::from_cached(chunk)))
64 .collect();
65 let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
66 let functions = functions
67 .into_iter()
68 .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
69 .collect();
70 Self {
71 provenance,
72 imports,
73 type_schema_init_chunks,
74 init_chunk,
75 functions,
76 public_exports,
77 public_value_names,
78 public_type_names,
79 }
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
84struct PreparedModuleCacheKey {
85 canonical_path: PathBuf,
86 source_hash: [u8; 32],
87 provenance: ModuleProvenance,
88 harn_version: &'static str,
89 codegen_fingerprint: &'static str,
90 optimizations_enabled: bool,
91 compilation_context_digest: [u8; 32],
92}
93
94impl PreparedModuleCacheKey {
95 #[cfg(test)]
100 fn new(canonical_path: PathBuf, source_hash: [u8; 32], provenance: ModuleProvenance) -> Self {
101 Self::with_context(
102 canonical_path,
103 source_hash,
104 provenance,
105 &ModuleCompilationContext::default(),
106 )
107 }
108
109 fn with_context(
110 canonical_path: PathBuf,
111 source_hash: [u8; 32],
112 provenance: ModuleProvenance,
113 compilation_context: &ModuleCompilationContext,
114 ) -> Self {
115 Self {
116 canonical_path,
117 source_hash,
118 provenance,
119 harn_version: crate::bytecode_cache::HARN_VERSION,
120 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
121 optimizations_enabled: crate::compiler::CompilerOptions::from_env()
122 .optimizations_enabled(),
123 compilation_context_digest: compilation_context.digest(),
124 }
125 }
126}
127
128#[derive(Default)]
129struct PreparedModuleCacheCounters {
130 hits: AtomicU64,
131 misses: AtomicU64,
132 insertions: AtomicU64,
133 evictions: AtomicU64,
134}
135
136fn saturating_increment(counter: &AtomicU64) {
137 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
138 Some(value.saturating_add(1))
139 });
140}
141
142#[derive(Clone)]
143struct PreparedModuleCacheLifecycle {
144 counters: Arc<PreparedModuleCacheCounters>,
145}
146
147impl Lifecycle<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>
148 for PreparedModuleCacheLifecycle
149{
150 type RequestState = ();
151
152 fn on_evict(
153 &self,
154 _state: &mut Self::RequestState,
155 _key: PreparedModuleCacheKey,
156 _artifact: Arc<PreparedModuleArtifact>,
157 ) {
158 saturating_increment(&self.counters.evictions);
159 }
160}
161
162type PreparedArtifactCache = Cache<
163 PreparedModuleCacheKey,
164 Arc<PreparedModuleArtifact>,
165 UnitWeighter,
166 DefaultHashBuilder,
167 PreparedModuleCacheLifecycle,
168>;
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
172#[non_exhaustive]
173pub struct PreparedModuleCacheStats {
174 pub hits: u64,
175 pub misses: u64,
176 pub insertions: u64,
177 pub evictions: u64,
180 pub entries: usize,
181}
182
183#[derive(Clone)]
191pub struct PreparedModuleCache {
192 entries: Arc<PreparedArtifactCache>,
193 counters: Arc<PreparedModuleCacheCounters>,
194 interfaces: Arc<Mutex<HashMap<InterfaceMemoKey, InterfaceMemoEntry>>>,
209 sources: Arc<Mutex<BTreeMap<PathBuf, Arc<str>>>>,
217}
218
219#[derive(Clone, PartialEq, Eq, Hash)]
222struct InterfaceMemoKey {
223 canonical_path: PathBuf,
224 source_hash: [u8; 32],
225 provenance: ModuleProvenance,
226}
227
228#[derive(Clone)]
229struct InterfaceMemoEntry {
230 context: ModuleCompilationContext,
231 manifest: Option<Arc<PreparedModuleManifest>>,
235}
236
237struct PreparedModuleManifest {
243 manifest: Mutex<ContextManifest>,
244}
245
246impl PreparedModuleManifest {
247 fn new(manifest: ContextManifest) -> Self {
248 Self {
249 manifest: Mutex::new(manifest),
250 }
251 }
252
253 fn is_valid(&self) -> bool {
254 let mut manifest = self
255 .manifest
256 .lock()
257 .expect("prepared-module manifest lock poisoned");
258 let entry = manifest.entry.clone();
259 match manifest.check(&entry) {
260 ManifestCheck::Stale => false,
261 ManifestCheck::Valid => true,
262 ManifestCheck::ValidAfterRecheck { refreshed } => {
263 *manifest = refreshed;
264 true
265 }
266 }
267 }
268}
269
270#[derive(Clone, Default)]
277pub(crate) struct PreparedModuleValidation {
278 checked: Arc<Mutex<Vec<PreparedModuleCheck>>>,
279}
280
281struct PreparedModuleCheck {
282 manifest: Arc<PreparedModuleManifest>,
283 valid: bool,
284}
285
286impl PreparedModuleValidation {
287 fn is_valid(&self, manifest: &Arc<PreparedModuleManifest>) -> bool {
288 {
289 let checked = self
290 .checked
291 .lock()
292 .expect("prepared-module validation lock poisoned");
293 if let Some(check) = checked
294 .iter()
295 .find(|check| Arc::ptr_eq(&check.manifest, manifest))
296 {
297 return check.valid;
298 }
299 }
300 let valid = manifest.is_valid();
304 let mut checked = self
305 .checked
306 .lock()
307 .expect("prepared-module validation lock poisoned");
308 if let Some(check) = checked
309 .iter()
310 .find(|check| Arc::ptr_eq(&check.manifest, manifest))
311 {
312 return check.valid;
313 }
314 checked.push(PreparedModuleCheck {
315 manifest: Arc::clone(manifest),
316 valid,
317 });
318 valid
319 }
320
321 fn remember_fresh(&self, manifest: &Arc<PreparedModuleManifest>) {
322 self.checked
323 .lock()
324 .expect("prepared-module validation lock poisoned")
325 .push(PreparedModuleCheck {
326 manifest: Arc::clone(manifest),
327 valid: true,
328 });
329 }
330}
331
332impl Default for PreparedModuleCache {
333 fn default() -> Self {
334 Self::with_capacity(
335 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
336 )
337 }
338}
339
340impl PreparedModuleCache {
341 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
342 let counters = Arc::new(PreparedModuleCacheCounters::default());
343 let lifecycle = PreparedModuleCacheLifecycle {
344 counters: Arc::clone(&counters),
345 };
346 let capacity = max_entries.get();
347 Self {
348 entries: Arc::new(Cache::with(
349 capacity,
350 capacity as u64,
351 UnitWeighter,
352 DefaultHashBuilder::default(),
353 lifecycle,
354 )),
355 counters,
356 interfaces: Arc::new(Mutex::new(HashMap::new())),
357 sources: Arc::new(Mutex::new(BTreeMap::new())),
358 }
359 }
360
361 fn remembered_interface(
362 &self,
363 key: &InterfaceMemoKey,
364 validation: &PreparedModuleValidation,
365 ) -> Option<ModuleCompilationContext> {
366 let entry = self
367 .interfaces
368 .lock()
369 .expect("interface memo lock poisoned")
370 .get(key)
371 .cloned()?;
372 if entry
373 .manifest
374 .as_ref()
375 .is_none_or(|manifest| validation.is_valid(manifest))
376 {
377 Some(entry.context)
378 } else {
379 None
380 }
381 }
382
383 fn remember_interface(
384 &self,
385 key: InterfaceMemoKey,
386 context: &ModuleCompilationContext,
387 manifest: Option<Arc<PreparedModuleManifest>>,
388 ) {
389 let mut interfaces = self
390 .interfaces
391 .lock()
392 .expect("interface memo lock poisoned");
393 if interfaces.len() >= MAX_REMEMBERED_INTERFACES {
400 interfaces.clear();
401 }
402 interfaces.insert(
403 key,
404 InterfaceMemoEntry {
405 context: context.clone(),
406 manifest,
407 },
408 );
409 }
410
411 fn remember_interface_graph(
412 &self,
413 root_key: InterfaceMemoKey,
414 root_context: &ModuleCompilationContext,
415 manifest: ContextManifest,
416 provenance: ModuleProvenance,
417 validation: &PreparedModuleValidation,
418 ) {
419 let files = manifest.files.clone();
420 let manifest = Arc::new(PreparedModuleManifest::new(manifest));
421 validation.remember_fresh(&manifest);
422 self.remember_interface(root_key, root_context, Some(Arc::clone(&manifest)));
423 for file in files {
424 self.remember_interface(
425 InterfaceMemoKey {
426 canonical_path: file.path,
427 source_hash: file.content_hash,
428 provenance,
429 },
430 &file.compilation_context,
431 Some(Arc::clone(&manifest)),
432 );
433 }
434 }
435
436 pub fn stats(&self) -> PreparedModuleCacheStats {
437 PreparedModuleCacheStats {
438 hits: self.counters.hits.load(Ordering::Relaxed),
439 misses: self.counters.misses.load(Ordering::Relaxed),
440 insertions: self.counters.insertions.load(Ordering::Relaxed),
441 evictions: self.counters.evictions.load(Ordering::Relaxed),
442 entries: self.entries.len(),
443 }
444 }
445
446 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
453 self.prepare_graph_with_provenance(roots, ModuleProvenance::User, false)
454 .unwrap_or_default()
455 }
456
457 pub fn prepare_trusted_host_dispatch_import_graph(
462 &self,
463 roots: &[PathBuf],
464 ) -> ModulePhaseStats {
465 self.prepare_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch, false)
466 .unwrap_or_default()
467 }
468
469 fn prepare_graph_with_provenance(
470 &self,
471 roots: &[PathBuf],
472 provenance: ModuleProvenance,
473 include_roots: bool,
474 ) -> Result<ModulePhaseStats, VmError> {
475 if roots.is_empty() {
476 return Ok(ModulePhaseStats::default());
477 }
478
479 self.interfaces
483 .lock()
484 .expect("interface memo lock poisoned")
485 .clear();
486 self.sources
487 .lock()
488 .expect("prepared-module source lock poisoned")
489 .clear();
490 let graph = harn_modules::build(roots);
491 let root_paths = roots
492 .iter()
493 .map(|path| harn_modules::canonical_path(path))
494 .collect::<std::collections::HashSet<_>>();
495 let recorder = ModulePhaseRecorder::new();
496 let validation = PreparedModuleValidation::default();
497
498 for path in graph.module_paths() {
499 if !include_roots && root_paths.contains(&harn_modules::canonical_path(&path)) {
500 continue;
501 }
502 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
503 let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
504 continue;
505 }
506
507 let source = {
508 let _load_span = recorder.load_span();
509 match crate::module_source::read(&path) {
510 Ok(source) => source,
511 Err(error) if include_roots => {
512 return Err(VmError::Runtime(format!(
513 "cannot prepare module generation source {}: {error}",
514 path.display()
515 )))
516 }
517 Err(_) => continue,
518 }
519 };
520 let compilation_context =
521 match ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
522 {
523 Ok(context) => context,
524 Err(error) if include_roots => return Err(error),
525 Err(_) => continue,
526 };
527 let canonical = harn_modules::canonical_path(&path);
528 self.sources
529 .lock()
530 .expect("prepared-module source lock poisoned")
531 .insert(canonical.clone(), Arc::clone(source.text()));
532 let prepared = self.prepare(
533 &path,
534 &canonical,
535 &source,
536 Some(&compilation_context),
537 Some(&recorder),
538 provenance,
539 &validation,
540 );
541 if include_roots {
542 prepared?;
543 }
544 }
545
546 if include_roots {
547 let mut sources = self
553 .sources
554 .lock()
555 .expect("prepared-module source lock poisoned");
556 let canonical_sources = sources
557 .iter()
558 .map(|(path, source)| (path.clone(), Arc::clone(source)))
559 .collect::<Vec<_>>();
560 for root in roots {
561 let Some(raw_parent) = root.parent() else {
562 continue;
563 };
564 let canonical_root = harn_modules::canonical_path(root);
565 let Some(canonical_parent) = canonical_root.parent() else {
566 continue;
567 };
568 for (path, source) in &canonical_sources {
569 if let Ok(relative) = path.strip_prefix(canonical_parent) {
570 sources
571 .entry(raw_parent.join(relative))
572 .or_insert_with(|| Arc::clone(source));
573 }
574 }
575 }
576 }
577
578 if include_roots {
579 for entry in self
586 .interfaces
587 .lock()
588 .expect("interface memo lock poisoned")
589 .values_mut()
590 {
591 entry.manifest = None;
592 }
593 }
594
595 Ok(recorder.snapshot())
596 }
597
598 #[cfg(test)]
599 pub(crate) fn get(
600 &self,
601 canonical_path: &Path,
602 source_hash: [u8; 32],
603 provenance: ModuleProvenance,
604 ) -> Option<Arc<PreparedModuleArtifact>> {
605 self.get_with_context(
606 canonical_path,
607 source_hash,
608 provenance,
609 &ModuleCompilationContext::default(),
610 )
611 }
612
613 pub(crate) fn get_with_context(
614 &self,
615 canonical_path: &Path,
616 source_hash: [u8; 32],
617 provenance: ModuleProvenance,
618 compilation_context: &ModuleCompilationContext,
619 ) -> Option<Arc<PreparedModuleArtifact>> {
620 let key = PreparedModuleCacheKey::with_context(
621 canonical_path.to_path_buf(),
622 source_hash,
623 provenance,
624 compilation_context,
625 );
626 let artifact = self.entries.get(&key);
627 if artifact.is_some() {
628 saturating_increment(&self.counters.hits);
629 } else {
630 saturating_increment(&self.counters.misses);
631 }
632 artifact
633 }
634
635 #[cfg(test)]
636 pub(crate) fn insert(
637 &self,
638 canonical_path: PathBuf,
639 source_hash: [u8; 32],
640 artifact: Arc<PreparedModuleArtifact>,
641 ) -> Arc<PreparedModuleArtifact> {
642 self.insert_with_context(
643 canonical_path,
644 source_hash,
645 &ModuleCompilationContext::default(),
646 artifact,
647 )
648 }
649
650 pub(crate) fn insert_with_context(
651 &self,
652 canonical_path: PathBuf,
653 source_hash: [u8; 32],
654 compilation_context: &ModuleCompilationContext,
655 artifact: Arc<PreparedModuleArtifact>,
656 ) -> Arc<PreparedModuleArtifact> {
657 let key = PreparedModuleCacheKey::with_context(
658 canonical_path,
659 source_hash,
660 artifact.provenance,
661 compilation_context,
662 );
663 match self.entries.get_value_or_guard(&key, None) {
664 GuardResult::Value(existing) => existing,
665 GuardResult::Guard(guard) => {
666 if guard.insert(Arc::clone(&artifact)).is_ok() {
667 saturating_increment(&self.counters.insertions);
668 }
669 artifact
670 }
671 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
672 }
673 }
674
675 fn prepare_exact_key(
676 &self,
677 key: &PreparedModuleCacheKey,
678 recorder: Option<&ModulePhaseRecorder>,
679 prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
680 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
681 let prepared = {
682 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
683 self.entries.get(key)
684 };
685 if let Some(prepared) = prepared {
686 saturating_increment(&self.counters.hits);
687 return Ok(prepared);
688 }
689 saturating_increment(&self.counters.misses);
690
691 let guarded = {
692 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
693 self.entries.get_value_or_guard(key, None)
694 };
695 match guarded {
696 GuardResult::Value(prepared) => Ok(prepared),
697 GuardResult::Guard(guard) => {
698 let prepared = prepare()?;
699 if guard.insert(Arc::clone(&prepared)).is_ok() {
700 saturating_increment(&self.counters.insertions);
701 }
702 Ok(prepared)
703 }
704 GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
705 }
706 }
707
708 pub(crate) fn prepare(
709 &self,
710 source_path: &Path,
711 canonical_path: &Path,
712 source: &ModuleSource,
713 compilation_context: Option<&ModuleCompilationContext>,
714 recorder: Option<&ModulePhaseRecorder>,
715 provenance: ModuleProvenance,
716 validation: &PreparedModuleValidation,
717 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
718 let source_hash = {
719 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
720 source.sha256()
721 };
722 let memo_key = InterfaceMemoKey {
723 canonical_path: canonical_path.to_path_buf(),
724 source_hash,
725 provenance,
726 };
727 let compilation_context = match compilation_context {
728 Some(context) => {
729 self.remember_interface(memo_key, context, None);
730 context.clone()
731 }
732 None => match self.remembered_interface(&memo_key, validation) {
733 Some(context) => context,
734 None => {
735 let (context, manifest) =
736 crate::bytecode_cache::module_compilation_context_with_manifest(
737 source_path,
738 source.as_str(),
739 )?;
740 if let Some(manifest) = manifest {
741 self.remember_interface_graph(
742 memo_key, &context, manifest, provenance, validation,
743 );
744 }
745 context
746 }
747 },
748 };
749 let key = PreparedModuleCacheKey::with_context(
750 canonical_path.to_path_buf(),
751 source_hash,
752 provenance,
753 &compilation_context,
754 );
755 self.prepare_exact_key(&key, recorder, || {
756 let cached = {
768 let lookup = {
769 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
770 crate::bytecode_cache::load_module(
771 source_path,
772 source,
773 &compilation_context,
774 provenance,
775 )
776 };
777 if let Some(artifact) = lookup.artifact {
778 artifact
779 } else {
780 let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
781 let compiled = match provenance {
785 ModuleProvenance::TrustedHostDispatch => {
786 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
787 source_path,
788 source.as_str(),
789 &compilation_context,
790 )?
791 }
792 ModuleProvenance::EmbeddedStdlib => {
793 compile_embedded_stdlib_module_artifact_from_source_with_context(
794 source_path,
795 source.as_str(),
796 &compilation_context,
797 )?
798 }
799 ModuleProvenance::User | ModuleProvenance::PrivilegedWire => {
800 compile_module_artifact_from_source_with_context(
801 source_path,
802 source.as_str(),
803 &compilation_context,
804 )?
805 }
806 };
807 if let Some(span) = &mut compile_span {
808 span.mark_compile_succeeded();
809 }
810 drop(compile_span);
811 if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
812 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
813 eprintln!(
814 "[harn] module cache write skipped for {}: {err}",
815 source_path.display()
816 );
817 }
818 }
819 compiled
820 }
821 };
822 let prepared = {
823 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
824 Arc::new(PreparedModuleArtifact::from_cached(cached))
825 };
826 Ok(prepared)
827 })
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
835 use crate::module_source::ModuleSource;
836 use harn_parser::TypeExpr;
837 use std::sync::Barrier;
838
839 fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
840 match type_expr {
841 Some(TypeExpr::List(inner)) => match inner.as_ref() {
842 TypeExpr::Named(name) => name,
843 other => panic!("expected named list element, got {other:?}"),
844 },
845 other => panic!("expected list parameter type, got {other:?}"),
846 }
847 }
848
849 fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
850 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
851 provenance,
852 imports: Vec::new(),
853 type_schema_init_chunks: Vec::new(),
854 init_chunk: None,
855 functions: BTreeMap::new(),
856 public_exports: BTreeMap::new(),
857 public_value_names: Default::default(),
858 public_type_names: Default::default(),
859 }))
860 }
861
862 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
863 empty_artifact_with_provenance(ModuleProvenance::User)
864 }
865
866 #[test]
867 fn repeated_preparation_derives_one_modules_interface_once() {
868 let dir = tempfile::tempdir().expect("temp module dir");
873 let module = dir.path().join("library.harn");
874 std::fs::write(&module, "pub fn value() { return 1 }\n").expect("write module");
875 let source = crate::module_source::read(&module).expect("read module");
876 let canonical = harn_modules::canonical_path(&module);
877 let cache = PreparedModuleCache::default();
878 let validation = PreparedModuleValidation::default();
879
880 let resolutions = |prepare: &dyn Fn()| {
881 let before = crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get);
882 prepare();
883 crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get) - before
884 };
885 let prepare = || {
886 cache
887 .prepare(
888 &module,
889 &canonical,
890 &source,
891 None,
892 None,
893 ModuleProvenance::User,
894 &validation,
895 )
896 .expect("module prepares");
897 };
898
899 assert_eq!(
903 resolutions(&prepare),
904 1,
905 "the first preparation of a module must derive its interface"
906 );
907 assert_eq!(
908 resolutions(&prepare),
909 0,
910 "the same bytes must not be re-parsed to re-derive the same interface"
911 );
912 }
913
914 #[test]
915 fn fresh_run_revalidates_a_remembered_interface_dependency() {
916 let dir = tempfile::tempdir().expect("temp module dir");
917 let dependency = dir.path().join("dep.harn");
918 std::fs::write(&dependency, "pub enum Color { Ready(string) }\n")
919 .expect("write enum dependency");
920 let module = dir.path().join("library.harn");
921 std::fs::write(
922 &module,
923 r#"import "./dep"
924pub fn exercise(value: any) -> string {
925 match value {
926 Color.Ready(message) -> { return message }
927 _ -> { return "fallback" }
928 }
929}
930"#,
931 )
932 .expect("write dependent module");
933
934 let source = crate::module_source::read(&module).expect("read dependent module");
935 let canonical = harn_modules::canonical_path(&module);
936 let cache = PreparedModuleCache::default();
937 let first = cache
938 .prepare(
939 &module,
940 &canonical,
941 &source,
942 None,
943 None,
944 ModuleProvenance::User,
945 &PreparedModuleValidation::default(),
946 )
947 .expect("prepare with imported enum");
948
949 std::fs::write(&dependency, "pub fn replacement() { return 1 }\n")
950 .expect("replace dependency interface");
951
952 let second = cache
953 .prepare(
954 &module,
955 &canonical,
956 &source,
957 None,
958 None,
959 ModuleProvenance::User,
960 &PreparedModuleValidation::default(),
961 )
962 .expect("prepare after dependency edit");
963
964 assert!(
965 !Arc::ptr_eq(&first, &second),
966 "a fresh run must not reuse bytecode lowered against the old interface"
967 );
968 assert_ne!(
969 postcard::to_allocvec(&first.functions["exercise"].freeze_for_cache())
970 .expect("serialize first function"),
971 postcard::to_allocvec(&second.functions["exercise"].freeze_for_cache())
972 .expect("serialize second function"),
973 "the dependency edit must reach the context-sensitive bytecode"
974 );
975 }
976
977 #[test]
978 fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
979 let cache = PreparedModuleCache::default();
980 let source = ModuleSource::from_text("const value = 1");
981 let _ = cache.insert(
982 PathBuf::from("same.harn"),
983 source.sha256(),
984 empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
985 );
986 assert!(
987 cache
988 .get(
989 Path::new("same.harn"),
990 source.sha256(),
991 ModuleProvenance::User,
992 )
993 .is_none(),
994 "user module lookup must be provenance-separated"
995 );
996 assert!(cache
997 .get(
998 Path::new("same.harn"),
999 source.sha256(),
1000 ModuleProvenance::PrivilegedWire,
1001 )
1002 .is_some());
1003 }
1004
1005 #[test]
1006 fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
1007 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
1008 let first_source = ModuleSource::from_text("pub fn first() { 1 }");
1009 let second_source = ModuleSource::from_text("pub fn second() { 2 }");
1010 let first = empty_artifact();
1011 let first_weak = Arc::downgrade(&first);
1012 drop(cache.insert(
1013 PathBuf::from("first.harn"),
1014 first_source.sha256(),
1015 Arc::clone(&first),
1016 ));
1017 drop(first);
1018
1019 let scanned = empty_artifact();
1022 let scanned_weak = Arc::downgrade(&scanned);
1023 drop(cache.insert(
1024 PathBuf::from("second.harn"),
1025 second_source.sha256(),
1026 Arc::clone(&scanned),
1027 ));
1028 drop(scanned);
1029
1030 assert!(cache
1031 .get(
1032 Path::new("first.harn"),
1033 first_source.sha256(),
1034 ModuleProvenance::User,
1035 )
1036 .is_some());
1037 assert!(cache
1038 .get(
1039 Path::new("second.harn"),
1040 second_source.sha256(),
1041 ModuleProvenance::User,
1042 )
1043 .is_none());
1044 assert!(first_weak.upgrade().is_some());
1045 assert!(scanned_weak.upgrade().is_none());
1046 assert_eq!(cache.stats().insertions, 2);
1047 assert_eq!(cache.stats().evictions, 1);
1048 assert_eq!(cache.stats().entries, 1);
1049
1050 drop(cache);
1051 assert!(first_weak.upgrade().is_none());
1052 }
1053
1054 #[test]
1055 fn cache_key_separates_compiler_configuration() {
1056 let path = PathBuf::from("module.harn");
1057 let key = PreparedModuleCacheKey::new(
1058 path,
1059 ModuleSource::from_text("pub fn value() { 1 }").sha256(),
1060 ModuleProvenance::User,
1061 );
1062 let mut other_compiler = key.clone();
1063 other_compiler.optimizations_enabled = !key.optimizations_enabled;
1064
1065 assert_ne!(key, other_compiler);
1066 }
1067
1068 #[test]
1069 fn cache_counters_saturate_instead_of_wrapping() {
1070 let counter = AtomicU64::new(u64::MAX);
1071 saturating_increment(&counter);
1072 assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
1073 }
1074
1075 #[test]
1076 fn cache_key_separates_imported_symbol_compilation_context() {
1077 let source_path = PathBuf::from("context-sensitive.harn");
1078 let source = ModuleSource::from_text(
1079 r#"
1080import "./library"
1081
1082pub fn exercise(value: any) -> string {
1083 match value {
1084 Color.Ready(message) -> { return message }
1085 _ -> { return "fallback" }
1086 }
1087}
1088"#,
1089 );
1090 let without_imported_enum = compile_module_artifact_from_source_with_context(
1091 &source_path,
1092 source.as_str(),
1093 &ModuleCompilationContext::default(),
1094 )
1095 .expect("compile dynamically-resolved pattern");
1096 let imported_enum_context =
1097 ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
1098 let with_imported_enum = compile_module_artifact_from_source_with_context(
1099 &source_path,
1100 source.as_str(),
1101 &imported_enum_context,
1102 )
1103 .expect("compile imported-enum-resolved pattern");
1104 assert_ne!(
1105 postcard::to_allocvec(&without_imported_enum.functions["exercise"])
1106 .expect("serialize dynamically-resolved function"),
1107 postcard::to_allocvec(&with_imported_enum.functions["exercise"])
1108 .expect("serialize imported-enum-resolved function"),
1109 "the imported enum projection must demonstrably alter bytecode"
1110 );
1111
1112 let cache = PreparedModuleCache::default();
1113 let validation = PreparedModuleValidation::default();
1114 let without_imported_enum = cache
1115 .prepare(
1116 &source_path,
1117 &source_path,
1118 &source,
1119 Some(&ModuleCompilationContext::default()),
1120 None,
1121 ModuleProvenance::User,
1122 &validation,
1123 )
1124 .expect("prepare dynamically-resolved artifact");
1125 let with_imported_enum = cache
1126 .prepare(
1127 &source_path,
1128 &source_path,
1129 &source,
1130 Some(&imported_enum_context),
1131 None,
1132 ModuleProvenance::User,
1133 &validation,
1134 )
1135 .expect("prepare imported-enum-resolved artifact");
1136
1137 assert!(
1138 !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
1139 "one source/path/provenance with distinct imported projections must not alias"
1140 );
1141 assert_ne!(
1142 postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
1143 .expect("serialize cached dynamically-resolved function"),
1144 postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
1145 .expect("serialize cached imported-enum-resolved function")
1146 );
1147 assert_eq!(cache.stats().insertions, 2);
1148 }
1149
1150 #[test]
1151 fn dropping_last_cache_handle_releases_prepared_artifacts() {
1152 let cache = PreparedModuleCache::default();
1153 let path = PathBuf::from("module.harn");
1154 let source = ModuleSource::from_text("pub fn value() { 1 }");
1155 let artifact = empty_artifact();
1156 let weak = Arc::downgrade(&artifact);
1157 let _ = cache.insert(path, source.sha256(), artifact);
1158 let clone = cache.clone();
1159
1160 drop(cache);
1161 assert!(weak.upgrade().is_some());
1162 drop(clone);
1163 assert!(weak.upgrade().is_none());
1164 }
1165
1166 #[test]
1167 fn concurrent_identical_misses_compile_one_immutable_artifact() {
1168 const WORKERS: usize = 8;
1169
1170 let cache = PreparedModuleCache::default();
1171 let nonce_dir = tempfile::tempdir().expect("temp dir for a unique module identity");
1184 let nonce = nonce_dir
1185 .path()
1186 .file_name()
1187 .expect("temp dir has a final component")
1188 .to_string_lossy()
1189 .into_owned();
1190 let source = Arc::new(ModuleSource::from_text(
1191 std::iter::once(format!("// {nonce}\n"))
1192 .chain(
1193 (0..128).map(|index| format!("pub fn value_{index}() {{ return {index} }}\n")),
1194 )
1195 .collect::<String>(),
1196 ));
1197 let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
1198 let validation = PreparedModuleValidation::default();
1199 let start = Arc::new(Barrier::new(WORKERS + 1));
1200 let mut handles = Vec::with_capacity(WORKERS);
1201
1202 for _ in 0..WORKERS {
1203 let cache = cache.clone();
1204 let source = Arc::clone(&source);
1205 let source_path = Arc::clone(&source_path);
1206 let validation = validation.clone();
1207 let start = Arc::clone(&start);
1208 handles.push(std::thread::spawn(move || {
1209 let recorder = ModulePhaseRecorder::new();
1210 start.wait();
1211 let artifact = cache
1212 .prepare(
1213 &source_path,
1214 &source_path,
1215 &source,
1216 None,
1217 Some(&recorder),
1218 ModuleProvenance::TrustedHostDispatch,
1219 &validation,
1220 )
1221 .expect("compile shared immutable module");
1222 (artifact, recorder.snapshot())
1223 }));
1224 }
1225
1226 start.wait();
1227 let outcomes = handles
1228 .into_iter()
1229 .map(|handle| handle.join().expect("module compiler worker joins"))
1230 .collect::<Vec<_>>();
1231 let first = &outcomes[0].0;
1232
1233 assert!(
1234 outcomes
1235 .iter()
1236 .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
1237 "all workers must consume the same immutable prepared artifact"
1238 );
1239 assert_eq!(
1240 outcomes
1241 .iter()
1242 .map(|(_, phases)| phases.modules_compiled)
1243 .sum::<u64>(),
1244 1,
1245 "one exact cache key must have one compilation owner regardless of worker count"
1246 );
1247 assert_eq!(cache.stats().insertions, 1);
1248 }
1249
1250 #[test]
1251 fn failed_preparation_is_not_cached_or_poisoned() {
1252 let cache = PreparedModuleCache::default();
1253 let key = PreparedModuleCacheKey::new(
1254 PathBuf::from("recoverable.harn"),
1255 ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
1256 ModuleProvenance::TrustedHostDispatch,
1257 );
1258
1259 let failed = cache.prepare_exact_key(&key, None, || {
1260 Err(VmError::Runtime(
1261 "synthetic compilation failure".to_string(),
1262 ))
1263 });
1264 assert!(
1265 matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
1266 );
1267 assert_eq!(cache.stats().entries, 0);
1268 assert_eq!(cache.stats().insertions, 0);
1269
1270 let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
1271 let prepared = cache
1272 .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
1273 .expect("a failed owner must release the exact-key preparation slot");
1274
1275 assert!(Arc::ptr_eq(&prepared, &expected));
1276 assert_eq!(cache.stats().misses, 2);
1277 assert_eq!(cache.stats().insertions, 1);
1278 assert_eq!(cache.stats().entries, 1);
1279 }
1280
1281 #[test]
1282 fn hydration_moves_module_owned_storage() {
1283 let source = r#"
1284import { assert_eq } from "std/testing"
1285pub type Result = {value: int}
1286pub const value = 1
1287pub fn answer(items: list<string>) {
1288 fn nested() { return 42 }
1289 return items
1290}
1291"#;
1292 let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
1293 .expect("compile typed module artifact");
1294
1295 let imports = artifact.imports.as_ptr();
1296 let import_path = artifact.imports[0].path.as_ptr();
1297 let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
1298 panic!("expected selective import");
1299 };
1300 let selected_names = selected.as_ptr();
1301 let selected_name = selected[0].as_ptr();
1302 let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
1303 let schema_init_codes = artifact
1304 .type_schema_init_chunks
1305 .iter()
1306 .map(|chunk| chunk.code.as_ptr())
1307 .collect::<Vec<_>>();
1308 let (function_key, function) = artifact.functions.first_key_value().unwrap();
1309 let function_key = function_key.as_ptr();
1310 let function_name = function.name.clone();
1311 let function_code = function.chunk.code.as_ptr();
1312 let param_name = function.params[0].name.as_ptr();
1313 let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
1314 let nested_name = function.chunk.functions[0].name.clone();
1315 let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
1316 let public_export_name = artifact
1317 .public_exports
1318 .get_key_value("answer")
1319 .unwrap()
1320 .0
1321 .as_ptr();
1322 let public_export_kind = *artifact.public_exports.get("answer").unwrap();
1323 let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
1324 let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
1325 let hydrated = PreparedModuleArtifact::from_cached(artifact);
1326
1327 assert_eq!(hydrated.imports.as_ptr(), imports);
1328 assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
1329 let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
1330 panic!("expected selective import");
1331 };
1332 assert_eq!(selected.as_ptr(), selected_names);
1333 assert_eq!(selected[0].as_ptr(), selected_name);
1334 assert_eq!(
1335 hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
1336 init_code
1337 );
1338 assert_eq!(
1339 hydrated
1340 .type_schema_init_chunks
1341 .iter()
1342 .map(|chunk| chunk.code.as_ptr())
1343 .collect::<Vec<_>>(),
1344 schema_init_codes
1345 );
1346 let (hydrated_function_key, hydrated_function) =
1347 hydrated.functions.first_key_value().unwrap();
1348 assert_eq!(hydrated_function_key.as_ptr(), function_key);
1349 assert_eq!(hydrated_function.name.as_str(), function_name);
1352 assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
1353 assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
1354 assert_eq!(
1355 named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
1356 param_type_name
1357 );
1358 assert_eq!(
1359 hydrated_function.chunk.functions[0].name.as_str(),
1360 nested_name
1361 );
1362 assert_eq!(
1363 hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
1364 nested_code
1365 );
1366 assert_eq!(
1367 hydrated
1368 .public_exports
1369 .get_key_value("answer")
1370 .unwrap()
1371 .0
1372 .as_ptr(),
1373 public_export_name
1374 );
1375 assert_eq!(
1376 hydrated.public_exports.get("answer"),
1377 Some(&public_export_kind)
1378 );
1379 assert_eq!(
1380 hydrated.public_value_names.get("value").unwrap().as_ptr(),
1381 public_value_name
1382 );
1383 assert_eq!(
1384 hydrated.public_type_names.get("Result").unwrap().as_ptr(),
1385 public_type_name
1386 );
1387 }
1388}