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;
8use std::num::NonZeroUsize;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
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
28/// Immutable runtime form of one compiled module artifact.
29pub(crate) struct PreparedModuleArtifact {
30    pub(crate) provenance: ModuleProvenance,
31    pub(crate) imports: Vec<ModuleImportSpec>,
32    pub(crate) type_schema_init_chunks: Vec<Arc<Chunk>>,
33    pub(crate) init_chunk: Option<Arc<Chunk>>,
34    pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
35    pub(crate) public_exports: BTreeMap<String, DefKind>,
36    pub(crate) public_value_names: std::collections::HashSet<String>,
37    pub(crate) public_type_names: std::collections::HashSet<String>,
38}
39
40impl PreparedModuleArtifact {
41    pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
42        let ModuleArtifact {
43            provenance,
44            imports,
45            type_schema_init_chunks,
46            init_chunk,
47            functions,
48            public_exports,
49            public_value_names,
50            public_type_names,
51        } = artifact;
52        let type_schema_init_chunks = type_schema_init_chunks
53            .into_iter()
54            .map(|chunk| Arc::new(Chunk::from_cached(chunk)))
55            .collect();
56        let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
57        let functions = functions
58            .into_iter()
59            .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
60            .collect();
61        Self {
62            provenance,
63            imports,
64            type_schema_init_chunks,
65            init_chunk,
66            functions,
67            public_exports,
68            public_value_names,
69            public_type_names,
70        }
71    }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
75struct PreparedModuleCacheKey {
76    canonical_path: PathBuf,
77    source_hash: [u8; 32],
78    provenance: ModuleProvenance,
79    harn_version: &'static str,
80    codegen_fingerprint: &'static str,
81    optimizations_enabled: bool,
82    compilation_context_digest: [u8; 32],
83}
84
85impl PreparedModuleCacheKey {
86    /// `source_hash` is the same SHA-256 that names the module's on-disk
87    /// artifact. Keying on it rather than a second digest of the same bytes
88    /// means a warm module load hashes its source once, and lets a caller
89    /// holding a recorded digest find a prepared artifact without the bytes.
90    #[cfg(test)]
91    fn new(canonical_path: PathBuf, source_hash: [u8; 32], provenance: ModuleProvenance) -> Self {
92        Self::with_context(
93            canonical_path,
94            source_hash,
95            provenance,
96            &ModuleCompilationContext::default(),
97        )
98    }
99
100    fn with_context(
101        canonical_path: PathBuf,
102        source_hash: [u8; 32],
103        provenance: ModuleProvenance,
104        compilation_context: &ModuleCompilationContext,
105    ) -> Self {
106        Self {
107            canonical_path,
108            source_hash,
109            provenance,
110            harn_version: crate::bytecode_cache::HARN_VERSION,
111            codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
112            optimizations_enabled: crate::compiler::CompilerOptions::from_env()
113                .optimizations_enabled(),
114            compilation_context_digest: compilation_context.digest(),
115        }
116    }
117}
118
119#[derive(Default)]
120struct PreparedModuleCacheCounters {
121    hits: AtomicU64,
122    misses: AtomicU64,
123    insertions: AtomicU64,
124    evictions: AtomicU64,
125}
126
127fn saturating_increment(counter: &AtomicU64) {
128    let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
129        Some(value.saturating_add(1))
130    });
131}
132
133#[derive(Clone)]
134struct PreparedModuleCacheLifecycle {
135    counters: Arc<PreparedModuleCacheCounters>,
136}
137
138impl Lifecycle<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>
139    for PreparedModuleCacheLifecycle
140{
141    type RequestState = ();
142
143    fn on_evict(
144        &self,
145        _state: &mut Self::RequestState,
146        _key: PreparedModuleCacheKey,
147        _artifact: Arc<PreparedModuleArtifact>,
148    ) {
149        saturating_increment(&self.counters.evictions);
150    }
151}
152
153type PreparedArtifactCache = Cache<
154    PreparedModuleCacheKey,
155    Arc<PreparedModuleArtifact>,
156    UnitWeighter,
157    DefaultHashBuilder,
158    PreparedModuleCacheLifecycle,
159>;
160
161/// Typed counters for a [`PreparedModuleCache`] lifetime.
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
163#[non_exhaustive]
164pub struct PreparedModuleCacheStats {
165    pub hits: u64,
166    pub misses: u64,
167    pub insertions: u64,
168    /// Artifacts discarded by the bounded cache, including cold scan
169    /// candidates rejected by S3-FIFO admission before becoming residents.
170    pub evictions: u64,
171    pub entries: usize,
172}
173
174/// A bounded, shareable cache of immutable module bytecode templates.
175///
176/// The handle is explicit so embedders can scope reuse to one test suite,
177/// worker, watch generation, or VM baseline. Dropping the final handle releases
178/// every prepared artifact; historical user source never accumulates globally.
179/// Concurrent misses for one exact key share a single preparation owner, while
180/// unrelated modules remain independently preparable.
181#[derive(Clone)]
182pub struct PreparedModuleCache {
183    entries: Arc<PreparedArtifactCache>,
184    counters: Arc<PreparedModuleCacheCounters>,
185}
186
187impl Default for PreparedModuleCache {
188    fn default() -> Self {
189        Self::with_capacity(
190            NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
191        )
192    }
193}
194
195impl PreparedModuleCache {
196    pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
197        let counters = Arc::new(PreparedModuleCacheCounters::default());
198        let lifecycle = PreparedModuleCacheLifecycle {
199            counters: Arc::clone(&counters),
200        };
201        let capacity = max_entries.get();
202        Self {
203            entries: Arc::new(Cache::with(
204                capacity,
205                capacity as u64,
206                UnitWeighter,
207                DefaultHashBuilder::default(),
208                lifecycle,
209            )),
210            counters,
211        }
212    }
213
214    pub fn stats(&self) -> PreparedModuleCacheStats {
215        PreparedModuleCacheStats {
216            hits: self.counters.hits.load(Ordering::Relaxed),
217            misses: self.counters.misses.load(Ordering::Relaxed),
218            insertions: self.counters.insertions.load(Ordering::Relaxed),
219            evictions: self.counters.evictions.load(Ordering::Relaxed),
220            entries: self.entries.len(),
221        }
222    }
223
224    /// Prepare every import reachable from `roots` without instantiating or
225    /// executing module state.
226    ///
227    /// Root files themselves are entry programs, not runtime imports, so only
228    /// their transitive import closure is prepared. Invalid modules are left
229    /// uncached for the canonical VM load to diagnose.
230    pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
231        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
232    }
233
234    /// Prepare a Rust-embedder-selected host-dispatch graph without making its
235    /// bytecode visible to ordinary user imports. The in-memory cache key
236    /// retains provenance, and fresh VMs still instantiate independent module
237    /// state from the immutable artifacts.
238    pub fn prepare_trusted_host_dispatch_import_graph(
239        &self,
240        roots: &[PathBuf],
241    ) -> ModulePhaseStats {
242        self.prepare_import_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch)
243    }
244
245    fn prepare_import_graph_with_provenance(
246        &self,
247        roots: &[PathBuf],
248        provenance: ModuleProvenance,
249    ) -> ModulePhaseStats {
250        if roots.is_empty() {
251            return ModulePhaseStats::default();
252        }
253
254        let graph = harn_modules::build(roots);
255        let root_paths = roots
256            .iter()
257            .map(|path| harn_modules::canonical_path(path))
258            .collect::<std::collections::HashSet<_>>();
259        let recorder = ModulePhaseRecorder::new();
260
261        for path in graph.module_paths() {
262            if root_paths.contains(&harn_modules::canonical_path(&path)) {
263                continue;
264            }
265            if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
266                let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
267                continue;
268            }
269
270            let source = {
271                let _load_span = recorder.load_span();
272                match crate::module_source::read(&path) {
273                    Ok(source) => source,
274                    Err(_) => continue,
275                }
276            };
277            let Ok(compilation_context) =
278                ModuleCompilationContext::for_source_in_graph(&graph, &path, source.as_str())
279            else {
280                continue;
281            };
282            let canonical = harn_modules::canonical_path(&path);
283            let _ = self.prepare(
284                &path,
285                &canonical,
286                &source,
287                Some(&compilation_context),
288                Some(&recorder),
289                provenance,
290            );
291        }
292
293        recorder.snapshot()
294    }
295
296    #[cfg(test)]
297    pub(crate) fn get(
298        &self,
299        canonical_path: &Path,
300        source_hash: [u8; 32],
301        provenance: ModuleProvenance,
302    ) -> Option<Arc<PreparedModuleArtifact>> {
303        self.get_with_context(
304            canonical_path,
305            source_hash,
306            provenance,
307            &ModuleCompilationContext::default(),
308        )
309    }
310
311    pub(crate) fn get_with_context(
312        &self,
313        canonical_path: &Path,
314        source_hash: [u8; 32],
315        provenance: ModuleProvenance,
316        compilation_context: &ModuleCompilationContext,
317    ) -> Option<Arc<PreparedModuleArtifact>> {
318        let key = PreparedModuleCacheKey::with_context(
319            canonical_path.to_path_buf(),
320            source_hash,
321            provenance,
322            compilation_context,
323        );
324        let artifact = self.entries.get(&key);
325        if artifact.is_some() {
326            saturating_increment(&self.counters.hits);
327        } else {
328            saturating_increment(&self.counters.misses);
329        }
330        artifact
331    }
332
333    #[cfg(test)]
334    pub(crate) fn insert(
335        &self,
336        canonical_path: PathBuf,
337        source_hash: [u8; 32],
338        artifact: Arc<PreparedModuleArtifact>,
339    ) -> Arc<PreparedModuleArtifact> {
340        self.insert_with_context(
341            canonical_path,
342            source_hash,
343            &ModuleCompilationContext::default(),
344            artifact,
345        )
346    }
347
348    pub(crate) fn insert_with_context(
349        &self,
350        canonical_path: PathBuf,
351        source_hash: [u8; 32],
352        compilation_context: &ModuleCompilationContext,
353        artifact: Arc<PreparedModuleArtifact>,
354    ) -> Arc<PreparedModuleArtifact> {
355        let key = PreparedModuleCacheKey::with_context(
356            canonical_path,
357            source_hash,
358            artifact.provenance,
359            compilation_context,
360        );
361        match self.entries.get_value_or_guard(&key, None) {
362            GuardResult::Value(existing) => existing,
363            GuardResult::Guard(guard) => {
364                if guard.insert(Arc::clone(&artifact)).is_ok() {
365                    saturating_increment(&self.counters.insertions);
366                }
367                artifact
368            }
369            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
370        }
371    }
372
373    fn prepare_exact_key(
374        &self,
375        key: &PreparedModuleCacheKey,
376        recorder: Option<&ModulePhaseRecorder>,
377        prepare: impl FnOnce() -> Result<Arc<PreparedModuleArtifact>, VmError>,
378    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
379        let prepared = {
380            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
381            self.entries.get(key)
382        };
383        if let Some(prepared) = prepared {
384            saturating_increment(&self.counters.hits);
385            return Ok(prepared);
386        }
387        saturating_increment(&self.counters.misses);
388
389        let guarded = {
390            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
391            self.entries.get_value_or_guard(key, None)
392        };
393        match guarded {
394            GuardResult::Value(prepared) => Ok(prepared),
395            GuardResult::Guard(guard) => {
396                let prepared = prepare()?;
397                if guard.insert(Arc::clone(&prepared)).is_ok() {
398                    saturating_increment(&self.counters.insertions);
399                }
400                Ok(prepared)
401            }
402            GuardResult::Timeout => unreachable!("an unbounded cache wait cannot time out"),
403        }
404    }
405
406    pub(crate) fn prepare(
407        &self,
408        source_path: &Path,
409        canonical_path: &Path,
410        source: &ModuleSource,
411        compilation_context: Option<&ModuleCompilationContext>,
412        recorder: Option<&ModulePhaseRecorder>,
413        provenance: ModuleProvenance,
414    ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
415        let source_hash = {
416            let _load_span = recorder.map(ModulePhaseRecorder::load_span);
417            source.sha256()
418        };
419        let compilation_context = match compilation_context {
420            Some(context) => context.clone(),
421            None => module_compilation_context_for_source(source_path, source.as_str())?,
422        };
423        let key = PreparedModuleCacheKey::with_context(
424            canonical_path.to_path_buf(),
425            source_hash,
426            provenance,
427            &compilation_context,
428        );
429        self.prepare_exact_key(&key, recorder, || {
430            // Disk cache hits skip parse + compile. The scoped prepared cache
431            // additionally skips deserialization and chunk hydration on later
432            // fresh VMs without sharing any runtime module state.
433            let cached = if provenance == ModuleProvenance::TrustedHostDispatch {
434                let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
435                let compiled =
436                    compile_trusted_host_dispatch_module_artifact_from_source_with_context(
437                        source_path,
438                        source.as_str(),
439                        &compilation_context,
440                    )?;
441                if let Some(span) = &mut compile_span {
442                    span.mark_compile_succeeded();
443                }
444                drop(compile_span);
445                compiled
446            } else {
447                // Only ordinary user bytecode enters the process-wide disk cache.
448                // Trusted host-dispatch artifacts remain in this explicitly scoped,
449                // provenance-keyed in-memory cache.
450                let lookup = {
451                    let _load_span = recorder.map(ModulePhaseRecorder::load_span);
452                    crate::bytecode_cache::load_module(source_path, source, &compilation_context)
453                };
454                if let Some(artifact) = lookup.artifact {
455                    artifact
456                } else {
457                    let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
458                    let compiled = compile_module_artifact_from_source_with_context(
459                        source_path,
460                        source.as_str(),
461                        &compilation_context,
462                    )?;
463                    if let Some(span) = &mut compile_span {
464                        span.mark_compile_succeeded();
465                    }
466                    drop(compile_span);
467                    if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
468                        if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
469                            eprintln!(
470                                "[harn] module cache write skipped for {}: {err}",
471                                source_path.display()
472                            );
473                        }
474                    }
475                    compiled
476                }
477            };
478            let prepared = {
479                let _load_span = recorder.map(ModulePhaseRecorder::load_span);
480                Arc::new(PreparedModuleArtifact::from_cached(cached))
481            };
482            Ok(prepared)
483        })
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
491    use crate::module_source::ModuleSource;
492    use harn_parser::TypeExpr;
493    use std::sync::Barrier;
494
495    fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
496        match type_expr {
497            Some(TypeExpr::List(inner)) => match inner.as_ref() {
498                TypeExpr::Named(name) => name,
499                other => panic!("expected named list element, got {other:?}"),
500            },
501            other => panic!("expected list parameter type, got {other:?}"),
502        }
503    }
504
505    fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
506        Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
507            provenance,
508            imports: Vec::new(),
509            type_schema_init_chunks: Vec::new(),
510            init_chunk: None,
511            functions: BTreeMap::new(),
512            public_exports: BTreeMap::new(),
513            public_value_names: Default::default(),
514            public_type_names: Default::default(),
515        }))
516    }
517
518    fn empty_artifact() -> Arc<PreparedModuleArtifact> {
519        empty_artifact_with_provenance(ModuleProvenance::User)
520    }
521
522    #[test]
523    fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
524        let cache = PreparedModuleCache::default();
525        let source = ModuleSource::from_text("const value = 1");
526        let _ = cache.insert(
527            PathBuf::from("same.harn"),
528            source.sha256(),
529            empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
530        );
531        assert!(
532            cache
533                .get(
534                    Path::new("same.harn"),
535                    source.sha256(),
536                    ModuleProvenance::User,
537                )
538                .is_none(),
539            "user module lookup must be provenance-separated"
540        );
541        assert!(cache
542            .get(
543                Path::new("same.harn"),
544                source.sha256(),
545                ModuleProvenance::PrivilegedWire,
546            )
547            .is_some());
548    }
549
550    #[test]
551    fn bounded_cache_rejects_a_one_off_scan_without_leaking_artifacts() {
552        let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
553        let first_source = ModuleSource::from_text("pub fn first() { 1 }");
554        let second_source = ModuleSource::from_text("pub fn second() { 2 }");
555        let first = empty_artifact();
556        let first_weak = Arc::downgrade(&first);
557        drop(cache.insert(
558            PathBuf::from("first.harn"),
559            first_source.sha256(),
560            Arc::clone(&first),
561        ));
562        drop(first);
563
564        // quick_cache's scan-resistant admission deliberately preserves the
565        // resident hot key when a new key appears only once at capacity.
566        let scanned = empty_artifact();
567        let scanned_weak = Arc::downgrade(&scanned);
568        drop(cache.insert(
569            PathBuf::from("second.harn"),
570            second_source.sha256(),
571            Arc::clone(&scanned),
572        ));
573        drop(scanned);
574
575        assert!(cache
576            .get(
577                Path::new("first.harn"),
578                first_source.sha256(),
579                ModuleProvenance::User,
580            )
581            .is_some());
582        assert!(cache
583            .get(
584                Path::new("second.harn"),
585                second_source.sha256(),
586                ModuleProvenance::User,
587            )
588            .is_none());
589        assert!(first_weak.upgrade().is_some());
590        assert!(scanned_weak.upgrade().is_none());
591        assert_eq!(cache.stats().insertions, 2);
592        assert_eq!(cache.stats().evictions, 1);
593        assert_eq!(cache.stats().entries, 1);
594
595        drop(cache);
596        assert!(first_weak.upgrade().is_none());
597    }
598
599    #[test]
600    fn cache_key_separates_compiler_configuration() {
601        let path = PathBuf::from("module.harn");
602        let key = PreparedModuleCacheKey::new(
603            path,
604            ModuleSource::from_text("pub fn value() { 1 }").sha256(),
605            ModuleProvenance::User,
606        );
607        let mut other_compiler = key.clone();
608        other_compiler.optimizations_enabled = !key.optimizations_enabled;
609
610        assert_ne!(key, other_compiler);
611    }
612
613    #[test]
614    fn cache_counters_saturate_instead_of_wrapping() {
615        let counter = AtomicU64::new(u64::MAX);
616        saturating_increment(&counter);
617        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
618    }
619
620    #[test]
621    fn cache_key_separates_imported_symbol_compilation_context() {
622        let source_path = PathBuf::from("context-sensitive.harn");
623        let source = ModuleSource::from_text(
624            r#"
625import "./library"
626
627pub fn exercise(value: any) -> string {
628  match value {
629    Color.Ready(message) -> { return message }
630    _ -> { return "fallback" }
631  }
632}
633"#,
634        );
635        let without_imported_enum = compile_module_artifact_from_source_with_context(
636            &source_path,
637            source.as_str(),
638            &ModuleCompilationContext::default(),
639        )
640        .expect("compile dynamically-resolved pattern");
641        let imported_enum_context =
642            ModuleCompilationContext::new(["Color".to_string()], Vec::<String>::new());
643        let with_imported_enum = compile_module_artifact_from_source_with_context(
644            &source_path,
645            source.as_str(),
646            &imported_enum_context,
647        )
648        .expect("compile imported-enum-resolved pattern");
649        assert_ne!(
650            postcard::to_allocvec(&without_imported_enum.functions["exercise"])
651                .expect("serialize dynamically-resolved function"),
652            postcard::to_allocvec(&with_imported_enum.functions["exercise"])
653                .expect("serialize imported-enum-resolved function"),
654            "the imported enum projection must demonstrably alter bytecode"
655        );
656
657        let cache = PreparedModuleCache::default();
658        let without_imported_enum = cache
659            .prepare(
660                &source_path,
661                &source_path,
662                &source,
663                Some(&ModuleCompilationContext::default()),
664                None,
665                ModuleProvenance::User,
666            )
667            .expect("prepare dynamically-resolved artifact");
668        let with_imported_enum = cache
669            .prepare(
670                &source_path,
671                &source_path,
672                &source,
673                Some(&imported_enum_context),
674                None,
675                ModuleProvenance::User,
676            )
677            .expect("prepare imported-enum-resolved artifact");
678
679        assert!(
680            !Arc::ptr_eq(&without_imported_enum, &with_imported_enum),
681            "one source/path/provenance with distinct imported projections must not alias"
682        );
683        assert_ne!(
684            postcard::to_allocvec(&without_imported_enum.functions["exercise"].freeze_for_cache(),)
685                .expect("serialize cached dynamically-resolved function"),
686            postcard::to_allocvec(&with_imported_enum.functions["exercise"].freeze_for_cache(),)
687                .expect("serialize cached imported-enum-resolved function")
688        );
689        assert_eq!(cache.stats().insertions, 2);
690    }
691
692    #[test]
693    fn dropping_last_cache_handle_releases_prepared_artifacts() {
694        let cache = PreparedModuleCache::default();
695        let path = PathBuf::from("module.harn");
696        let source = ModuleSource::from_text("pub fn value() { 1 }");
697        let artifact = empty_artifact();
698        let weak = Arc::downgrade(&artifact);
699        let _ = cache.insert(path, source.sha256(), artifact);
700        let clone = cache.clone();
701
702        drop(cache);
703        assert!(weak.upgrade().is_some());
704        drop(clone);
705        assert!(weak.upgrade().is_none());
706    }
707
708    #[test]
709    fn concurrent_identical_misses_compile_one_immutable_artifact() {
710        const WORKERS: usize = 8;
711
712        let cache = PreparedModuleCache::default();
713        let source = Arc::new(ModuleSource::from_text(
714            (0..128)
715                .map(|index| format!("pub fn value_{index}() {{ return {index} }}\n"))
716                .collect::<String>(),
717        ));
718        let source_path = Arc::new(PathBuf::from("shared-runtime-module.harn"));
719        let start = Arc::new(Barrier::new(WORKERS + 1));
720        let mut handles = Vec::with_capacity(WORKERS);
721
722        for _ in 0..WORKERS {
723            let cache = cache.clone();
724            let source = Arc::clone(&source);
725            let source_path = Arc::clone(&source_path);
726            let start = Arc::clone(&start);
727            handles.push(std::thread::spawn(move || {
728                let recorder = ModulePhaseRecorder::new();
729                start.wait();
730                let artifact = cache
731                    .prepare(
732                        &source_path,
733                        &source_path,
734                        &source,
735                        None,
736                        Some(&recorder),
737                        ModuleProvenance::TrustedHostDispatch,
738                    )
739                    .expect("compile shared immutable module");
740                (artifact, recorder.snapshot())
741            }));
742        }
743
744        start.wait();
745        let outcomes = handles
746            .into_iter()
747            .map(|handle| handle.join().expect("module compiler worker joins"))
748            .collect::<Vec<_>>();
749        let first = &outcomes[0].0;
750
751        assert!(
752            outcomes
753                .iter()
754                .all(|(artifact, _)| Arc::ptr_eq(first, artifact)),
755            "all workers must consume the same immutable prepared artifact"
756        );
757        assert_eq!(
758            outcomes
759                .iter()
760                .map(|(_, phases)| phases.modules_compiled)
761                .sum::<u64>(),
762            1,
763            "one exact cache key must have one compilation owner regardless of worker count"
764        );
765        assert_eq!(cache.stats().insertions, 1);
766    }
767
768    #[test]
769    fn failed_preparation_is_not_cached_or_poisoned() {
770        let cache = PreparedModuleCache::default();
771        let key = PreparedModuleCacheKey::new(
772            PathBuf::from("recoverable.harn"),
773            ModuleSource::from_text("pub fn value() { return 1 }").sha256(),
774            ModuleProvenance::TrustedHostDispatch,
775        );
776
777        let failed = cache.prepare_exact_key(&key, None, || {
778            Err(VmError::Runtime(
779                "synthetic compilation failure".to_string(),
780            ))
781        });
782        assert!(
783            matches!(failed, Err(VmError::Runtime(message)) if message == "synthetic compilation failure")
784        );
785        assert_eq!(cache.stats().entries, 0);
786        assert_eq!(cache.stats().insertions, 0);
787
788        let expected = empty_artifact_with_provenance(ModuleProvenance::TrustedHostDispatch);
789        let prepared = cache
790            .prepare_exact_key(&key, None, || Ok(Arc::clone(&expected)))
791            .expect("a failed owner must release the exact-key preparation slot");
792
793        assert!(Arc::ptr_eq(&prepared, &expected));
794        assert_eq!(cache.stats().misses, 2);
795        assert_eq!(cache.stats().insertions, 1);
796        assert_eq!(cache.stats().entries, 1);
797    }
798
799    #[test]
800    fn hydration_moves_module_owned_storage() {
801        let source = r#"
802import { assert_eq } from "std/testing"
803pub type Result = {value: int}
804pub const value = 1
805pub fn answer(items: list<string>) {
806  fn nested() { return 42 }
807  return items
808}
809"#;
810        let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
811            .expect("compile typed module artifact");
812
813        let imports = artifact.imports.as_ptr();
814        let import_path = artifact.imports[0].path.as_ptr();
815        let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
816            panic!("expected selective import");
817        };
818        let selected_names = selected.as_ptr();
819        let selected_name = selected[0].as_ptr();
820        let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
821        let schema_init_codes = artifact
822            .type_schema_init_chunks
823            .iter()
824            .map(|chunk| chunk.code.as_ptr())
825            .collect::<Vec<_>>();
826        let (function_key, function) = artifact.functions.first_key_value().unwrap();
827        let function_key = function_key.as_ptr();
828        let function_name = function.name.clone();
829        let function_code = function.chunk.code.as_ptr();
830        let param_name = function.params[0].name.as_ptr();
831        let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
832        let nested_name = function.chunk.functions[0].name.clone();
833        let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
834        let public_export_name = artifact
835            .public_exports
836            .get_key_value("answer")
837            .unwrap()
838            .0
839            .as_ptr();
840        let public_export_kind = *artifact.public_exports.get("answer").unwrap();
841        let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
842        let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
843        let hydrated = PreparedModuleArtifact::from_cached(artifact);
844
845        assert_eq!(hydrated.imports.as_ptr(), imports);
846        assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
847        let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
848            panic!("expected selective import");
849        };
850        assert_eq!(selected.as_ptr(), selected_names);
851        assert_eq!(selected[0].as_ptr(), selected_name);
852        assert_eq!(
853            hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
854            init_code
855        );
856        assert_eq!(
857            hydrated
858                .type_schema_init_chunks
859                .iter()
860                .map(|chunk| chunk.code.as_ptr())
861                .collect::<Vec<_>>(),
862            schema_init_codes
863        );
864        let (hydrated_function_key, hydrated_function) =
865            hydrated.functions.first_key_value().unwrap();
866        assert_eq!(hydrated_function_key.as_ptr(), function_key);
867        // Function names convert into a shared `HarnStr` at hydration (one
868        // short copy) so per-call consumers can share them; compare by value.
869        assert_eq!(hydrated_function.name.as_str(), function_name);
870        assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
871        assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
872        assert_eq!(
873            named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
874            param_type_name
875        );
876        assert_eq!(
877            hydrated_function.chunk.functions[0].name.as_str(),
878            nested_name
879        );
880        assert_eq!(
881            hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
882            nested_code
883        );
884        assert_eq!(
885            hydrated
886                .public_exports
887                .get_key_value("answer")
888                .unwrap()
889                .0
890                .as_ptr(),
891            public_export_name
892        );
893        assert_eq!(
894            hydrated.public_exports.get("answer"),
895            Some(&public_export_kind)
896        );
897        assert_eq!(
898            hydrated.public_value_names.get("value").unwrap().as_ptr(),
899            public_value_name
900        );
901        assert_eq!(
902            hydrated.public_type_names.get("Result").unwrap().as_ptr(),
903            public_type_name
904        );
905    }
906}