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