Skip to main content

harn_vm/
prepared_module.rs

1//! Scoped cache of immutable, hydrated module bytecode.
2//!
3//! Prepared modules deliberately stop before runtime instantiation. Each VM
4//! still receives fresh closures, function registries, module state, and init
5//! execution; only the serialized-to-runtime bytecode conversion is reused.
6
7use 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;
28/// Ceiling on remembered imported interfaces. Independent of the artifact
29/// capacity above: an interface is a small projection of names, and one is
30/// worth keeping for every module a tree contains, not just for the artifacts
31/// that fit in the bounded cache.
32const MAX_REMEMBERED_INTERFACES: usize = 8192;
33
34mod generation;
35pub use generation::PreparedModuleGenerationStats;
36
37/// Immutable runtime form of one compiled module artifact.
38pub(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    /// `source_hash` is the same SHA-256 that names the module's on-disk
96    /// artifact. Keying on it rather than a second digest of the same bytes
97    /// means a warm module load hashes its source once, and lets a caller
98    /// holding a recorded digest find a prepared artifact without the bytes.
99    #[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/// Typed counters for a [`PreparedModuleCache`] lifetime.
171#[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    /// Artifacts discarded by the bounded cache, including cold scan
178    /// candidates rejected by S3-FIFO admission before becoming residents.
179    pub evictions: u64,
180    pub entries: usize,
181}
182
183/// A bounded, shareable cache of immutable module bytecode templates.
184///
185/// The handle is explicit so embedders can scope reuse to one test suite,
186/// worker, watch generation, or VM baseline. Dropping the final handle releases
187/// every prepared artifact; historical user source never accumulates globally.
188/// Concurrent misses for one exact key share a single preparation owner, while
189/// unrelated modules remain independently preparable.
190#[derive(Clone)]
191pub struct PreparedModuleCache {
192    entries: Arc<PreparedArtifactCache>,
193    counters: Arc<PreparedModuleCacheCounters>,
194    /// Imported interfaces already derived for a module's exact bytes.
195    ///
196    /// The interface is part of an entry's key, so it has to be in hand before
197    /// this cache can be asked whether it holds that entry — and deriving one
198    /// lexes and parses the module. That put a full parse of every module in
199    /// front of every lookup, which is most of what this cache exists to
200    /// avoid: a suite that prepares its import graph once then re-derives the
201    /// same interfaces for every VM that imports them.
202    ///
203    /// Keyed by the module's own bytes, so an edited module derives afresh.
204    /// It is scoped to this cache handle rather than the process, and
205    /// [`PreparedModuleCache::prepare_import_graph`] clears it before seeding
206    /// from a freshly walked graph, so a run that re-prepares its graph starts
207    /// from current interfaces rather than a previous generation's.
208    interfaces: Arc<Mutex<HashMap<InterfaceMemoKey, InterfaceMemoEntry>>>,
209    /// Exact source bytes captured with the most recently prepared graph.
210    ///
211    /// A long-lived host can install this closed snapshot into each fresh VM
212    /// alongside the immutable bytecode templates. Runtime instantiation then
213    /// performs no source reads while still creating fresh module state. The
214    /// snapshot is generation-scoped and replaced, never extended, on a new
215    /// graph preparation.
216    sources: Arc<Mutex<BTreeMap<PathBuf, Arc<str>>>>,
217}
218
219/// One module's bytes under one authority — everything a derived interface is
220/// a function of, apart from its dependencies' bytes.
221#[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    /// One graph capture is shared by every interface it produced. `None` is
232    /// reserved for `prepare_import_graph`, whose fresh whole-graph walk clears
233    /// and replaces the memo before publishing its contexts.
234    manifest: Option<Arc<PreparedModuleManifest>>,
235}
236
237/// One import graph's refreshable filesystem proof.
238///
239/// A racily clean manifest carries a newer capture stamp after its content
240/// check. Keep that stamp beside every interface from the graph so later runs
241/// settle onto stats-only validation instead of rereading the same files.
242struct 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/// Filesystem validation shared by one VM execution tree.
271///
272/// A manifest can describe every module in a closure. Cache its result by Arc
273/// identity so loading N modules from that closure performs one graph recheck,
274/// while a fresh VM gets a fresh observation and notices edits made between
275/// runs that share the same prepared-module cache handle.
276#[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        // The graph owns its lock while checking and refreshing. The validation
301        // registry lock is deliberately not held across filesystem work, so
302        // unrelated graphs can validate in parallel.
303        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        // A handle held across many generations of an edited tree would
394        // otherwise accumulate one entry per version of every module ever
395        // prepared. Start over rather than grow without bound: the entries are
396        // derivable, so the cost of dropping them is bounded by re-deriving the
397        // ones still in use. The bound is far above the module count of a real
398        // tree, so an ordinary run never reaches it.
399        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    /// Prepare every import reachable from `roots` without instantiating or
447    /// executing module state.
448    ///
449    /// Root files themselves are entry programs, not runtime imports, so only
450    /// their transitive import closure is prepared. Invalid modules are left
451    /// uncached for the canonical VM load to diagnose.
452    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    /// Prepare a Rust-embedder-selected host-dispatch graph without making its
458    /// bytecode visible to ordinary user imports. The in-memory cache key
459    /// retains provenance, and fresh VMs still instantiate independent module
460    /// state from the immutable artifacts.
461    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        // This walk reads every reachable file, so the interfaces it derives
480        // supersede anything remembered from an earlier generation of the same
481        // tree.
482        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            // A root may be spelled through a filesystem alias (for example,
548            // macOS `/var` and `/private/var`). Runtime relative imports retain
549            // the caller's root spelling after the source files disappear, so
550            // seed that lexical spelling beside the canonical graph identity.
551            // Both names point at the same immutable bytes.
552            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            // A complete generation is immutable by construction. Its source
580            // snapshot, interfaces, and artifacts were captured by this one
581            // graph walk and are swapped together by the owning host. Do not
582            // revalidate those interfaces against a later filesystem state on
583            // every invocation; a watcher prepares a replacement generation
584            // when source changes.
585            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            // Disk cache hits skip parse + compile. The scoped prepared cache
757            // additionally skips deserialization and chunk hydration on later
758            // fresh VMs without sharing any runtime module state.
759            // Every provenance shares one cache path. The cache key carries the
760            // authority, so the on-disk identity already separates a trusted
761            // artifact from an ordinary one: they hash to different shared-cache
762            // filenames, and an adjacent artifact found by path fails the other
763            // authority's header check. Before the key had that field, the only
764            // thing keeping privileged bytecode out of an ordinary reader's
765            // reach was this branch skipping the cache entirely, which also
766            // meant a trusted graph recompiled from source on every process.
767            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                    // Same `provenance` that keyed the lookup above, so the
782                    // artifact stored on a miss can only be found by a reader
783                    // asking for the authority it was compiled under.
784                    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        // Every VM that imports a module asks this cache for it, and the
869        // interface is needed to form the key it asks with. Deriving one costs
870        // a full lex and parse, so re-deriving it per VM put the cache's own
871        // cost back in front of every hit it served.
872        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        // The first caller has nothing to reuse. This arm is the counter's
900        // positive control: without it, a seam that never increments would
901        // satisfy the assertion below vacuously.
902        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        // quick_cache's scan-resistant admission deliberately preserves the
1020        // resident hot key when a new key appears only once at capacity.
1021        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        // The nonce makes this source content nobody has compiled before, so
1172        // the shared disk cache cannot serve it and every worker genuinely
1173        // races to compile. Without it the test asserts single-flight against
1174        // a key an earlier run already stored: it passes cold and reads zero
1175        // compilations warm. Trusted modules used to skip the disk cache
1176        // entirely, which hid this by making the test hermetic by accident.
1177        //
1178        // What this needs is uniqueness, not time, so it takes the randomness
1179        // `tempfile` already uses to name a directory no other process holds.
1180        // A clock would be a flaky-test pattern, and a pid plus a counter can
1181        // repeat once the OS recycles that pid against a cache that outlives
1182        // the run.
1183        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        // Function names convert into a shared `HarnStr` at hydration (one
1350        // short copy) so per-call consumers can share them; compare by value.
1351        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}