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