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::module_artifact::{
19    compile_module_artifact_from_source_with_context,
20    compile_trusted_host_dispatch_module_artifact_from_source_with_context,
21    module_compilation_context_for_source, ModuleArtifact, ModuleCompilationContext,
22    ModuleImportSpec, ModuleProvenance,
23};
24use crate::module_source::ModuleSource;
25use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
26const DEFAULT_MAX_ENTRIES: usize = 512;
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, ModuleCompilationContext>>>,
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
216impl Default for PreparedModuleCache {
217    fn default() -> Self {
218        Self::with_capacity(
219            NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
220        )
221    }
222}
223
224impl PreparedModuleCache {
225    pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
226        let counters = Arc::new(PreparedModuleCacheCounters::default());
227        let lifecycle = PreparedModuleCacheLifecycle {
228            counters: Arc::clone(&counters),
229        };
230        let capacity = max_entries.get();
231        Self {
232            entries: Arc::new(Cache::with(
233                capacity,
234                capacity as u64,
235                UnitWeighter,
236                DefaultHashBuilder::default(),
237                lifecycle,
238            )),
239            counters,
240            interfaces: Arc::new(Mutex::new(HashMap::new())),
241        }
242    }
243
244    fn remembered_interface(&self, key: &InterfaceMemoKey) -> Option<ModuleCompilationContext> {
245        self.interfaces
246            .lock()
247            .expect("interface memo lock poisoned")
248            .get(key)
249            .cloned()
250    }
251
252    fn remember_interface(&self, key: InterfaceMemoKey, context: &ModuleCompilationContext) {
253        let mut interfaces = self
254            .interfaces
255            .lock()
256            .expect("interface memo lock poisoned");
257        // A handle held across many generations of an edited tree would
258        // otherwise accumulate one entry per version of every module ever
259        // prepared. Start over rather than grow without bound: the entries are
260        // derivable, so the cost of dropping them is bounded by re-deriving the
261        // ones still in use. The bound is far above the module count of a real
262        // tree, so an ordinary run never reaches it.
263        if interfaces.len() >= MAX_REMEMBERED_INTERFACES {
264            interfaces.clear();
265        }
266        interfaces.insert(key, context.clone());
267    }
268
269    pub fn stats(&self) -> PreparedModuleCacheStats {
270        PreparedModuleCacheStats {
271            hits: self.counters.hits.load(Ordering::Relaxed),
272            misses: self.counters.misses.load(Ordering::Relaxed),
273            insertions: self.counters.insertions.load(Ordering::Relaxed),
274            evictions: self.counters.evictions.load(Ordering::Relaxed),
275            entries: self.entries.len(),
276        }
277    }
278
279    /// Prepare every import reachable from `roots` without instantiating or
280    /// executing module state.
281    ///
282    /// Root files themselves are entry programs, not runtime imports, so only
283    /// their transitive import closure is prepared. Invalid modules are left
284    /// uncached for the canonical VM load to diagnose.
285    pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
286        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
287    }
288
289    /// Prepare a Rust-embedder-selected host-dispatch graph without making its
290    /// bytecode visible to ordinary user imports. The in-memory cache key
291    /// retains provenance, and fresh VMs still instantiate independent module
292    /// state from the immutable artifacts.
293    pub fn prepare_trusted_host_dispatch_import_graph(
294        &self,
295        roots: &[PathBuf],
296    ) -> ModulePhaseStats {
297        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch)
298    }
299
300    fn prepare_import_graph_with_provenance(
301        &self,
302        roots: &[PathBuf],
303        provenance: ModuleProvenance,
304    ) -> ModulePhaseStats {
305        if roots.is_empty() {
306            return ModulePhaseStats::default();
307        }
308
309        // This walk reads every reachable file, so the interfaces it derives
310        // supersede anything remembered from an earlier generation of the same
311        // tree.
312        self.interfaces
313            .lock()
314            .expect("interface memo lock poisoned")
315            .clear();
316        let graph = harn_modules::build(roots);
317        let root_paths = roots
318            .iter()
319            .map(|path| harn_modules::canonical_path(path))
320            .collect::<std::collections::HashSet<_>>();
321        let recorder = ModulePhaseRecorder::new();
322
323        for path in graph.module_paths() {
324            if root_paths.contains(&harn_modules::canonical_path(&path)) {
325                continue;
326            }
327            if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
328                let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
329                continue;
330            }
331
332            let source = {
333                let _load_span = recorder.load_span();
334                match crate::module_source::read(&path) {
335                    Ok(source) => source,
336                    Err(_) => continue,
337                }
338            };
339            let Ok(compilation_context) =
340                ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
341            else {
342                continue;
343            };
344            let canonical = harn_modules::canonical_path(&path);
345            let _ = self.prepare(
346                &path,
347                &canonical,
348                &source,
349                Some(&compilation_context),
350                Some(&recorder),
351                provenance,
352            );
353        }
354
355        recorder.snapshot()
356    }
357
358    #[cfg(test)]
359    pub(crate) fn get(
360        &self,
361        canonical_path: &Path,
362        source_hash: [u8; 32],
363        provenance: ModuleProvenance,
364    ) -> Option<Arc<PreparedModuleArtifact>> {
365        self.get_with_context(
366            canonical_path,
367            source_hash,
368            provenance,
369            &ModuleCompilationContext::default(),
370        )
371    }
372
373    pub(crate) fn get_with_context(
374        &self,
375        canonical_path: &Path,
376        source_hash: [u8; 32],
377        provenance: ModuleProvenance,
378        compilation_context: &ModuleCompilationContext,
379    ) -> Option<Arc<PreparedModuleArtifact>> {
380        let key = PreparedModuleCacheKey::with_context(
381            canonical_path.to_path_buf(),
382            source_hash,
383            provenance,
384            compilation_context,
385        );
386        let artifact = self.entries.get(&key);
387        if artifact.is_some() {
388            saturating_increment(&self.counters.hits);
389        } else {
390            saturating_increment(&self.counters.misses);
391        }
392        artifact
393    }
394
395    #[cfg(test)]
396    pub(crate) fn insert(
397        &self,
398        canonical_path: PathBuf,
399        source_hash: [u8; 32],
400        artifact: Arc<PreparedModuleArtifact>,
401    ) -> Arc<PreparedModuleArtifact> {
402        self.insert_with_context(
403            canonical_path,
404            source_hash,
405            &ModuleCompilationContext::default(),
406            artifact,
407        )
408    }
409
410    pub(crate) fn insert_with_context(
411        &self,
412        canonical_path: PathBuf,
413        source_hash: [u8; 32],
414        compilation_context: &ModuleCompilationContext,
415        artifact: Arc<PreparedModuleArtifact>,
416    ) -> Arc<PreparedModuleArtifact> {
417        let key = PreparedModuleCacheKey::with_context(
418            canonical_path,
419            source_hash,
420            artifact.provenance,
421            compilation_context,
422        );
423        match self.entries.get_value_or_guard(&key, None) {
424            GuardResult::Value(existing) => existing,
425            GuardResult::Guard(guard) => {
426                if guard.insert(Arc::clone(&artifact)).is_ok() {
427                    saturating_increment(&self.counters.insertions);
428                }
429                artifact
430            }
431            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
432        }
433    }
434
435    fn prepare_exact_key(
436        &self,
437        key: &PreparedModuleCacheKey,
438        recorder: Option<&ModulePhaseRecorder>,
439        prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
440    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
441        let prepared = {
442            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
443            self.entries.get(key)
444        };
445        if let Some(prepared) = prepared {
446            saturating_increment(&self.counters.hits);
447            return Ok(prepared);
448        }
449        saturating_increment(&self.counters.misses);
450
451        let guarded = {
452            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
453            self.entries.get_value_or_guard(key, None)
454        };
455        match guarded {
456            GuardResult::Value(prepared) => Ok(prepared),
457            GuardResult::Guard(guard) => {
458                let prepared = prepare()?;
459                if guard.insert(Arc::clone(&prepared)).is_ok() {
460                    saturating_increment(&self.counters.insertions);
461                }
462                Ok(prepared)
463            }
464            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
465        }
466    }
467
468    pub(crate) fn prepare(
469        &self,
470        source_path: &Path,
471        canonical_path: &Path,
472        source: &ModuleSource,
473        compilation_context: Option<&ModuleCompilationContext>,
474        recorder: Option<&ModulePhaseRecorder>,
475        provenance: ModuleProvenance,
476    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
477        let source_hash = {
478            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
479            source.sha256()
480        };
481        let memo_key = InterfaceMemoKey {
482            canonical_path: canonical_path.to_path_buf(),
483            source_hash,
484            provenance,
485        };
486        let compilation_context = match compilation_context {
487            Some(context) => {
488                self.remember_interface(memo_key, context);
489                context.clone()
490            }
491            None => match self.remembered_interface(&memo_key) {
492                Some(context) => context,
493                None => {
494                    let context =
495                        module_compilation_context_for_source(source_path, source.as_str())?;
496                    self.remember_interface(memo_key, &context);
497                    context
498                }
499            },
500        };
501        let key = PreparedModuleCacheKey::with_context(
502            canonical_path.to_path_buf(),
503            source_hash,
504            provenance,
505            &compilation_context,
506        );
507        self.prepare_exact_key(&key, recorder, || {
508            // Disk cache hits skip parse + compile. The scoped prepared cache
509            // additionally skips deserialization and chunk hydration on later
510            // fresh VMs without sharing any runtime module state.
511            // Every provenance shares one cache path. The cache key carries the
512            // authority, so the on-disk identity already separates a trusted
513            // artifact from an ordinary one: they hash to different shared-cache
514            // filenames, and an adjacent artifact found by path fails the other
515            // authority's header check. Before the key had that field, the only
516            // thing keeping privileged bytecode out of an ordinary reader's
517            // reach was this branch skipping the cache entirely, which also
518            // meant a trusted graph recompiled from source on every process.
519            let cached = {
520                let lookup = {
521                    let _load_span = recorder.map(ModulePhaseRecorder::load_span);
522                    crate::bytecode_cache::load_module(
523                        source_path,
524                        source,
525                        &compilation_context,
526                        provenance,
527                    )
528                };
529                if let Some(artifact) = lookup.artifact {
530                    artifact
531                } else {
532                    let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
533                    // Same `provenance` that keyed the lookup above, so the
534                    // artifact stored on a miss can only be found by a reader
535                    // asking for the authority it was compiled under.
536                    let compiled = if provenance == ModuleProvenance::TrustedHostDispatch {
537                        compile_trusted_host_dispatch_module_artifact_from_source_with_context(
538                            source_path,
539                            source.as_str(),
540                            &compilation_context,
541                        )?
542                    } else {
543                        compile_module_artifact_from_source_with_context(
544                            source_path,
545                            source.as_str(),
546                            &compilation_context,
547                        )?
548                    };
549                    if let Some(span) = &mut compile_span {
550                        span.mark_compile_succeeded();
551                    }
552                    drop(compile_span);
553                    if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
554                        if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
555                            eprintln!(
556                                "[harn] module cache write skipped for {}: {err}",
557                                source_path.display()
558                            );
559                        }
560                    }
561                    compiled
562                }
563            };
564            let prepared = {
565                let _load_span = recorder.map(ModulePhaseRecorder::load_span);
566                Arc::new(PreparedModuleArtifact::from_cached(cached))
567            };
568            Ok(prepared)
569        })
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
577    use crate::module_source::ModuleSource;
578    use harn_parser::TypeExpr;
579    use std::sync::Barrier;
580
581    fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
582        match type_expr {
583            Some(TypeExpr::List(inner)) => match inner.as_ref() {
584                TypeExpr::Named(name) => name,
585                other => panic!("expected named list element, got {other:?}"),
586            },
587            other => panic!("expected list parameter type, got {other:?}"),
588        }
589    }
590
591    fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
592        Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
593            provenance,
594            imports: Vec::new(),
595            type_schema_init_chunks: Vec::new(),
596            init_chunk: None,
597            functions: BTreeMap::new(),
598            public_exports: BTreeMap::new(),
599            public_value_names: Default::default(),
600            public_type_names: Default::default(),
601        }))
602    }
603
604    fn empty_artifact() -> Arc<PreparedModuleArtifact> {
605        empty_artifact_with_provenance(ModuleProvenance::User)
606    }
607
608    #[test]
609    fn repeated_preparation_derives_one_modules_interface_once() {
610        // Every VM that imports a module asks this cache for it, and the
611        // interface is needed to form the key it asks with. Deriving one costs
612        // a full lex and parse, so re-deriving it per VM put the cache's own
613        // cost back in front of every hit it served.
614        let dir = tempfile::tempdir().expect("temp module dir");
615        let module = dir.path().join("library.harn");
616        std::fs::write(&module, "pub fn value() { return 1 }\n").expect("write module");
617        let source = crate::module_source::read(&module).expect("read module");
618        let canonical = harn_modules::canonical_path(&module);
619        let cache = PreparedModuleCache::default();
620
621        let resolutions = |prepare: &dyn Fn()| {
622            let before = crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get);
623            prepare();
624            crate::module_artifact::INTERFACE_RESOLUTIONS.with(std::cell::Cell::get) - before
625        };
626        let prepare = || {
627            cache
628                .prepare(
629                    &module,
630                    &canonical,
631                    &source,
632                    None,
633                    None,
634                    ModuleProvenance::User,
635                )
636                .expect("module prepares");
637        };
638
639        // The first caller has nothing to reuse. This arm is the counter's
640        // positive control: without it, a seam that never increments would
641        // satisfy the assertion below vacuously.
642        assert_eq!(
643            resolutions(&prepare),
644            1,
645            "the first preparation of a module must derive its interface"
646        );
647        assert_eq!(
648            resolutions(&prepare),
649            0,
650            "the same bytes must not be re-parsed to re-derive the same interface"
651        );
652    }
653
654    #[test]
655    fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
656        let cache = PreparedModuleCache::default();
657        let source = ModuleSource::from_text("const value = 1");
658        let _ = cache.insert(
659            PathBuf::from("same.harn"),
660            source.sha256(),
661            empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
662        );
663        assert!(
664            cache
665                .get(
666                    Path::new("same.harn"),
667                    source.sha256(),
668                    ModuleProvenance::User,
669                )
670                .is_none(),
671            "user module lookup must be provenance-separated"
672        );
673        assert!(cache
674            .get(
675                Path::new("same.harn"),
676                source.sha256(),
677                ModuleProvenance::PrivilegedWire,
678            )
679            .is_some());
680    }
681
682    #[test]
683    fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
684        let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
685        let first_source = ModuleSource::from_text("pub fn first() { 1 }");
686        let second_source = ModuleSource::from_text("pub fn second() { 2 }");
687        let first = empty_artifact();
688        let first_weak = Arc::downgrade(&first);
689        drop(cache.insert(
690            PathBuf::from("first.harn"),
691            first_source.sha256(),
692            Arc::clone(&first),
693        ));
694        drop(first);
695
696        // quick_cache's scan-resistant admission deliberately preserves the
697        // resident hot key when a new key appears only once at capacity.
698        let scanned = empty_artifact();
699        let scanned_weak = Arc::downgrade(&scanned);
700        drop(cache.insert(
701            PathBuf::from("second.harn"),
702            second_source.sha256(),
703            Arc::clone(&scanned),
704        ));
705        drop(scanned);
706
707        assert!(cache
708            .get(
709                Path::new("first.harn"),
710                first_source.sha256(),
711                ModuleProvenance::User,
712            )
713            .is_some());
714        assert!(cache
715            .get(
716                Path::new("second.harn"),
717                second_source.sha256(),
718                ModuleProvenance::User,
719            )
720            .is_none());
721        assert!(first_weak.upgrade().is_some());
722        assert!(scanned_weak.upgrade().is_none());
723        assert_eq!(cache.stats().insertions, 2);
724        assert_eq!(cache.stats().evictions, 1);
725        assert_eq!(cache.stats().entries, 1);
726
727        drop(cache);
728        assert!(first_weak.upgrade().is_none());
729    }
730
731    #[test]
732    fn cache_key_separates_compiler_configuration() {
733        let path = PathBuf::from("module.harn");
734        let key = PreparedModuleCacheKey::new(
735            path,
736            ModuleSource::from_text("pub fn value() { 1 }").sha256(),
737            ModuleProvenance::User,
738        );
739        let mut other_compiler = key.clone();
740        other_compiler.optimizations_enabled = !key.optimizations_enabled;
741
742        assert_ne!(key, other_compiler);
743    }
744
745    #[test]
746    fn cache_counters_saturate_instead_of_wrapping() {
747        let counter = AtomicU64::new(u64::MAX);
748        saturating_increment(&counter);
749        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
750    }
751
752    #[test]
753    fn cache_key_separates_imported_symbol_compilation_context() {
754        let source_path = PathBuf::from("context-sensitive.harn");
755        let source = ModuleSource::from_text(
756            r#"
757import "./library"
758
759pub fn exercise(value: any) -> string {
760  match value {
761    Color.Ready(message) -> { return message }
762    _ -> { return "fallback" }
763  }
764}
765"#,
766        );
767        let without_imported_enum = compile_module_artifact_from_source_with_context(
768            &source_path,
769            source.as_str(),
770            &ModuleCompilationContext::default(),
771        )
772        .expect("compile dynamically-resolved pattern");
773        let imported_enum_context =
774            ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
775        let with_imported_enum = compile_module_artifact_from_source_with_context(
776            &source_path,
777            source.as_str(),
778            &imported_enum_context,
779        )
780        .expect("compile imported-enum-resolved pattern");
781        assert_ne!(
782            postcard::to_allocvec(&without_imported_enum.functions["exercise"])
783                .expect("serialize dynamically-resolved function"),
784            postcard::to_allocvec(&with_imported_enum.functions["exercise"])
785                .expect("serialize imported-enum-resolved function"),
786            "the imported enum projection must demonstrably alter bytecode"
787        );
788
789        let cache = PreparedModuleCache::default();
790        let without_imported_enum = cache
791            .prepare(
792                &source_path,
793                &source_path,
794                &source,
795                Some(&ModuleCompilationContext::default()),
796                None,
797                ModuleProvenance::User,
798            )
799            .expect("prepare dynamically-resolved artifact");
800        let with_imported_enum = cache
801            .prepare(
802                &source_path,
803                &source_path,
804                &source,
805                Some(&imported_enum_context),
806                None,
807                ModuleProvenance::User,
808            )
809            .expect("prepare imported-enum-resolved artifact");
810
811        assert!(
812            !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
813            "one source/path/provenance with distinct imported projections must not alias"
814        );
815        assert_ne!(
816            postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
817                .expect("serialize cached dynamically-resolved function"),
818            postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
819                .expect("serialize cached imported-enum-resolved function")
820        );
821        assert_eq!(cache.stats().insertions, 2);
822    }
823
824    #[test]
825    fn dropping_last_cache_handle_releases_prepared_artifacts() {
826        let cache = PreparedModuleCache::default();
827        let path = PathBuf::from("module.harn");
828        let source = ModuleSource::from_text("pub fn value() { 1 }");
829        let artifact = empty_artifact();
830        let weak = Arc::downgrade(&artifact);
831        let _ = cache.insert(path, source.sha256(), artifact);
832        let clone = cache.clone();
833
834        drop(cache);
835        assert!(weak.upgrade().is_some());
836        drop(clone);
837        assert!(weak.upgrade().is_none());
838    }
839
840    #[test]
841    fn concurrent_identical_misses_compile_one_immutable_artifact() {
842        const WORKERS: usize = 8;
843
844        let cache = PreparedModuleCache::default();
845        // The nonce makes this source content nobody has compiled before, so
846        // the shared disk cache cannot serve it and every worker genuinely
847        // races to compile. Without it the test asserts single-flight against
848        // a key an earlier run already stored: it passes cold and reads zero
849        // compilations warm. Trusted modules used to skip the disk cache
850        // entirely, which hid this by making the test hermetic by accident.
851        //
852        // What this needs is uniqueness, not time, so it takes the randomness
853        // `tempfile` already uses to name a directory no other process holds.
854        // A clock would be a flaky-test pattern, and a pid plus a counter can
855        // repeat once the OS recycles that pid against a cache that outlives
856        // the run.
857        let nonce_dir = tempfile::tempdir().expect("temp dir for a unique module identity");
858        let nonce = nonce_dir
859            .path()
860            .file_name()
861            .expect("temp dir has a final component")
862            .to_string_lossy()
863            .into_owned();
864        let source = Arc::new(ModuleSource::from_text(
865            std::iter::once(format!("// {nonce}\n"))
866                .chain(
867                    (0..128).map(|index| format!("pub fn value_{index}() {{ return {index} }}\n")),
868                )
869                .collect::<String>(),
870        ));
871        let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
872        let start = Arc::new(Barrier::new(WORKERS + 1));
873        let mut handles = Vec::with_capacity(WORKERS);
874
875        for _ in 0..WORKERS {
876            let cache = cache.clone();
877            let source = Arc::clone(&source);
878            let source_path = Arc::clone(&source_path);
879            let start = Arc::clone(&start);
880            handles.push(std::thread::spawn(move || {
881                let recorder = ModulePhaseRecorder::new();
882                start.wait();
883                let artifact = cache
884                    .prepare(
885                        &source_path,
886                        &source_path,
887                        &source,
888                        None,
889                        Some(&recorder),
890                        ModuleProvenance::TrustedHostDispatch,
891                    )
892                    .expect("compile shared immutable module");
893                (artifact, recorder.snapshot())
894            }));
895        }
896
897        start.wait();
898        let outcomes = handles
899            .into_iter()
900            .map(|handle| handle.join().expect("module compiler worker joins"))
901            .collect::<Vec<_>>();
902        let first = &outcomes[0].0;
903
904        assert!(
905            outcomes
906                .iter()
907                .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
908            "all workers must consume the same immutable prepared artifact"
909        );
910        assert_eq!(
911            outcomes
912                .iter()
913                .map(|(_, phases)| phases.modules_compiled)
914                .sum::<u64>(),
915            1,
916            "one exact cache key must have one compilation owner regardless of worker count"
917        );
918        assert_eq!(cache.stats().insertions, 1);
919    }
920
921    #[test]
922    fn failed_preparation_is_not_cached_or_poisoned() {
923        let cache = PreparedModuleCache::default();
924        let key = PreparedModuleCacheKey::new(
925            PathBuf::from("recoverable.harn"),
926            ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
927            ModuleProvenance::TrustedHostDispatch,
928        );
929
930        let failed = cache.prepare_exact_key(&key, None, || {
931            Err(VmError::Runtime(
932                "synthetic compilation failure".to_string(),
933            ))
934        });
935        assert!(
936            matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
937        );
938        assert_eq!(cache.stats().entries, 0);
939        assert_eq!(cache.stats().insertions, 0);
940
941        let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
942        let prepared = cache
943            .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
944            .expect("a failed owner must release the exact-key preparation slot");
945
946        assert!(Arc::ptr_eq(&prepared, &expected));
947        assert_eq!(cache.stats().misses, 2);
948        assert_eq!(cache.stats().insertions, 1);
949        assert_eq!(cache.stats().entries, 1);
950    }
951
952    #[test]
953    fn hydration_moves_module_owned_storage() {
954        let source = r#"
955import { assert_eq } from "std/testing"
956pub type Result = {value: int}
957pub const value = 1
958pub fn answer(items: list<string>) {
959  fn nested() { return 42 }
960  return items
961}
962"#;
963        let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
964            .expect("compile typed module artifact");
965
966        let imports = artifact.imports.as_ptr();
967        let import_path = artifact.imports[0].path.as_ptr();
968        let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
969            panic!("expected selective import");
970        };
971        let selected_names = selected.as_ptr();
972        let selected_name = selected[0].as_ptr();
973        let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
974        let schema_init_codes = artifact
975            .type_schema_init_chunks
976            .iter()
977            .map(|chunk| chunk.code.as_ptr())
978            .collect::<Vec<_>>();
979        let (function_key, function) = artifact.functions.first_key_value().unwrap();
980        let function_key = function_key.as_ptr();
981        let function_name = function.name.clone();
982        let function_code = function.chunk.code.as_ptr();
983        let param_name = function.params[0].name.as_ptr();
984        let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
985        let nested_name = function.chunk.functions[0].name.clone();
986        let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
987        let public_export_name = artifact
988            .public_exports
989            .get_key_value("answer")
990            .unwrap()
991            .0
992            .as_ptr();
993        let public_export_kind = *artifact.public_exports.get("answer").unwrap();
994        let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
995        let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
996        let hydrated = PreparedModuleArtifact::from_cached(artifact);
997
998        assert_eq!(hydrated.imports.as_ptr(), imports);
999        assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
1000        let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
1001            panic!("expected selective import");
1002        };
1003        assert_eq!(selected.as_ptr(), selected_names);
1004        assert_eq!(selected[0].as_ptr(), selected_name);
1005        assert_eq!(
1006            hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
1007            init_code
1008        );
1009        assert_eq!(
1010            hydrated
1011                .type_schema_init_chunks
1012                .iter()
1013                .map(|chunk| chunk.code.as_ptr())
1014                .collect::<Vec<_>>(),
1015            schema_init_codes
1016        );
1017        let (hydrated_function_key, hydrated_function) =
1018            hydrated.functions.first_key_value().unwrap();
1019        assert_eq!(hydrated_function_key.as_ptr(), function_key);
1020        // Function names convert into a shared `HarnStr` at hydration (one
1021        // short copy) so per-call consumers can share them; compare by value.
1022        assert_eq!(hydrated_function.name.as_str(), function_name);
1023        assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
1024        assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
1025        assert_eq!(
1026            named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
1027            param_type_name
1028        );
1029        assert_eq!(
1030            hydrated_function.chunk.functions[0].name.as_str(),
1031            nested_name
1032        );
1033        assert_eq!(
1034            hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
1035            nested_code
1036        );
1037        assert_eq!(
1038            hydrated
1039                .public_exports
1040                .get_key_value("answer")
1041                .unwrap()
1042                .0
1043                .as_ptr(),
1044            public_export_name
1045        );
1046        assert_eq!(
1047            hydrated.public_exports.get("answer"),
1048            Some(&public_export_kind)
1049        );
1050        assert_eq!(
1051            hydrated.public_value_names.get("value").unwrap().as_ptr(),
1052            public_value_name
1053        );
1054        assert_eq!(
1055            hydrated.public_type_names.get("Result").unwrap().as_ptr(),
1056            public_type_name
1057        );
1058    }
1059}