1use 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
28pub(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 #[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#[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 pub evictions: u64,
171 pub entries: usize,
172}
173
174#[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 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
231 self.prepare_import_graph_with_provenance(roots, ModuleProvenance::User)
232 }
233
234 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 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 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 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 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}