1use std::collections::{BTreeMap, HashSet};
2use std::future::Future;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::sync::{Arc, Mutex, OnceLock};
7
8use crate::bytecode_cache;
9use crate::chunk::{Chunk, CompiledFunction};
10use crate::module_artifact::{compile_module_artifact_from_source, ModuleArtifact};
11use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
12
13use super::{ScopeSpan, Vm};
14
15static STDLIB_MODULE_ARTIFACT_CACHE: OnceLock<Mutex<BTreeMap<String, Arc<ModuleArtifact>>>> =
16 OnceLock::new();
17
18fn stdlib_module_artifact_cache() -> &'static Mutex<BTreeMap<String, Arc<ModuleArtifact>>> {
19 STDLIB_MODULE_ARTIFACT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
20}
21
22#[cfg(test)]
23fn reset_stdlib_module_artifact_cache() {
24 stdlib_module_artifact_cache().lock().unwrap().clear();
25}
26
27#[cfg(test)]
28fn stdlib_module_artifact_cache_ptr(module: &str, source: &str) -> Option<usize> {
29 let key = stdlib_artifact_cache_key(module, source);
30 stdlib_module_artifact_cache()
31 .lock()
32 .unwrap()
33 .get(&key)
34 .map(|artifact| Arc::as_ptr(artifact) as usize)
35}
36
37#[derive(Clone)]
38pub(crate) struct LoadedModule {
39 pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
40 pub(crate) public_names: HashSet<String>,
41 pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
42 pub(crate) _module_state: crate::value::ModuleState,
43}
44
45#[derive(Clone, Debug)]
51pub(crate) struct DeferredCyclicImport {
52 pub(crate) importer: PathBuf,
54 pub(crate) target: PathBuf,
56 pub(crate) selected_names: Option<Vec<String>>,
58}
59
60pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
61 let synthetic_current_file = base.join("__harn_import_base__.harn");
62 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
63 return resolved;
64 }
65
66 let mut file_path = base.join(path);
67
68 if !file_path.exists() && file_path.extension().is_none() {
69 file_path.set_extension("harn");
70 }
71
72 file_path
73}
74
75fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
76 let mut hasher = std::collections::hash_map::DefaultHasher::new();
77 module.hash(&mut hasher);
78 source.hash(&mut hasher);
79 format!("{module}:{:016x}", hasher.finish())
80}
81
82fn stdlib_module_artifact(
83 module: &str,
84 synthetic: &Path,
85 source: &'static str,
86) -> Result<Arc<ModuleArtifact>, VmError> {
87 let key = stdlib_artifact_cache_key(module, source);
88 {
89 let cache = stdlib_module_artifact_cache().lock().unwrap();
90 if let Some(cached) = cache.get(&key) {
91 return Ok(Arc::clone(cached));
92 }
93 }
94
95 let lookup = bytecode_cache::load_module(synthetic, source);
100 let artifact = if let Some(artifact) = lookup.artifact {
101 artifact
102 } else {
103 let compiled = compile_module_artifact_from_source(synthetic, source)?;
104 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
105 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
106 eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
107 }
108 }
109 compiled
110 };
111
112 let compiled = Arc::new(artifact);
113 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
114 if let Some(cached) = cache.get(&key) {
115 return Ok(Arc::clone(cached));
116 }
117 cache.insert(key, Arc::clone(&compiled));
118 Ok(compiled)
119}
120
121impl Vm {
122 async fn load_module_from_source(
123 &mut self,
124 synthetic: PathBuf,
125 source: &str,
126 ) -> Result<LoadedModule, VmError> {
127 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
128 return Ok(loaded);
129 }
130 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
131
132 let artifact = compile_module_artifact_from_source(&synthetic, source)?;
133
134 self.imported_paths.push(synthetic.clone());
135 let loaded = self.instantiate_module(None, &artifact).await?;
136 self.imported_paths.pop();
137 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
138 Ok(loaded)
139 }
140
141 async fn load_stdlib_module_from_source(
142 &mut self,
143 module: &str,
144 synthetic: PathBuf,
145 source: &'static str,
146 ) -> Result<LoadedModule, VmError> {
147 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
148 return Ok(loaded);
149 }
150 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
151
152 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
153 self.imported_paths.push(synthetic.clone());
154 let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
155 self.imported_paths.pop();
156 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
157 Ok(loaded)
158 }
159
160 async fn instantiate_stdlib_module(
161 &mut self,
162 artifact: &ModuleArtifact,
163 ) -> Result<LoadedModule, VmError> {
164 self.instantiate_module(None, artifact).await
165 }
166
167 async fn instantiate_module(
175 &mut self,
176 module_source_dir: Option<PathBuf>,
177 artifact: &ModuleArtifact,
178 ) -> Result<LoadedModule, VmError> {
179 let caller_env = self.env.clone();
180 let old_source_dir = self.source_dir.clone();
181 self.env = VmEnv::new();
182 self.source_dir = module_source_dir.clone();
183
184 for import in &artifact.imports {
185 self.execute_import(&import.path, import.selected_names.as_deref())
186 .await?;
187 }
188
189 let module_state: crate::value::ModuleState = {
190 let mut init_env = self.env.clone();
191 if let Some(init_chunk) = &artifact.init_chunk {
192 let fresh_init_chunk = Chunk::from_cached(init_chunk);
193 let saved_env = std::mem::replace(&mut self.env, init_env);
194 let saved_frames = std::mem::take(&mut self.frames);
195 let saved_handlers = std::mem::take(&mut self.exception_handlers);
196 let saved_iterators = std::mem::take(&mut self.iterators);
197 let saved_deadlines = std::mem::take(&mut self.deadlines);
198 let active_context = crate::step_runtime::take_active_context();
209 let init_result = self.run_chunk(std::sync::Arc::new(fresh_init_chunk)).await;
210 crate::step_runtime::restore_active_context(active_context);
211 init_env = std::mem::replace(&mut self.env, saved_env);
212 self.frames = saved_frames;
213 self.exception_handlers = saved_handlers;
214 self.iterators = saved_iterators;
215 self.deadlines = saved_deadlines;
216 init_result?;
217 }
218 Arc::new(crate::value::VmMutex::new(init_env))
219 };
220
221 let module_env = self.env.clone();
222 let registry: ModuleFunctionRegistry =
223 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
224 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
225 let mut public_names = artifact.public_names.clone();
226
227 for (name, compiled) in &artifact.functions {
228 let closure = Arc::new(VmClosure {
229 func: Arc::new(CompiledFunction::from_cached(compiled)),
230 env: module_env.clone(),
231 source_dir: module_source_dir.clone(),
232 module_functions: Some(Arc::downgrade(®istry)),
233 module_state: Some(Arc::downgrade(&module_state)),
234 });
235 registry.lock().insert(name.clone(), Arc::clone(&closure));
236 self.env
237 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
238 module_state
239 .lock()
240 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
241 functions.insert(name.clone(), Arc::clone(&closure));
242 }
243
244 for import in artifact.imports.iter().filter(|import| import.is_pub) {
245 let cache_key = self.cache_key_for_import(&import.path);
246 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
247 if self.imported_paths.contains(&cache_key) {
254 return Err(VmError::Runtime(format!(
255 "Re-export error: cannot `pub import` from '{}' because it forms an \
256 import cycle with this module (its public surface is still being \
257 built). Use a plain `import` here, or re-export from a module that is \
258 not part of the cycle.",
259 import.path
260 )));
261 }
262 return Err(VmError::Runtime(format!(
263 "Re-export error: imported module '{}' was not loaded",
264 import.path
265 )));
266 };
267 let names_to_reexport: Vec<String> = match &import.selected_names {
268 Some(names) => names.clone(),
269 None => loaded.public_names.iter().cloned().collect(),
272 };
273 for name in names_to_reexport {
274 let Some(closure) = loaded.functions.get(&name) else {
275 return Err(VmError::Runtime(format!(
276 "Re-export error: '{name}' is not exported by '{}'",
277 import.path
278 )));
279 };
280 if let Some(existing) = functions.get(&name) {
281 if !Arc::ptr_eq(existing, closure) {
282 return Err(VmError::Runtime(format!(
283 "Re-export collision: '{name}' is defined here and also \
284 re-exported from '{}'",
285 import.path
286 )));
287 }
288 }
289 functions.insert(name.clone(), Arc::clone(closure));
290 public_names.insert(name);
291 }
292 }
293
294 self.env = caller_env;
295 self.source_dir = old_source_dir;
296
297 Ok(LoadedModule {
298 functions,
299 public_names,
300 _module_functions: registry,
301 _module_state: module_state,
302 })
303 }
304
305 fn export_loaded_module(
306 &mut self,
307 module_path: &Path,
308 loaded: &LoadedModule,
309 selected_names: Option<&[String]>,
310 ) -> Result<(), VmError> {
311 let module_name = module_path.display().to_string();
312 let export_names: Vec<String> = if let Some(names) = selected_names {
313 for name in names {
319 if !loaded.public_names.contains(name) {
320 let hint = if loaded.functions.contains_key(name) {
321 " — it is defined there but not `pub`; mark it `pub` to export it"
322 } else {
323 ""
324 };
325 return Err(VmError::Runtime(format!(
326 "Import error: '{name}' is not exported by {module_name}{hint}"
327 )));
328 }
329 }
330 names.to_vec()
331 } else {
332 loaded.public_names.iter().cloned().collect()
334 };
335
336 for name in export_names {
337 let Some(closure) = loaded.functions.get(&name) else {
338 return Err(VmError::Runtime(format!(
339 "Import error: '{name}' is not defined in {module_name}"
340 )));
341 };
342 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
343 return Err(VmError::Runtime(format!(
344 "Import collision: '{name}' is already defined when importing {module_name}. \
345 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
346 )));
347 }
348 self.env
349 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
350 }
351 Ok(())
352 }
353
354 pub(super) fn execute_import<'a>(
356 &'a mut self,
357 path: &'a str,
358 selected_names: Option<&'a [String]>,
359 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
360 Box::pin(async move {
361 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
362
363 let stdlib_module = path
364 .strip_prefix("std/")
365 .or_else(|| (path == "observability").then_some("observability"));
366 if let Some(module) = stdlib_module {
367 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
368 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
369 if self.imported_paths.contains(&synthetic) {
370 return Ok(());
371 }
372 let loaded = self
373 .load_stdlib_module_from_source(module, synthetic.clone(), source)
374 .await?;
375 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
376 return Ok(());
377 }
378 return Err(VmError::Runtime(format!(
379 "Unknown stdlib module: std/{module}"
380 )));
381 }
382
383 let base = self
384 .source_dir
385 .clone()
386 .unwrap_or_else(|| PathBuf::from("."));
387 let file_path = resolve_module_import_path(&base, path);
388
389 let canonical = file_path
390 .canonicalize()
391 .unwrap_or_else(|_| file_path.clone());
392 if self.imported_paths.contains(&canonical) {
393 if let Some(importer) = self.imported_paths.last().cloned() {
401 if importer != canonical {
402 self.deferred_cyclic_imports.push(DeferredCyclicImport {
403 importer,
404 target: canonical.clone(),
405 selected_names: selected_names.map(<[String]>::to_vec),
406 });
407 }
408 }
409 return Ok(());
410 }
411 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
412 return self.export_loaded_module(&canonical, &loaded, selected_names);
413 }
414 self.imported_paths.push(canonical.clone());
415
416 let source = std::fs::read_to_string(&file_path).map_err(|e| {
417 VmError::Runtime(format!(
422 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
423 file_path.display(),
424 base.display()
425 ))
426 })?;
427 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
428 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
429
430 let lookup = bytecode_cache::load_module(&file_path, &source);
433 let artifact = if let Some(artifact) = lookup.artifact {
434 artifact
435 } else {
436 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
437 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
438 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
439 eprintln!(
440 "[harn] module cache write skipped for {}: {err}",
441 file_path.display()
442 );
443 }
444 }
445 compiled
446 };
447
448 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
449 let loaded = self
450 .instantiate_module(module_source_dir, &artifact)
451 .await?;
452 self.imported_paths.pop();
453 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
454 self.export_loaded_module(&canonical, &loaded, selected_names)?;
455
456 if self.imported_paths.is_empty() {
460 self.flush_deferred_cyclic_imports()?;
461 }
462
463 Ok(())
464 })
465 }
466
467 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
476 if self.deferred_cyclic_imports.is_empty() {
477 return Ok(());
478 }
479 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
480 let mut still_pending = Vec::new();
481 for import in deferred {
482 let (Some(importer), Some(target)) = (
483 self.module_cache.get(&import.importer).cloned(),
484 self.module_cache.get(&import.target).cloned(),
485 ) else {
486 still_pending.push(import);
490 continue;
491 };
492
493 let export_names: Vec<String> = match &import.selected_names {
494 Some(names) => names.clone(),
495 None if !target.public_names.is_empty() => {
496 target.public_names.iter().cloned().collect()
497 }
498 None => target.functions.keys().cloned().collect(),
499 };
500
501 let mut module_state = importer._module_state.lock();
502 for name in export_names {
503 let Some(closure) = target.functions.get(&name) else {
504 return Err(VmError::Runtime(format!(
505 "Import error: '{name}' is not defined in {}",
506 import.target.display()
507 )));
508 };
509 if module_state.get(&name).is_none() {
512 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
513 }
514 }
515 }
516 self.deferred_cyclic_imports = still_pending;
517 Ok(())
518 }
519
520 fn cache_key_for_import(&self, path: &str) -> PathBuf {
525 if let Some(module) = path
526 .strip_prefix("std/")
527 .or_else(|| (path == "observability").then_some("observability"))
528 {
529 return PathBuf::from(format!("<stdlib>/{module}.harn"));
530 }
531 let base = self
532 .source_dir
533 .clone()
534 .unwrap_or_else(|| PathBuf::from("."));
535 let file_path = resolve_module_import_path(&base, path);
536 file_path.canonicalize().unwrap_or(file_path)
537 }
538
539 pub async fn load_module_exports(
542 &mut self,
543 path: &Path,
544 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
545 let path_str = path.to_string_lossy().into_owned();
546 self.execute_import(&path_str, None).await?;
547
548 let mut file_path = if path.is_absolute() {
549 path.to_path_buf()
550 } else {
551 self.source_dir
552 .clone()
553 .unwrap_or_else(|| PathBuf::from("."))
554 .join(path)
555 };
556 if !file_path.exists() && file_path.extension().is_none() {
557 file_path.set_extension("harn");
558 }
559
560 let canonical = file_path
561 .canonicalize()
562 .unwrap_or_else(|_| file_path.clone());
563 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
564 VmError::Runtime(format!(
565 "Import error: failed to cache loaded module '{}'",
566 canonical.display()
567 ))
568 })?;
569
570 let export_names: Vec<String> = if loaded.public_names.is_empty() {
571 loaded.functions.keys().cloned().collect()
572 } else {
573 loaded.public_names.iter().cloned().collect()
574 };
575
576 let mut exports = BTreeMap::new();
577 for name in export_names {
578 let Some(closure) = loaded.functions.get(&name) else {
579 return Err(VmError::Runtime(format!(
580 "Import error: exported function '{name}' is missing from {}",
581 canonical.display()
582 )));
583 };
584 exports.insert(name, Arc::clone(closure));
585 }
586
587 Ok(exports)
588 }
589
590 pub async fn load_module_exports_from_source(
593 &mut self,
594 source_key: impl Into<PathBuf>,
595 source: &str,
596 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
597 let synthetic = source_key.into();
598 let loaded = self
599 .load_module_from_source(synthetic.clone(), source)
600 .await?;
601 let export_names: Vec<String> = if loaded.public_names.is_empty() {
602 loaded.functions.keys().cloned().collect()
603 } else {
604 loaded.public_names.iter().cloned().collect()
605 };
606
607 let mut exports = BTreeMap::new();
608 for name in export_names {
609 let Some(closure) = loaded.functions.get(&name) else {
610 return Err(VmError::Runtime(format!(
611 "Import error: exported function '{name}' is missing from {}",
612 synthetic.display()
613 )));
614 };
615 exports.insert(name, Arc::clone(closure));
616 }
617
618 Ok(exports)
619 }
620
621 pub async fn load_module_exports_from_import(
625 &mut self,
626 import_path: &str,
627 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
628 self.execute_import(import_path, None).await?;
629
630 if let Some(module) = import_path
631 .strip_prefix("std/")
632 .or_else(|| (import_path == "observability").then_some("observability"))
633 {
634 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
635 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
636 VmError::Runtime(format!(
637 "Import error: failed to cache loaded module '{}'",
638 synthetic.display()
639 ))
640 })?;
641 let mut exports = BTreeMap::new();
642 let export_names: Vec<String> = if loaded.public_names.is_empty() {
643 loaded.functions.keys().cloned().collect()
644 } else {
645 loaded.public_names.iter().cloned().collect()
646 };
647 for name in export_names {
648 let Some(closure) = loaded.functions.get(&name) else {
649 return Err(VmError::Runtime(format!(
650 "Import error: exported function '{name}' is missing from {}",
651 synthetic.display()
652 )));
653 };
654 exports.insert(name, Arc::clone(closure));
655 }
656 return Ok(exports);
657 }
658
659 let base = self
660 .source_dir
661 .clone()
662 .unwrap_or_else(|| PathBuf::from("."));
663 let file_path = resolve_module_import_path(&base, import_path);
664 self.load_module_exports(&file_path).await
665 }
666}
667
668#[cfg(test)]
669mod tests {
670
671 use std::sync::{Mutex, MutexGuard, OnceLock};
672
673 use super::*;
674
675 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
676
677 fn cache_test_guard() -> MutexGuard<'static, ()> {
678 CACHE_TEST_LOCK
679 .get_or_init(|| Mutex::new(()))
680 .lock()
681 .unwrap()
682 }
683
684 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
685 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
686 stdlib_module_artifact_cache_ptr(module, source)
687 }
688
689 #[test]
690 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
691 let _guard = cache_test_guard();
692 reset_stdlib_module_artifact_cache();
693 let runtime = tokio::runtime::Builder::new_current_thread()
694 .enable_all()
695 .build()
696 .expect("runtime builds");
697
698 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
699 .block_on(async {
700 let mut first_vm = Vm::new();
701 let first_exports = first_vm
702 .load_module_exports_from_import("std/agent/prompts")
703 .await
704 .expect("first stdlib import succeeds");
705 let first_state = first_exports
706 .get("render_agent_prompt")
707 .expect("first export exists")
708 .module_state()
709 .expect("first module state stays live while VM owns module");
710 let first_state_weak = Arc::downgrade(&first_state);
711 let first_state_ptr = Arc::as_ptr(&first_state);
712
713 let mut second_vm = Vm::new();
714 let second_exports = second_vm
715 .load_module_exports_from_import("std/agent/prompts")
716 .await
717 .expect("second stdlib import succeeds");
718 let second_state = second_exports
719 .get("render_agent_prompt")
720 .expect("second export exists")
721 .module_state()
722 .expect("second module state stays live while VM owns module");
723 let second_state_weak = Arc::downgrade(&second_state);
724
725 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
726 (
727 first_exports,
728 second_exports,
729 first_state_weak,
730 second_state_weak,
731 )
732 });
733 let first_cached =
734 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
735 assert_eq!(
736 cached_stdlib_module_ptr("agent/prompts"),
737 Some(first_cached)
738 );
739
740 let first = first_exports
741 .get("render_agent_prompt")
742 .expect("first export exists");
743 let second = second_exports
744 .get("render_agent_prompt")
745 .expect("second export exists");
746
747 assert!(!Arc::ptr_eq(first, second));
748 assert!(!Arc::ptr_eq(&first.func, &second.func));
749 assert!(!Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
750 assert!(first.module_state().is_none());
751 assert!(second.module_state().is_none());
752 assert!(first_state_weak.upgrade().is_none());
753 assert!(second_state_weak.upgrade().is_none());
754 }
755
756 #[test]
757 fn stdlib_artifact_cache_is_process_wide_across_threads() {
758 let _guard = cache_test_guard();
759 reset_stdlib_module_artifact_cache();
760
761 let handle = std::thread::spawn(|| {
762 let runtime = tokio::runtime::Builder::new_current_thread()
763 .enable_all()
764 .build()
765 .expect("runtime builds");
766 runtime.block_on(async {
767 let mut vm = Vm::new();
768 vm.load_module_exports_from_import("std/agent/prompts")
769 .await
770 .expect("thread stdlib import succeeds");
771 });
772 });
773 handle.join().expect("thread joins");
774 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
775 .expect("thread import cached stdlib artifact");
776
777 let runtime = tokio::runtime::Builder::new_current_thread()
778 .enable_all()
779 .build()
780 .expect("runtime builds");
781 runtime.block_on(async {
782 let mut vm = Vm::new();
783 vm.load_module_exports_from_import("std/agent/prompts")
784 .await
785 .expect("main-thread stdlib import succeeds");
786 });
787 assert_eq!(
788 cached_stdlib_module_ptr("agent/prompts"),
789 Some(thread_cached)
790 );
791 }
792
793 #[test]
794 fn module_closures_release_state_after_vm_drop() {
795 let runtime = tokio::runtime::Builder::new_current_thread()
796 .enable_all()
797 .build()
798 .expect("runtime builds");
799
800 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
801 let mut vm = Vm::new();
802 let loaded = vm
803 .load_module_from_source(
804 PathBuf::from("<test>/module_cycle.harn"),
805 r#"
806var payload = "x" * 1024
807
808pub fn touch() {
809 return len(payload)
810}
811"#,
812 )
813 .await
814 .expect("module loads");
815 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
816 let closure_weak = Arc::downgrade(&closure);
817 let registry_weak = Arc::downgrade(&loaded._module_functions);
818 let state_weak = Arc::downgrade(&loaded._module_state);
819
820 drop(closure);
821 drop(loaded);
822 drop(vm);
823
824 (closure_weak, registry_weak, state_weak)
825 });
826
827 assert!(
828 closure_weak.upgrade().is_none(),
829 "module closure should drop with its VM"
830 );
831 assert!(
832 registry_weak.upgrade().is_none(),
833 "module function registry should drop with its VM"
834 );
835 assert!(
836 state_weak.upgrade().is_none(),
837 "module state should drop with its VM"
838 );
839 }
840}