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_module_artifact_from_source_with_context,
21    compile_trusted_host_dispatch_module_artifact_from_source_with_context, ModuleArtifact,
22    ModuleCompilationContext, ModuleImportSpec, ModuleProvenance,
23};
24use crate::module_source::ModuleSource;
25use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
26const DEFAULT_MAX_ENTRIES: usize = 512;
27/// Ceiling on remembered imported interfaces. Independent of the artifact
28/// capacity above: an interface is a small projection of names, and one is
29/// worth keeping for every module a tree contains, not just for the artifacts
30/// that fit in the bounded cache.
31const MAX_REMEMBERED_INTERFACES: usize = 8192;
32
33/// Immutable runtime form of one compiled module artifact.
34pub(crate) struct PreparedModuleArtifact {
35    pub(crate) provenance: ModuleProvenance,
36    pub(crate) imports: Vec<ModuleImportSpec>,
37    pub(crate) type_schema_init_chunks: Vec<Arc<Chunk>>,
38    pub(crate) init_chunk: Option<Arc<Chunk>>,
39    pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
40    pub(crate) public_exports: BTreeMap<String, DefKind>,
41    pub(crate) public_value_names: std::collections::HashSet<String>,
42    pub(crate) public_type_names: std::collections::HashSet<String>,
43}
44
45impl PreparedModuleArtifact {
46    pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
47        let ModuleArtifact {
48            provenance,
49            imports,
50            type_schema_init_chunks,
51            init_chunk,
52            functions,
53            public_exports,
54            public_value_names,
55            public_type_names,
56        } = artifact;
57        let type_schema_init_chunks = type_schema_init_chunks
58            .into_iter()
59            .map(|chunk| Arc::new(Chunk::from_cached(chunk)))
60            .collect();
61        let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
62        let functions = functions
63            .into_iter()
64            .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
65            .collect();
66        Self {
67            provenance,
68            imports,
69            type_schema_init_chunks,
70            init_chunk,
71            functions,
72            public_exports,
73            public_value_names,
74            public_type_names,
75        }
76    }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
80struct PreparedModuleCacheKey {
81    canonical_path: PathBuf,
82    source_hash: [u8; 32],
83    provenance: ModuleProvenance,
84    harn_version: &'static str,
85    codegen_fingerprint: &'static str,
86    optimizations_enabled: bool,
87    compilation_context_digest: [u8; 32],
88}
89
90impl PreparedModuleCacheKey {
91    /// `source_hash` is the same SHA-256 that names the module's on-disk
92    /// artifact. Keying on it rather than a second digest of the same bytes
93    /// means a warm module load hashes its source once, and lets a caller
94    /// holding a recorded digest find a prepared artifact without the bytes.
95    #[cfg(test)]
96    fn new(canonical_path: PathBuf, source_hash: [u8; 32], provenance: ModuleProvenance) -> Self {
97        Self::with_context(
98            canonical_path,
99            source_hash,
100            provenance,
101            &ModuleCompilationContext::default(),
102        )
103    }
104
105    fn with_context(
106        canonical_path: PathBuf,
107        source_hash: [u8; 32],
108        provenance: ModuleProvenance,
109        compilation_context: &ModuleCompilationContext,
110    ) -> Self {
111        Self {
112            canonical_path,
113            source_hash,
114            provenance,
115            harn_version: crate::bytecode_cache::HARN_VERSION,
116            codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
117            optimizations_enabled: crate::compiler::CompilerOptions::from_env()
118                .optimizations_enabled(),
119            compilation_context_digest: compilation_context.digest(),
120        }
121    }
122}
123
124#[derive(Default)]
125struct PreparedModuleCacheCounters {
126    hits: AtomicU64,
127    misses: AtomicU64,
128    insertions: AtomicU64,
129    evictions: AtomicU64,
130}
131
132fn saturating_increment(counter: &AtomicU64) {
133    let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
134        Some(value.saturating_add(1))
135    });
136}
137
138#[derive(Clone)]
139struct PreparedModuleCacheLifecycle {
140    counters: Arc<PreparedModuleCacheCounters>,
141}
142
143impl Lifecycle<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>
144    for PreparedModuleCacheLifecycle
145{
146    type RequestState = ();
147
148    fn on_evict(
149        &self,
150        _state: &mut Self::RequestState,
151        _key: PreparedModuleCacheKey,
152        _artifact: Arc<PreparedModuleArtifact>,
153    ) {
154        saturating_increment(&self.counters.evictions);
155    }
156}
157
158type PreparedArtifactCache = Cache<
159    PreparedModuleCacheKey,
160    Arc<PreparedModuleArtifact>,
161    UnitWeighter,
162    DefaultHashBuilder,
163    PreparedModuleCacheLifecycle,
164>;
165
166/// Typed counters for a [`PreparedModuleCache`] lifetime.
167#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct PreparedModuleCacheStats {
170    pub hits: u64,
171    pub misses: u64,
172    pub insertions: u64,
173    /// Artifacts discarded by the bounded cache, including cold scan
174    /// candidates rejected by S3-FIFO admission before becoming residents.
175    pub evictions: u64,
176    pub entries: usize,
177}
178
179/// A bounded, shareable cache of immutable module bytecode templates.
180///
181/// The handle is explicit so embedders can scope reuse to one test suite,
182/// worker, watch generation, or VM baseline. Dropping the final handle releases
183/// every prepared artifact; historical user source never accumulates globally.
184/// Concurrent misses for one exact key share a single preparation owner, while
185/// unrelated modules remain independently preparable.
186#[derive(Clone)]
187pub struct PreparedModuleCache {
188    entries: Arc<PreparedArtifactCache>,
189    counters: Arc<PreparedModuleCacheCounters>,
190    /// Imported interfaces already derived for a module's exact bytes.
191    ///
192    /// The interface is part of an entry's key, so it has to be in hand before
193    /// this cache can be asked whether it holds that entry — and deriving one
194    /// lexes and parses the module. That put a full parse of every module in
195    /// front of every lookup, which is most of what this cache exists to
196    /// avoid: a suite that prepares its import graph once then re-derives the
197    /// same interfaces for every VM that imports them.
198    ///
199    /// Keyed by the module's own bytes, so an edited module derives afresh.
200    /// It is scoped to this cache handle rather than the process, and
201    /// [`PreparedModuleCache::prepare_import_graph`] clears it before seeding
202    /// from a freshly walked graph, so a run that re-prepares its graph starts
203    /// from current interfaces rather than a previous generation's.
204    interfaces: Arc<Mutex<HashMap<InterfaceMemoKey, InterfaceMemoEntry>>>,
205}
206
207/// One module's bytes under one authority — everything a derived interface is
208/// a function of, apart from its dependencies' bytes.
209#[derive(Clone, PartialEq, Eq, Hash)]
210struct InterfaceMemoKey {
211    canonical_path: PathBuf,
212    source_hash: [u8; 32],
213    provenance: ModuleProvenance,
214}
215
216#[derive(Clone)]
217struct InterfaceMemoEntry {
218    context: ModuleCompilationContext,
219    /// One graph capture is shared by every interface it produced. `None` is
220    /// reserved for `prepare_import_graph`, whose fresh whole-graph walk clears
221    /// and replaces the memo before publishing its contexts.
222    manifest: Option<Arc<PreparedModuleManifest>>,
223}
224
225/// One import graph's refreshable filesystem proof.
226///
227/// A racily clean manifest carries a newer capture stamp after its content
228/// check. Keep that stamp beside every interface from the graph so later runs
229/// settle onto stats-only validation instead of rereading the same files.
230struct PreparedModuleManifest {
231    manifest: Mutex<ContextManifest>,
232}
233
234impl PreparedModuleManifest {
235    fn new(manifest: ContextManifest) -> Self {
236        Self {
237            manifest: Mutex::new(manifest),
238        }
239    }
240
241    fn is_valid(&self) -> bool {
242        let mut manifest = self
243            .manifest
244            .lock()
245            .expect("prepared-module manifest lock poisoned");
246        let entry = manifest.entry.clone();
247        match manifest.check(&entry) {
248            ManifestCheck::Stale => false,
249            ManifestCheck::Valid => true,
250            ManifestCheck::ValidAfterRecheck { refreshed } => {
251                *manifest = refreshed;
252                true
253            }
254        }
255    }
256}
257
258/// Filesystem validation shared by one VM execution tree.
259///
260/// A manifest can describe every module in a closure. Cache its result by Arc
261/// identity so loading N modules from that closure performs one graph recheck,
262/// while a fresh VM gets a fresh observation and notices edits made between
263/// runs that share the same prepared-module cache handle.
264#[derive(Clone, Default)]
265pub(crate) struct PreparedModuleValidation {
266    checked: Arc<Mutex<Vec<PreparedModuleCheck>>>,
267}
268
269struct PreparedModuleCheck {
270    manifest: Arc<PreparedModuleManifest>,
271    valid: bool,
272}
273
274impl PreparedModuleValidation {
275    fn is_valid(&self, manifest: &Arc<PreparedModuleManifest>) -> bool {
276        {
277            let checked = self
278                .checked
279                .lock()
280                .expect("prepared-module validation lock poisoned");
281            if let Some(check) = checked
282                .iter()
283                .find(|check| Arc::ptr_eq(&check.manifest, manifest))
284            {
285                return check.valid;
286            }
287        }
288        // The graph owns its lock while checking and refreshing. The validation
289        // registry lock is deliberately not held across filesystem work, so
290        // unrelated graphs can validate in parallel.
291        let valid = manifest.is_valid();
292        let mut checked = self
293            .checked
294            .lock()
295            .expect("prepared-module validation lock poisoned");
296        if let Some(check) = checked
297            .iter()
298            .find(|check| Arc::ptr_eq(&check.manifest, manifest))
299        {
300            return check.valid;
301        }
302        checked.push(PreparedModuleCheck {
303            manifest: Arc::clone(manifest),
304            valid,
305        });
306        valid
307    }
308
309    fn remember_fresh(&self, manifest: &Arc<PreparedModuleManifest>) {
310        self.checked
311            .lock()
312            .expect("prepared-module validation lock poisoned")
313            .push(PreparedModuleCheck {
314                manifest: Arc::clone(manifest),
315                valid: true,
316            });
317    }
318}
319
320impl Default for PreparedModuleCache {
321    fn default() -> Self {
322        Self::with_capacity(
323            NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
324        )
325    }
326}
327
328impl PreparedModuleCache {
329    pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
330        let counters = Arc::new(PreparedModuleCacheCounters::default());
331        let lifecycle = PreparedModuleCacheLifecycle {
332            counters: Arc::clone(&counters),
333        };
334        let capacity = max_entries.get();
335        Self {
336            entries: Arc::new(Cache::with(
337                capacity,
338                capacity as u64,
339                UnitWeighter,
340                DefaultHashBuilder::default(),
341                lifecycle,
342            )),
343            counters,
344            interfaces: Arc::new(Mutex::new(HashMap::new())),
345        }
346    }
347
348    fn remembered_interface(
349        &self,
350        key: &InterfaceMemoKey,
351        validation: &PreparedModuleValidation,
352    ) -> Option<ModuleCompilationContext> {
353        let entry = self
354            .interfaces
355            .lock()
356            .expect("interface memo lock poisoned")
357            .get(key)
358            .cloned()?;
359        if entry
360            .manifest
361            .as_ref()
362            .is_none_or(|manifest| validation.is_valid(manifest))
363        {
364            Some(entry.context)
365        } else {
366            None
367        }
368    }
369
370    fn remember_interface(
371        &self,
372        key: InterfaceMemoKey,
373        context: &ModuleCompilationContext,
374        manifest: Option<Arc<PreparedModuleManifest>>,
375    ) {
376        let mut interfaces = self
377            .interfaces
378            .lock()
379            .expect("interface memo lock poisoned");
380        // A handle held across many generations of an edited tree would
381        // otherwise accumulate one entry per version of every module ever
382        // prepared. Start over rather than grow without bound: the entries are
383        // derivable, so the cost of dropping them is bounded by re-deriving the
384        // ones still in use. The bound is far above the module count of a real
385        // tree, so an ordinary run never reaches it.
386        if interfaces.len() >= MAX_REMEMBERED_INTERFACES {
387            interfaces.clear();
388        }
389        interfaces.insert(
390            key,
391            InterfaceMemoEntry {
392                context: context.clone(),
393                manifest,
394            },
395        );
396    }
397
398    fn remember_interface_graph(
399        &self,
400        root_key: InterfaceMemoKey,
401        root_context: &ModuleCompilationContext,
402        manifest: ContextManifest,
403        provenance: ModuleProvenance,
404        validation: &PreparedModuleValidation,
405    ) {
406        let files = manifest.files.clone();
407        let manifest = Arc::new(PreparedModuleManifest::new(manifest));
408        validation.remember_fresh(&manifest);
409        self.remember_interface(root_key, root_context, Some(Arc::clone(&manifest)));
410        for file in files {
411            self.remember_interface(
412                InterfaceMemoKey {
413                    canonical_path: file.path,
414                    source_hash: file.content_hash,
415                    provenance,
416                },
417                &file.compilation_context,
418                Some(Arc::clone(&manifest)),
419            );
420        }
421    }
422
423    pub fn stats(&self) -> PreparedModuleCacheStats {
424        PreparedModuleCacheStats {
425            hits: self.counters.hits.load(Ordering::Relaxed),
426            misses: self.counters.misses.load(Ordering::Relaxed),
427            insertions: self.counters.insertions.load(Ordering::Relaxed),
428            evictions: self.counters.evictions.load(Ordering::Relaxed),
429            entries: self.entries.len(),
430        }
431    }
432
433    /// Prepare every import reachable from `roots` without instantiating or
434    /// executing module state.
435    ///
436    /// Root files themselves are entry programs, not runtime imports, so only
437    /// their transitive import closure is prepared. Invalid modules are left
438    /// uncached for the canonical VM load to diagnose.
439    pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
440        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
441    }
442
443    /// Prepare a Rust-embedder-selected host-dispatch graph without making its
444    /// bytecode visible to ordinary user imports. The in-memory cache key
445    /// retains provenance, and fresh VMs still instantiate independent module
446    /// state from the immutable artifacts.
447    pub fn prepare_trusted_host_dispatch_import_graph(
448        &self,
449        roots: &[PathBuf],
450    ) -> ModulePhaseStats {
451        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch)
452    }
453
454    fn prepare_import_graph_with_provenance(
455        &self,
456        roots: &[PathBuf],
457        provenance: ModuleProvenance,
458    ) -> ModulePhaseStats {
459        if roots.is_empty() {
460            return ModulePhaseStats::default();
461        }
462
463        // This walk reads every reachable file, so the interfaces it derives
464        // supersede anything remembered from an earlier generation of the same
465        // tree.
466        self.interfaces
467            .lock()
468            .expect("interface memo lock poisoned")
469            .clear();
470        let graph = harn_modules::build(roots);
471        let root_paths = roots
472            .iter()
473            .map(|path| harn_modules::canonical_path(path))
474            .collect::<std::collections::HashSet<_>>();
475        let recorder = ModulePhaseRecorder::new();
476        let validation = PreparedModuleValidation::default();
477
478        for path in graph.module_paths() {
479            if root_paths.contains(&harn_modules::canonical_path(&path)) {
480                continue;
481            }
482            if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
483                let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
484                continue;
485            }
486
487            let source = {
488                let _load_span = recorder.load_span();
489                match crate::module_source::read(&path) {
490                    Ok(source) => source,
491                    Err(_) => continue,
492                }
493            };
494            let Ok(compilation_context) =
495                ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
496            else {
497                continue;
498            };
499            let canonical = harn_modules::canonical_path(&path);
500            let _ = self.prepare(
501                &path,
502                &canonical,
503                &source,
504                Some(&compilation_context),
505                Some(&recorder),
506                provenance,
507                &validation,
508            );
509        }
510
511        recorder.snapshot()
512    }
513
514    #[cfg(test)]
515    pub(crate) fn get(
516        &self,
517        canonical_path: &Path,
518        source_hash: [u8; 32],
519        provenance: ModuleProvenance,
520    ) -> Option<Arc<PreparedModuleArtifact>> {
521        self.get_with_context(
522            canonical_path,
523            source_hash,
524            provenance,
525            &ModuleCompilationContext::default(),
526        )
527    }
528
529    pub(crate) fn get_with_context(
530        &self,
531        canonical_path: &Path,
532        source_hash: [u8; 32],
533        provenance: ModuleProvenance,
534        compilation_context: &ModuleCompilationContext,
535    ) -> Option<Arc<PreparedModuleArtifact>> {
536        let key = PreparedModuleCacheKey::with_context(
537            canonical_path.to_path_buf(),
538            source_hash,
539            provenance,
540            compilation_context,
541        );
542        let artifact = self.entries.get(&key);
543        if artifact.is_some() {
544            saturating_increment(&self.counters.hits);
545        } else {
546            saturating_increment(&self.counters.misses);
547        }
548        artifact
549    }
550
551    #[cfg(test)]
552    pub(crate) fn insert(
553        &self,
554        canonical_path: PathBuf,
555        source_hash: [u8; 32],
556        artifact: Arc<PreparedModuleArtifact>,
557    ) -> Arc<PreparedModuleArtifact> {
558        self.insert_with_context(
559            canonical_path,
560            source_hash,
561            &ModuleCompilationContext::default(),
562            artifact,
563        )
564    }
565
566    pub(crate) fn insert_with_context(
567        &self,
568        canonical_path: PathBuf,
569        source_hash: [u8; 32],
570        compilation_context: &ModuleCompilationContext,
571        artifact: Arc<PreparedModuleArtifact>,
572    ) -> Arc<PreparedModuleArtifact> {
573        let key = PreparedModuleCacheKey::with_context(
574            canonical_path,
575            source_hash,
576            artifact.provenance,
577            compilation_context,
578        );
579        match self.entries.get_value_or_guard(&key, None) {
580            GuardResult::Value(existing) => existing,
581            GuardResult::Guard(guard) => {
582                if guard.insert(Arc::clone(&artifact)).is_ok() {
583                    saturating_increment(&self.counters.insertions);
584                }
585                artifact
586            }
587            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
588        }
589    }
590
591    fn prepare_exact_key(
592        &self,
593        key: &PreparedModuleCacheKey,
594        recorder: Option<&ModulePhaseRecorder>,
595        prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
596    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
597        let prepared = {
598            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
599            self.entries.get(key)
600        };
601        if let Some(prepared) = prepared {
602            saturating_increment(&self.counters.hits);
603            return Ok(prepared);
604        }
605        saturating_increment(&self.counters.misses);
606
607        let guarded = {
608            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
609            self.entries.get_value_or_guard(key, None)
610        };
611        match guarded {
612            GuardResult::Value(prepared) => Ok(prepared),
613            GuardResult::Guard(guard) => {
614                let prepared = prepare()?;
615                if guard.insert(Arc::clone(&prepared)).is_ok() {
616                    saturating_increment(&self.counters.insertions);
617                }
618                Ok(prepared)
619            }
620            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
621        }
622    }
623
624    pub(crate) fn prepare(
625        &self,
626        source_path: &Path,
627        canonical_path: &Path,
628        source: &ModuleSource,
629        compilation_context: Option<&ModuleCompilationContext>,
630        recorder: Option<&ModulePhaseRecorder>,
631        provenance: ModuleProvenance,
632        validation: &PreparedModuleValidation,
633    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
634        let source_hash = {
635            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
636            source.sha256()
637        };
638        let memo_key = InterfaceMemoKey {
639            canonical_path: canonical_path.to_path_buf(),
640            source_hash,
641            provenance,
642        };
643        let compilation_context = match compilation_context {
644            Some(context) => {
645                self.remember_interface(memo_key, context, None);
646                context.clone()
647            }
648            None => match self.remembered_interface(&memo_key, validation) {
649                Some(context) => context,
650                None => {
651                    let (context, manifest) =
652                        crate::bytecode_cache::module_compilation_context_with_manifest(
653                            source_path,
654                            source.as_str(),
655                        )?;
656                    if let Some(manifest) = manifest {
657                        self.remember_interface_graph(
658                            memo_key, &context, manifest, provenance, validation,
659                        );
660                    }
661                    context
662                }
663            },
664        };
665        let key = PreparedModuleCacheKey::with_context(
666            canonical_path.to_path_buf(),
667            source_hash,
668            provenance,
669            &compilation_context,
670        );
671        self.prepare_exact_key(&key, recorder, || {
672            // Disk cache hits skip parse + compile. The scoped prepared cache
673            // additionally skips deserialization and chunk hydration on later
674            // fresh VMs without sharing any runtime module state.
675            // Every provenance shares one cache path. The cache key carries the
676            // authority, so the on-disk identity already separates a trusted
677            // artifact from an ordinary one: they hash to different shared-cache
678            // filenames, and an adjacent artifact found by path fails the other
679            // authority's header check. Before the key had that field, the only
680            // thing keeping privileged bytecode out of an ordinary reader's
681            // reach was this branch skipping the cache entirely, which also
682            // meant a trusted graph recompiled from source on every process.
683            let cached = {
684                let lookup = {
685                    let _load_span = recorder.map(ModulePhaseRecorder::load_span);
686                    crate::bytecode_cache::load_module(
687                        source_path,
688                        source,
689                        &compilation_context,
690                        provenance,
691                    )
692                };
693                if let Some(artifact) = lookup.artifact {
694                    artifact
695                } else {
696                    let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
697                    // Same `provenance` that keyed the lookup above, so the
698                    // artifact stored on a miss can only be found by a reader
699                    // asking for the authority it was compiled under.
700                    let compiled = if provenance == ModuleProvenance::TrustedHostDispatch {
701                        compile_trusted_host_dispatch_module_artifact_from_source_with_context(
702                            source_path,
703                            source.as_str(),
704                            &compilation_context,
705                        )?
706                    } else {
707                        compile_module_artifact_from_source_with_context(
708                            source_path,
709                            source.as_str(),
710                            &compilation_context,
711                        )?
712                    };
713                    if let Some(span) = &mut compile_span {
714                        span.mark_compile_succeeded();
715                    }
716                    drop(compile_span);
717                    if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
718                        if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
719                            eprintln!(
720                                "[harn] module cache write skipped for {}: {err}",
721                                source_path.display()
722                            );
723                        }
724                    }
725                    compiled
726                }
727            };
728            let prepared = {
729                let _load_span = recorder.map(ModulePhaseRecorder::load_span);
730                Arc::new(PreparedModuleArtifact::from_cached(cached))
731            };
732            Ok(prepared)
733        })
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
741    use crate::module_source::ModuleSource;
742    use harn_parser::TypeExpr;
743    use std::sync::Barrier;
744
745    fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
746        match type_expr {
747            Some(TypeExpr::List(inner)) => match inner.as_ref() {
748                TypeExpr::Named(name) => name,
749                other => panic!("expected named list element, got {other:?}"),
750            },
751            other => panic!("expected list parameter type, got {other:?}"),
752        }
753    }
754
755    fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
756        Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
757            provenance,
758            imports: Vec::new(),
759            type_schema_init_chunks: Vec::new(),
760            init_chunk: None,
761            functions: BTreeMap::new(),
762            public_exports: BTreeMap::new(),
763            public_value_names: Default::default(),
764            public_type_names: Default::default(),
765        }))
766    }
767
768    fn empty_artifact() -> Arc<PreparedModuleArtifact> {
769        empty_artifact_with_provenance(ModuleProvenance::User)
770    }
771
772    #[test]
773    fn repeated_preparation_derives_one_modules_interface_once() {
774        // Every VM that imports a module asks this cache for it, and the
775        // interface is needed to form the key it asks with. Deriving one costs
776        // a full lex and parse, so re-deriving it per VM put the cache's own
777        // cost back in front of every hit it served.
778        let dir = tempfile::tempdir().expect("temp module dir");
779        let module = dir.path().join("library.harn");
780        std::fs::write(&module, "pub fn value() { return 1 }\n").expect("write module");
781        let source = crate::module_source::read(&module).expect("read module");
782        let canonical = harn_modules::canonical_path(&module);
783        let cache = PreparedModuleCache::default();
784        let validation = PreparedModuleValidation::default();
785
786        let resolutions = |prepare: &dyn Fn()| {
787            let before = crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get);
788            prepare();
789            crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get) - before
790        };
791        let prepare = || {
792            cache
793                .prepare(
794                    &module,
795                    &canonical,
796                    &source,
797                    None,
798                    None,
799                    ModuleProvenance::User,
800                    &validation,
801                )
802                .expect("module prepares");
803        };
804
805        // The first caller has nothing to reuse. This arm is the counter's
806        // positive control: without it, a seam that never increments would
807        // satisfy the assertion below vacuously.
808        assert_eq!(
809            resolutions(&prepare),
810            1,
811            "the first preparation of a module must derive its interface"
812        );
813        assert_eq!(
814            resolutions(&prepare),
815            0,
816            "the same bytes must not be re-parsed to re-derive the same interface"
817        );
818    }
819
820    #[test]
821    fn fresh_run_revalidates_a_remembered_interface_dependency() {
822        let dir = tempfile::tempdir().expect("temp module dir");
823        let dependency = dir.path().join("dep.harn");
824        std::fs::write(&dependency, "pub enum Color { Ready(string) }\n")
825            .expect("write enum dependency");
826        let module = dir.path().join("library.harn");
827        std::fs::write(
828            &module,
829            r#"import "./dep"
830pub fn exercise(value: any) -> string {
831  match value {
832    Color.Ready(message) -> { return message }
833    _ -> { return "fallback" }
834  }
835}
836"#,
837        )
838        .expect("write dependent module");
839
840        let source = crate::module_source::read(&module).expect("read dependent module");
841        let canonical = harn_modules::canonical_path(&module);
842        let cache = PreparedModuleCache::default();
843        let first = cache
844            .prepare(
845                &module,
846                &canonical,
847                &source,
848                None,
849                None,
850                ModuleProvenance::User,
851                &PreparedModuleValidation::default(),
852            )
853            .expect("prepare with imported enum");
854
855        std::fs::write(&dependency, "pub fn replacement() { return 1 }\n")
856            .expect("replace dependency interface");
857
858        let second = cache
859            .prepare(
860                &module,
861                &canonical,
862                &source,
863                None,
864                None,
865                ModuleProvenance::User,
866                &PreparedModuleValidation::default(),
867            )
868            .expect("prepare after dependency edit");
869
870        assert!(
871            !Arc::ptr_eq(&first, &second),
872            "a fresh run must not reuse bytecode lowered against the old interface"
873        );
874        assert_ne!(
875            postcard::to_allocvec(&first.functions["exercise"].freeze_for_cache())
876                .expect("serialize first function"),
877            postcard::to_allocvec(&second.functions["exercise"].freeze_for_cache())
878                .expect("serialize second function"),
879            "the dependency edit must reach the context-sensitive bytecode"
880        );
881    }
882
883    #[test]
884    fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
885        let cache = PreparedModuleCache::default();
886        let source = ModuleSource::from_text("const value = 1");
887        let _ = cache.insert(
888            PathBuf::from("same.harn"),
889            source.sha256(),
890            empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
891        );
892        assert!(
893            cache
894                .get(
895                    Path::new("same.harn"),
896                    source.sha256(),
897                    ModuleProvenance::User,
898                )
899                .is_none(),
900            "user module lookup must be provenance-separated"
901        );
902        assert!(cache
903            .get(
904                Path::new("same.harn"),
905                source.sha256(),
906                ModuleProvenance::PrivilegedWire,
907            )
908            .is_some());
909    }
910
911    #[test]
912    fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
913        let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
914        let first_source = ModuleSource::from_text("pub fn first() { 1 }");
915        let second_source = ModuleSource::from_text("pub fn second() { 2 }");
916        let first = empty_artifact();
917        let first_weak = Arc::downgrade(&first);
918        drop(cache.insert(
919            PathBuf::from("first.harn"),
920            first_source.sha256(),
921            Arc::clone(&first),
922        ));
923        drop(first);
924
925        // quick_cache's scan-resistant admission deliberately preserves the
926        // resident hot key when a new key appears only once at capacity.
927        let scanned = empty_artifact();
928        let scanned_weak = Arc::downgrade(&scanned);
929        drop(cache.insert(
930            PathBuf::from("second.harn"),
931            second_source.sha256(),
932            Arc::clone(&scanned),
933        ));
934        drop(scanned);
935
936        assert!(cache
937            .get(
938                Path::new("first.harn"),
939                first_source.sha256(),
940                ModuleProvenance::User,
941            )
942            .is_some());
943        assert!(cache
944            .get(
945                Path::new("second.harn"),
946                second_source.sha256(),
947                ModuleProvenance::User,
948            )
949            .is_none());
950        assert!(first_weak.upgrade().is_some());
951        assert!(scanned_weak.upgrade().is_none());
952        assert_eq!(cache.stats().insertions, 2);
953        assert_eq!(cache.stats().evictions, 1);
954        assert_eq!(cache.stats().entries, 1);
955
956        drop(cache);
957        assert!(first_weak.upgrade().is_none());
958    }
959
960    #[test]
961    fn cache_key_separates_compiler_configuration() {
962        let path = PathBuf::from("module.harn");
963        let key = PreparedModuleCacheKey::new(
964            path,
965            ModuleSource::from_text("pub fn value() { 1 }").sha256(),
966            ModuleProvenance::User,
967        );
968        let mut other_compiler = key.clone();
969        other_compiler.optimizations_enabled = !key.optimizations_enabled;
970
971        assert_ne!(key, other_compiler);
972    }
973
974    #[test]
975    fn cache_counters_saturate_instead_of_wrapping() {
976        let counter = AtomicU64::new(u64::MAX);
977        saturating_increment(&counter);
978        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
979    }
980
981    #[test]
982    fn cache_key_separates_imported_symbol_compilation_context() {
983        let source_path = PathBuf::from("context-sensitive.harn");
984        let source = ModuleSource::from_text(
985            r#"
986import "./library"
987
988pub fn exercise(value: any) -> string {
989  match value {
990    Color.Ready(message) -> { return message }
991    _ -> { return "fallback" }
992  }
993}
994"#,
995        );
996        let without_imported_enum = compile_module_artifact_from_source_with_context(
997            &source_path,
998            source.as_str(),
999            &ModuleCompilationContext::default(),
1000        )
1001        .expect("compile dynamically-resolved pattern");
1002        let imported_enum_context =
1003            ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
1004        let with_imported_enum = compile_module_artifact_from_source_with_context(
1005            &source_path,
1006            source.as_str(),
1007            &imported_enum_context,
1008        )
1009        .expect("compile imported-enum-resolved pattern");
1010        assert_ne!(
1011            postcard::to_allocvec(&without_imported_enum.functions["exercise"])
1012                .expect("serialize dynamically-resolved function"),
1013            postcard::to_allocvec(&with_imported_enum.functions["exercise"])
1014                .expect("serialize imported-enum-resolved function"),
1015            "the imported enum projection must demonstrably alter bytecode"
1016        );
1017
1018        let cache = PreparedModuleCache::default();
1019        let validation = PreparedModuleValidation::default();
1020        let without_imported_enum = cache
1021            .prepare(
1022                &source_path,
1023                &source_path,
1024                &source,
1025                Some(&ModuleCompilationContext::default()),
1026                None,
1027                ModuleProvenance::User,
1028                &validation,
1029            )
1030            .expect("prepare dynamically-resolved artifact");
1031        let with_imported_enum = cache
1032            .prepare(
1033                &source_path,
1034                &source_path,
1035                &source,
1036                Some(&imported_enum_context),
1037                None,
1038                ModuleProvenance::User,
1039                &validation,
1040            )
1041            .expect("prepare imported-enum-resolved artifact");
1042
1043        assert!(
1044            !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
1045            "one source/path/provenance with distinct imported projections must not alias"
1046        );
1047        assert_ne!(
1048            postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
1049                .expect("serialize cached dynamically-resolved function"),
1050            postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
1051                .expect("serialize cached imported-enum-resolved function")
1052        );
1053        assert_eq!(cache.stats().insertions, 2);
1054    }
1055
1056    #[test]
1057    fn dropping_last_cache_handle_releases_prepared_artifacts() {
1058        let cache = PreparedModuleCache::default();
1059        let path = PathBuf::from("module.harn");
1060        let source = ModuleSource::from_text("pub fn value() { 1 }");
1061        let artifact = empty_artifact();
1062        let weak = Arc::downgrade(&artifact);
1063        let _ = cache.insert(path, source.sha256(), artifact);
1064        let clone = cache.clone();
1065
1066        drop(cache);
1067        assert!(weak.upgrade().is_some());
1068        drop(clone);
1069        assert!(weak.upgrade().is_none());
1070    }
1071
1072    #[test]
1073    fn concurrent_identical_misses_compile_one_immutable_artifact() {
1074        const WORKERS: usize = 8;
1075
1076        let cache = PreparedModuleCache::default();
1077        // The nonce makes this source content nobody has compiled before, so
1078        // the shared disk cache cannot serve it and every worker genuinely
1079        // races to compile. Without it the test asserts single-flight against
1080        // a key an earlier run already stored: it passes cold and reads zero
1081        // compilations warm. Trusted modules used to skip the disk cache
1082        // entirely, which hid this by making the test hermetic by accident.
1083        //
1084        // What this needs is uniqueness, not time, so it takes the randomness
1085        // `tempfile` already uses to name a directory no other process holds.
1086        // A clock would be a flaky-test pattern, and a pid plus a counter can
1087        // repeat once the OS recycles that pid against a cache that outlives
1088        // the run.
1089        let nonce_dir = tempfile::tempdir().expect("temp dir for a unique module identity");
1090        let nonce = nonce_dir
1091            .path()
1092            .file_name()
1093            .expect("temp dir has a final component")
1094            .to_string_lossy()
1095            .into_owned();
1096        let source = Arc::new(ModuleSource::from_text(
1097            std::iter::once(format!("// {nonce}\n"))
1098                .chain(
1099                    (0..128).map(|index| format!("pub fn value_{index}() {{ return {index} }}\n")),
1100                )
1101                .collect::<String>(),
1102        ));
1103        let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
1104        let validation = PreparedModuleValidation::default();
1105        let start = Arc::new(Barrier::new(WORKERS + 1));
1106        let mut handles = Vec::with_capacity(WORKERS);
1107
1108        for _ in 0..WORKERS {
1109            let cache = cache.clone();
1110            let source = Arc::clone(&source);
1111            let source_path = Arc::clone(&source_path);
1112            let validation = validation.clone();
1113            let start = Arc::clone(&start);
1114            handles.push(std::thread::spawn(move || {
1115                let recorder = ModulePhaseRecorder::new();
1116                start.wait();
1117                let artifact = cache
1118                    .prepare(
1119                        &source_path,
1120                        &source_path,
1121                        &source,
1122                        None,
1123                        Some(&recorder),
1124                        ModuleProvenance::TrustedHostDispatch,
1125                        &validation,
1126                    )
1127                    .expect("compile shared immutable module");
1128                (artifact, recorder.snapshot())
1129            }));
1130        }
1131
1132        start.wait();
1133        let outcomes = handles
1134            .into_iter()
1135            .map(|handle| handle.join().expect("module compiler worker joins"))
1136            .collect::<Vec<_>>();
1137        let first = &outcomes[0].0;
1138
1139        assert!(
1140            outcomes
1141                .iter()
1142                .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
1143            "all workers must consume the same immutable prepared artifact"
1144        );
1145        assert_eq!(
1146            outcomes
1147                .iter()
1148                .map(|(_, phases)| phases.modules_compiled)
1149                .sum::<u64>(),
1150            1,
1151            "one exact cache key must have one compilation owner regardless of worker count"
1152        );
1153        assert_eq!(cache.stats().insertions, 1);
1154    }
1155
1156    #[test]
1157    fn failed_preparation_is_not_cached_or_poisoned() {
1158        let cache = PreparedModuleCache::default();
1159        let key = PreparedModuleCacheKey::new(
1160            PathBuf::from("recoverable.harn"),
1161            ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
1162            ModuleProvenance::TrustedHostDispatch,
1163        );
1164
1165        let failed = cache.prepare_exact_key(&key, None, || {
1166            Err(VmError::Runtime(
1167                "synthetic compilation failure".to_string(),
1168            ))
1169        });
1170        assert!(
1171            matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
1172        );
1173        assert_eq!(cache.stats().entries, 0);
1174        assert_eq!(cache.stats().insertions, 0);
1175
1176        let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
1177        let prepared = cache
1178            .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
1179            .expect("a failed owner must release the exact-key preparation slot");
1180
1181        assert!(Arc::ptr_eq(&prepared, &expected));
1182        assert_eq!(cache.stats().misses, 2);
1183        assert_eq!(cache.stats().insertions, 1);
1184        assert_eq!(cache.stats().entries, 1);
1185    }
1186
1187    #[test]
1188    fn hydration_moves_module_owned_storage() {
1189        let source = r#"
1190import { assert_eq } from "std/testing"
1191pub type Result = {value: int}
1192pub const value = 1
1193pub fn answer(items: list<string>) {
1194  fn nested() { return 42 }
1195  return items
1196}
1197"#;
1198        let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
1199            .expect("compile typed module artifact");
1200
1201        let imports = artifact.imports.as_ptr();
1202        let import_path = artifact.imports[0].path.as_ptr();
1203        let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
1204            panic!("expected selective import");
1205        };
1206        let selected_names = selected.as_ptr();
1207        let selected_name = selected[0].as_ptr();
1208        let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
1209        let schema_init_codes = artifact
1210            .type_schema_init_chunks
1211            .iter()
1212            .map(|chunk| chunk.code.as_ptr())
1213            .collect::<Vec<_>>();
1214        let (function_key, function) = artifact.functions.first_key_value().unwrap();
1215        let function_key = function_key.as_ptr();
1216        let function_name = function.name.clone();
1217        let function_code = function.chunk.code.as_ptr();
1218        let param_name = function.params[0].name.as_ptr();
1219        let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
1220        let nested_name = function.chunk.functions[0].name.clone();
1221        let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
1222        let public_export_name = artifact
1223            .public_exports
1224            .get_key_value("answer")
1225            .unwrap()
1226            .0
1227            .as_ptr();
1228        let public_export_kind = *artifact.public_exports.get("answer").unwrap();
1229        let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
1230        let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
1231        let hydrated = PreparedModuleArtifact::from_cached(artifact);
1232
1233        assert_eq!(hydrated.imports.as_ptr(), imports);
1234        assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
1235        let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
1236            panic!("expected selective import");
1237        };
1238        assert_eq!(selected.as_ptr(), selected_names);
1239        assert_eq!(selected[0].as_ptr(), selected_name);
1240        assert_eq!(
1241            hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
1242            init_code
1243        );
1244        assert_eq!(
1245            hydrated
1246                .type_schema_init_chunks
1247                .iter()
1248                .map(|chunk| chunk.code.as_ptr())
1249                .collect::<Vec<_>>(),
1250            schema_init_codes
1251        );
1252        let (hydrated_function_key, hydrated_function) =
1253            hydrated.functions.first_key_value().unwrap();
1254        assert_eq!(hydrated_function_key.as_ptr(), function_key);
1255        // Function names convert into a shared `HarnStr` at hydration (one
1256        // short copy) so per-call consumers can share them; compare by value.
1257        assert_eq!(hydrated_function.name.as_str(), function_name);
1258        assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
1259        assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
1260        assert_eq!(
1261            named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
1262            param_type_name
1263        );
1264        assert_eq!(
1265            hydrated_function.chunk.functions[0].name.as_str(),
1266            nested_name
1267        );
1268        assert_eq!(
1269            hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
1270            nested_code
1271        );
1272        assert_eq!(
1273            hydrated
1274                .public_exports
1275                .get_key_value("answer")
1276                .unwrap()
1277                .0
1278                .as_ptr(),
1279            public_export_name
1280        );
1281        assert_eq!(
1282            hydrated.public_exports.get("answer"),
1283            Some(&public_export_kind)
1284        );
1285        assert_eq!(
1286            hydrated.public_value_names.get("value").unwrap().as_ptr(),
1287            public_value_name
1288        );
1289        assert_eq!(
1290            hydrated.public_type_names.get("Result").unwrap().as_ptr(),
1291            public_type_name
1292        );
1293    }
1294}