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 => {
270 if loaded.public_names.is_empty() {
271 loaded.functions.keys().cloned().collect()
272 } else {
273 loaded.public_names.iter().cloned().collect()
274 }
275 }
276 };
277 for name in names_to_reexport {
278 let Some(closure) = loaded.functions.get(&name) else {
279 return Err(VmError::Runtime(format!(
280 "Re-export error: '{name}' is not exported by '{}'",
281 import.path
282 )));
283 };
284 if let Some(existing) = functions.get(&name) {
285 if !Arc::ptr_eq(existing, closure) {
286 return Err(VmError::Runtime(format!(
287 "Re-export collision: '{name}' is defined here and also \
288 re-exported from '{}'",
289 import.path
290 )));
291 }
292 }
293 functions.insert(name.clone(), Arc::clone(closure));
294 public_names.insert(name);
295 }
296 }
297
298 self.env = caller_env;
299 self.source_dir = old_source_dir;
300
301 Ok(LoadedModule {
302 functions,
303 public_names,
304 _module_functions: registry,
305 _module_state: module_state,
306 })
307 }
308
309 fn export_loaded_module(
310 &mut self,
311 module_path: &Path,
312 loaded: &LoadedModule,
313 selected_names: Option<&[String]>,
314 ) -> Result<(), VmError> {
315 let module_name = module_path.display().to_string();
316 let export_names: Vec<String> = if let Some(names) = selected_names {
317 if !loaded.public_names.is_empty() {
324 for name in names {
325 if !loaded.public_names.contains(name) {
326 let hint = if loaded.functions.contains_key(name) {
327 " — it is defined there but not `pub`; mark it `pub` to export it"
328 } else {
329 ""
330 };
331 return Err(VmError::Runtime(format!(
332 "Import error: '{name}' is not exported by {module_name}{hint}"
333 )));
334 }
335 }
336 }
337 names.to_vec()
338 } else if !loaded.public_names.is_empty() {
339 loaded.public_names.iter().cloned().collect()
340 } else {
341 loaded.functions.keys().cloned().collect()
342 };
343
344 for name in export_names {
345 let Some(closure) = loaded.functions.get(&name) else {
346 return Err(VmError::Runtime(format!(
347 "Import error: '{name}' is not defined in {module_name}"
348 )));
349 };
350 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
351 return Err(VmError::Runtime(format!(
352 "Import collision: '{name}' is already defined when importing {module_name}. \
353 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
354 )));
355 }
356 self.env
357 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
358 }
359 Ok(())
360 }
361
362 pub(super) fn execute_import<'a>(
364 &'a mut self,
365 path: &'a str,
366 selected_names: Option<&'a [String]>,
367 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
368 Box::pin(async move {
369 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
370
371 let stdlib_module = path
372 .strip_prefix("std/")
373 .or_else(|| (path == "observability").then_some("observability"));
374 if let Some(module) = stdlib_module {
375 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
376 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
377 if self.imported_paths.contains(&synthetic) {
378 return Ok(());
379 }
380 let loaded = self
381 .load_stdlib_module_from_source(module, synthetic.clone(), source)
382 .await?;
383 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
384 return Ok(());
385 }
386 return Err(VmError::Runtime(format!(
387 "Unknown stdlib module: std/{module}"
388 )));
389 }
390
391 let base = self
392 .source_dir
393 .clone()
394 .unwrap_or_else(|| PathBuf::from("."));
395 let file_path = resolve_module_import_path(&base, path);
396
397 let canonical = file_path
398 .canonicalize()
399 .unwrap_or_else(|_| file_path.clone());
400 if self.imported_paths.contains(&canonical) {
401 if let Some(importer) = self.imported_paths.last().cloned() {
409 if importer != canonical {
410 self.deferred_cyclic_imports.push(DeferredCyclicImport {
411 importer,
412 target: canonical.clone(),
413 selected_names: selected_names.map(<[String]>::to_vec),
414 });
415 }
416 }
417 return Ok(());
418 }
419 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
420 return self.export_loaded_module(&canonical, &loaded, selected_names);
421 }
422 self.imported_paths.push(canonical.clone());
423
424 let source = std::fs::read_to_string(&file_path).map_err(|e| {
425 VmError::Runtime(format!(
430 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
431 file_path.display(),
432 base.display()
433 ))
434 })?;
435 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
436 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
437
438 let lookup = bytecode_cache::load_module(&file_path, &source);
441 let artifact = if let Some(artifact) = lookup.artifact {
442 artifact
443 } else {
444 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
445 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
446 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
447 eprintln!(
448 "[harn] module cache write skipped for {}: {err}",
449 file_path.display()
450 );
451 }
452 }
453 compiled
454 };
455
456 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
457 let loaded = self
458 .instantiate_module(module_source_dir, &artifact)
459 .await?;
460 self.imported_paths.pop();
461 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
462 self.export_loaded_module(&canonical, &loaded, selected_names)?;
463
464 if self.imported_paths.is_empty() {
468 self.flush_deferred_cyclic_imports()?;
469 }
470
471 Ok(())
472 })
473 }
474
475 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
484 if self.deferred_cyclic_imports.is_empty() {
485 return Ok(());
486 }
487 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
488 let mut still_pending = Vec::new();
489 for import in deferred {
490 let (Some(importer), Some(target)) = (
491 self.module_cache.get(&import.importer).cloned(),
492 self.module_cache.get(&import.target).cloned(),
493 ) else {
494 still_pending.push(import);
498 continue;
499 };
500
501 let export_names: Vec<String> = match &import.selected_names {
502 Some(names) => names.clone(),
503 None if !target.public_names.is_empty() => {
504 target.public_names.iter().cloned().collect()
505 }
506 None => target.functions.keys().cloned().collect(),
507 };
508
509 let mut module_state = importer._module_state.lock();
510 for name in export_names {
511 let Some(closure) = target.functions.get(&name) else {
512 return Err(VmError::Runtime(format!(
513 "Import error: '{name}' is not defined in {}",
514 import.target.display()
515 )));
516 };
517 if module_state.get(&name).is_none() {
520 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
521 }
522 }
523 }
524 self.deferred_cyclic_imports = still_pending;
525 Ok(())
526 }
527
528 fn cache_key_for_import(&self, path: &str) -> PathBuf {
533 if let Some(module) = path
534 .strip_prefix("std/")
535 .or_else(|| (path == "observability").then_some("observability"))
536 {
537 return PathBuf::from(format!("<stdlib>/{module}.harn"));
538 }
539 let base = self
540 .source_dir
541 .clone()
542 .unwrap_or_else(|| PathBuf::from("."));
543 let file_path = resolve_module_import_path(&base, path);
544 file_path.canonicalize().unwrap_or(file_path)
545 }
546
547 pub async fn load_module_exports(
550 &mut self,
551 path: &Path,
552 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
553 let path_str = path.to_string_lossy().into_owned();
554 self.execute_import(&path_str, None).await?;
555
556 let mut file_path = if path.is_absolute() {
557 path.to_path_buf()
558 } else {
559 self.source_dir
560 .clone()
561 .unwrap_or_else(|| PathBuf::from("."))
562 .join(path)
563 };
564 if !file_path.exists() && file_path.extension().is_none() {
565 file_path.set_extension("harn");
566 }
567
568 let canonical = file_path
569 .canonicalize()
570 .unwrap_or_else(|_| file_path.clone());
571 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
572 VmError::Runtime(format!(
573 "Import error: failed to cache loaded module '{}'",
574 canonical.display()
575 ))
576 })?;
577
578 let export_names: Vec<String> = if loaded.public_names.is_empty() {
579 loaded.functions.keys().cloned().collect()
580 } else {
581 loaded.public_names.iter().cloned().collect()
582 };
583
584 let mut exports = BTreeMap::new();
585 for name in export_names {
586 let Some(closure) = loaded.functions.get(&name) else {
587 return Err(VmError::Runtime(format!(
588 "Import error: exported function '{name}' is missing from {}",
589 canonical.display()
590 )));
591 };
592 exports.insert(name, Arc::clone(closure));
593 }
594
595 Ok(exports)
596 }
597
598 pub async fn load_module_exports_from_source(
601 &mut self,
602 source_key: impl Into<PathBuf>,
603 source: &str,
604 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
605 let synthetic = source_key.into();
606 let loaded = self
607 .load_module_from_source(synthetic.clone(), source)
608 .await?;
609 let export_names: Vec<String> = if loaded.public_names.is_empty() {
610 loaded.functions.keys().cloned().collect()
611 } else {
612 loaded.public_names.iter().cloned().collect()
613 };
614
615 let mut exports = BTreeMap::new();
616 for name in export_names {
617 let Some(closure) = loaded.functions.get(&name) else {
618 return Err(VmError::Runtime(format!(
619 "Import error: exported function '{name}' is missing from {}",
620 synthetic.display()
621 )));
622 };
623 exports.insert(name, Arc::clone(closure));
624 }
625
626 Ok(exports)
627 }
628
629 pub async fn load_module_exports_from_import(
633 &mut self,
634 import_path: &str,
635 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
636 self.execute_import(import_path, None).await?;
637
638 if let Some(module) = import_path
639 .strip_prefix("std/")
640 .or_else(|| (import_path == "observability").then_some("observability"))
641 {
642 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
643 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
644 VmError::Runtime(format!(
645 "Import error: failed to cache loaded module '{}'",
646 synthetic.display()
647 ))
648 })?;
649 let mut exports = BTreeMap::new();
650 let export_names: Vec<String> = if loaded.public_names.is_empty() {
651 loaded.functions.keys().cloned().collect()
652 } else {
653 loaded.public_names.iter().cloned().collect()
654 };
655 for name in export_names {
656 let Some(closure) = loaded.functions.get(&name) else {
657 return Err(VmError::Runtime(format!(
658 "Import error: exported function '{name}' is missing from {}",
659 synthetic.display()
660 )));
661 };
662 exports.insert(name, Arc::clone(closure));
663 }
664 return Ok(exports);
665 }
666
667 let base = self
668 .source_dir
669 .clone()
670 .unwrap_or_else(|| PathBuf::from("."));
671 let file_path = resolve_module_import_path(&base, import_path);
672 self.load_module_exports(&file_path).await
673 }
674}
675
676#[cfg(test)]
677mod tests {
678
679 use std::sync::{Mutex, MutexGuard, OnceLock};
680
681 use super::*;
682
683 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
684
685 fn cache_test_guard() -> MutexGuard<'static, ()> {
686 CACHE_TEST_LOCK
687 .get_or_init(|| Mutex::new(()))
688 .lock()
689 .unwrap()
690 }
691
692 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
693 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
694 stdlib_module_artifact_cache_ptr(module, source)
695 }
696
697 #[test]
698 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
699 let _guard = cache_test_guard();
700 reset_stdlib_module_artifact_cache();
701 let runtime = tokio::runtime::Builder::new_current_thread()
702 .enable_all()
703 .build()
704 .expect("runtime builds");
705
706 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
707 .block_on(async {
708 let mut first_vm = Vm::new();
709 let first_exports = first_vm
710 .load_module_exports_from_import("std/agent/prompts")
711 .await
712 .expect("first stdlib import succeeds");
713 let first_state = first_exports
714 .get("render_agent_prompt")
715 .expect("first export exists")
716 .module_state()
717 .expect("first module state stays live while VM owns module");
718 let first_state_weak = Arc::downgrade(&first_state);
719 let first_state_ptr = Arc::as_ptr(&first_state);
720
721 let mut second_vm = Vm::new();
722 let second_exports = second_vm
723 .load_module_exports_from_import("std/agent/prompts")
724 .await
725 .expect("second stdlib import succeeds");
726 let second_state = second_exports
727 .get("render_agent_prompt")
728 .expect("second export exists")
729 .module_state()
730 .expect("second module state stays live while VM owns module");
731 let second_state_weak = Arc::downgrade(&second_state);
732
733 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
734 (
735 first_exports,
736 second_exports,
737 first_state_weak,
738 second_state_weak,
739 )
740 });
741 let first_cached =
742 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
743 assert_eq!(
744 cached_stdlib_module_ptr("agent/prompts"),
745 Some(first_cached)
746 );
747
748 let first = first_exports
749 .get("render_agent_prompt")
750 .expect("first export exists");
751 let second = second_exports
752 .get("render_agent_prompt")
753 .expect("second export exists");
754
755 assert!(!Arc::ptr_eq(first, second));
756 assert!(!Arc::ptr_eq(&first.func, &second.func));
757 assert!(!Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
758 assert!(first.module_state().is_none());
759 assert!(second.module_state().is_none());
760 assert!(first_state_weak.upgrade().is_none());
761 assert!(second_state_weak.upgrade().is_none());
762 }
763
764 #[test]
765 fn stdlib_artifact_cache_is_process_wide_across_threads() {
766 let _guard = cache_test_guard();
767 reset_stdlib_module_artifact_cache();
768
769 let handle = std::thread::spawn(|| {
770 let runtime = tokio::runtime::Builder::new_current_thread()
771 .enable_all()
772 .build()
773 .expect("runtime builds");
774 runtime.block_on(async {
775 let mut vm = Vm::new();
776 vm.load_module_exports_from_import("std/agent/prompts")
777 .await
778 .expect("thread stdlib import succeeds");
779 });
780 });
781 handle.join().expect("thread joins");
782 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
783 .expect("thread import cached stdlib artifact");
784
785 let runtime = tokio::runtime::Builder::new_current_thread()
786 .enable_all()
787 .build()
788 .expect("runtime builds");
789 runtime.block_on(async {
790 let mut vm = Vm::new();
791 vm.load_module_exports_from_import("std/agent/prompts")
792 .await
793 .expect("main-thread stdlib import succeeds");
794 });
795 assert_eq!(
796 cached_stdlib_module_ptr("agent/prompts"),
797 Some(thread_cached)
798 );
799 }
800
801 #[test]
802 fn module_closures_release_state_after_vm_drop() {
803 let runtime = tokio::runtime::Builder::new_current_thread()
804 .enable_all()
805 .build()
806 .expect("runtime builds");
807
808 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
809 let mut vm = Vm::new();
810 let loaded = vm
811 .load_module_from_source(
812 PathBuf::from("<test>/module_cycle.harn"),
813 r#"
814var payload = "x" * 1024
815
816pub fn touch() {
817 return len(payload)
818}
819"#,
820 )
821 .await
822 .expect("module loads");
823 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
824 let closure_weak = Arc::downgrade(&closure);
825 let registry_weak = Arc::downgrade(&loaded._module_functions);
826 let state_weak = Arc::downgrade(&loaded._module_state);
827
828 drop(closure);
829 drop(loaded);
830 drop(vm);
831
832 (closure_weak, registry_weak, state_weak)
833 });
834
835 assert!(
836 closure_weak.upgrade().is_none(),
837 "module closure should drop with its VM"
838 );
839 assert!(
840 registry_weak.upgrade().is_none(),
841 "module function registry should drop with its VM"
842 );
843 assert!(
844 state_weak.upgrade().is_none(),
845 "module state should drop with its VM"
846 );
847 }
848}