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
45pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
46 let synthetic_current_file = base.join("__harn_import_base__.harn");
47 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
48 return resolved;
49 }
50
51 let mut file_path = base.join(path);
52
53 if !file_path.exists() && file_path.extension().is_none() {
54 file_path.set_extension("harn");
55 }
56
57 file_path
58}
59
60fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
61 let mut hasher = std::collections::hash_map::DefaultHasher::new();
62 module.hash(&mut hasher);
63 source.hash(&mut hasher);
64 format!("{module}:{:016x}", hasher.finish())
65}
66
67fn stdlib_module_artifact(
68 module: &str,
69 synthetic: &Path,
70 source: &'static str,
71) -> Result<Arc<ModuleArtifact>, VmError> {
72 let key = stdlib_artifact_cache_key(module, source);
73 {
74 let cache = stdlib_module_artifact_cache().lock().unwrap();
75 if let Some(cached) = cache.get(&key) {
76 return Ok(Arc::clone(cached));
77 }
78 }
79
80 let lookup = bytecode_cache::load_module(synthetic, source);
85 let artifact = if let Some(artifact) = lookup.artifact {
86 artifact
87 } else {
88 let compiled = compile_module_artifact_from_source(synthetic, source)?;
89 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
90 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
91 eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
92 }
93 }
94 compiled
95 };
96
97 let compiled = Arc::new(artifact);
98 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
99 if let Some(cached) = cache.get(&key) {
100 return Ok(Arc::clone(cached));
101 }
102 cache.insert(key, Arc::clone(&compiled));
103 Ok(compiled)
104}
105
106impl Vm {
107 async fn load_module_from_source(
108 &mut self,
109 synthetic: PathBuf,
110 source: &str,
111 ) -> Result<LoadedModule, VmError> {
112 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
113 return Ok(loaded);
114 }
115 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
116
117 let artifact = compile_module_artifact_from_source(&synthetic, source)?;
118
119 self.imported_paths.push(synthetic.clone());
120 let loaded = self.instantiate_module(None, &artifact).await?;
121 self.imported_paths.pop();
122 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
123 Ok(loaded)
124 }
125
126 async fn load_stdlib_module_from_source(
127 &mut self,
128 module: &str,
129 synthetic: PathBuf,
130 source: &'static str,
131 ) -> Result<LoadedModule, VmError> {
132 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
133 return Ok(loaded);
134 }
135 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
136
137 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
138 self.imported_paths.push(synthetic.clone());
139 let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
140 self.imported_paths.pop();
141 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
142 Ok(loaded)
143 }
144
145 async fn instantiate_stdlib_module(
146 &mut self,
147 artifact: &ModuleArtifact,
148 ) -> Result<LoadedModule, VmError> {
149 self.instantiate_module(None, artifact).await
150 }
151
152 async fn instantiate_module(
160 &mut self,
161 module_source_dir: Option<PathBuf>,
162 artifact: &ModuleArtifact,
163 ) -> Result<LoadedModule, VmError> {
164 let caller_env = self.env.clone();
165 let old_source_dir = self.source_dir.clone();
166 self.env = VmEnv::new();
167 self.source_dir = module_source_dir.clone();
168
169 for import in &artifact.imports {
170 self.execute_import(&import.path, import.selected_names.as_deref())
171 .await?;
172 }
173
174 let module_state: crate::value::ModuleState = {
175 let mut init_env = self.env.clone();
176 if let Some(init_chunk) = &artifact.init_chunk {
177 let fresh_init_chunk = Chunk::from_cached(init_chunk);
178 let saved_env = std::mem::replace(&mut self.env, init_env);
179 let saved_frames = std::mem::take(&mut self.frames);
180 let saved_handlers = std::mem::take(&mut self.exception_handlers);
181 let saved_iterators = std::mem::take(&mut self.iterators);
182 let saved_deadlines = std::mem::take(&mut self.deadlines);
183 let active_context = crate::step_runtime::take_active_context();
194 let init_result = self.run_chunk(&fresh_init_chunk).await;
195 crate::step_runtime::restore_active_context(active_context);
196 init_env = std::mem::replace(&mut self.env, saved_env);
197 self.frames = saved_frames;
198 self.exception_handlers = saved_handlers;
199 self.iterators = saved_iterators;
200 self.deadlines = saved_deadlines;
201 init_result?;
202 }
203 Arc::new(crate::value::VmMutex::new(init_env))
204 };
205
206 let module_env = self.env.clone();
207 let registry: ModuleFunctionRegistry =
208 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
209 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
210 let mut public_names = artifact.public_names.clone();
211
212 for (name, compiled) in &artifact.functions {
213 let closure = Arc::new(VmClosure {
214 func: Arc::new(CompiledFunction::from_cached(compiled)),
215 env: module_env.clone(),
216 source_dir: module_source_dir.clone(),
217 module_functions: Some(Arc::downgrade(®istry)),
218 module_state: Some(Arc::downgrade(&module_state)),
219 });
220 registry.lock().insert(name.clone(), Arc::clone(&closure));
221 self.env
222 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
223 module_state
224 .lock()
225 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
226 functions.insert(name.clone(), Arc::clone(&closure));
227 }
228
229 for import in artifact.imports.iter().filter(|import| import.is_pub) {
230 let cache_key = self.cache_key_for_import(&import.path);
231 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
232 return Err(VmError::Runtime(format!(
233 "Re-export error: imported module '{}' was not loaded",
234 import.path
235 )));
236 };
237 let names_to_reexport: Vec<String> = match &import.selected_names {
238 Some(names) => names.clone(),
239 None => {
240 if loaded.public_names.is_empty() {
241 loaded.functions.keys().cloned().collect()
242 } else {
243 loaded.public_names.iter().cloned().collect()
244 }
245 }
246 };
247 for name in names_to_reexport {
248 let Some(closure) = loaded.functions.get(&name) else {
249 return Err(VmError::Runtime(format!(
250 "Re-export error: '{name}' is not exported by '{}'",
251 import.path
252 )));
253 };
254 if let Some(existing) = functions.get(&name) {
255 if !Arc::ptr_eq(existing, closure) {
256 return Err(VmError::Runtime(format!(
257 "Re-export collision: '{name}' is defined here and also \
258 re-exported from '{}'",
259 import.path
260 )));
261 }
262 }
263 functions.insert(name.clone(), Arc::clone(closure));
264 public_names.insert(name);
265 }
266 }
267
268 self.env = caller_env;
269 self.source_dir = old_source_dir;
270
271 Ok(LoadedModule {
272 functions,
273 public_names,
274 _module_functions: registry,
275 _module_state: module_state,
276 })
277 }
278
279 fn export_loaded_module(
280 &mut self,
281 module_path: &Path,
282 loaded: &LoadedModule,
283 selected_names: Option<&[String]>,
284 ) -> Result<(), VmError> {
285 let export_names: Vec<String> = if let Some(names) = selected_names {
286 names.to_vec()
287 } else if !loaded.public_names.is_empty() {
288 loaded.public_names.iter().cloned().collect()
289 } else {
290 loaded.functions.keys().cloned().collect()
291 };
292
293 let module_name = module_path.display().to_string();
294 for name in export_names {
295 let Some(closure) = loaded.functions.get(&name) else {
296 return Err(VmError::Runtime(format!(
297 "Import error: '{name}' is not defined in {module_name}"
298 )));
299 };
300 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
301 return Err(VmError::Runtime(format!(
302 "Import collision: '{name}' is already defined when importing {module_name}. \
303 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
304 )));
305 }
306 self.env
307 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
308 }
309 Ok(())
310 }
311
312 pub(super) fn execute_import<'a>(
314 &'a mut self,
315 path: &'a str,
316 selected_names: Option<&'a [String]>,
317 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
318 Box::pin(async move {
319 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
320
321 let stdlib_module = path
322 .strip_prefix("std/")
323 .or_else(|| (path == "observability").then_some("observability"));
324 if let Some(module) = stdlib_module {
325 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
326 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
327 if self.imported_paths.contains(&synthetic) {
328 return Ok(());
329 }
330 let loaded = self
331 .load_stdlib_module_from_source(module, synthetic.clone(), source)
332 .await?;
333 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
334 return Ok(());
335 }
336 return Err(VmError::Runtime(format!(
337 "Unknown stdlib module: std/{module}"
338 )));
339 }
340
341 let base = self
342 .source_dir
343 .clone()
344 .unwrap_or_else(|| PathBuf::from("."));
345 let file_path = resolve_module_import_path(&base, path);
346
347 let canonical = file_path
348 .canonicalize()
349 .unwrap_or_else(|_| file_path.clone());
350 if self.imported_paths.contains(&canonical) {
351 return Ok(());
352 }
353 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
354 return self.export_loaded_module(&canonical, &loaded, selected_names);
355 }
356 self.imported_paths.push(canonical.clone());
357
358 let source = std::fs::read_to_string(&file_path).map_err(|e| {
359 VmError::Runtime(format!(
364 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
365 file_path.display(),
366 base.display()
367 ))
368 })?;
369 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
370 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
371
372 let lookup = bytecode_cache::load_module(&file_path, &source);
375 let artifact = if let Some(artifact) = lookup.artifact {
376 artifact
377 } else {
378 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
379 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
380 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
381 eprintln!(
382 "[harn] module cache write skipped for {}: {err}",
383 file_path.display()
384 );
385 }
386 }
387 compiled
388 };
389
390 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
391 let loaded = self
392 .instantiate_module(module_source_dir, &artifact)
393 .await?;
394 self.imported_paths.pop();
395 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
396 self.export_loaded_module(&canonical, &loaded, selected_names)?;
397
398 Ok(())
399 })
400 }
401
402 fn cache_key_for_import(&self, path: &str) -> PathBuf {
407 if let Some(module) = path
408 .strip_prefix("std/")
409 .or_else(|| (path == "observability").then_some("observability"))
410 {
411 return PathBuf::from(format!("<stdlib>/{module}.harn"));
412 }
413 let base = self
414 .source_dir
415 .clone()
416 .unwrap_or_else(|| PathBuf::from("."));
417 let file_path = resolve_module_import_path(&base, path);
418 file_path.canonicalize().unwrap_or(file_path)
419 }
420
421 pub async fn load_module_exports(
424 &mut self,
425 path: &Path,
426 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
427 let path_str = path.to_string_lossy().into_owned();
428 self.execute_import(&path_str, None).await?;
429
430 let mut file_path = if path.is_absolute() {
431 path.to_path_buf()
432 } else {
433 self.source_dir
434 .clone()
435 .unwrap_or_else(|| PathBuf::from("."))
436 .join(path)
437 };
438 if !file_path.exists() && file_path.extension().is_none() {
439 file_path.set_extension("harn");
440 }
441
442 let canonical = file_path
443 .canonicalize()
444 .unwrap_or_else(|_| file_path.clone());
445 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
446 VmError::Runtime(format!(
447 "Import error: failed to cache loaded module '{}'",
448 canonical.display()
449 ))
450 })?;
451
452 let export_names: Vec<String> = if loaded.public_names.is_empty() {
453 loaded.functions.keys().cloned().collect()
454 } else {
455 loaded.public_names.iter().cloned().collect()
456 };
457
458 let mut exports = BTreeMap::new();
459 for name in export_names {
460 let Some(closure) = loaded.functions.get(&name) else {
461 return Err(VmError::Runtime(format!(
462 "Import error: exported function '{name}' is missing from {}",
463 canonical.display()
464 )));
465 };
466 exports.insert(name, Arc::clone(closure));
467 }
468
469 Ok(exports)
470 }
471
472 pub async fn load_module_exports_from_source(
475 &mut self,
476 source_key: impl Into<PathBuf>,
477 source: &str,
478 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
479 let synthetic = source_key.into();
480 let loaded = self
481 .load_module_from_source(synthetic.clone(), source)
482 .await?;
483 let export_names: Vec<String> = if loaded.public_names.is_empty() {
484 loaded.functions.keys().cloned().collect()
485 } else {
486 loaded.public_names.iter().cloned().collect()
487 };
488
489 let mut exports = BTreeMap::new();
490 for name in export_names {
491 let Some(closure) = loaded.functions.get(&name) else {
492 return Err(VmError::Runtime(format!(
493 "Import error: exported function '{name}' is missing from {}",
494 synthetic.display()
495 )));
496 };
497 exports.insert(name, Arc::clone(closure));
498 }
499
500 Ok(exports)
501 }
502
503 pub async fn load_module_exports_from_import(
507 &mut self,
508 import_path: &str,
509 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
510 self.execute_import(import_path, None).await?;
511
512 if let Some(module) = import_path
513 .strip_prefix("std/")
514 .or_else(|| (import_path == "observability").then_some("observability"))
515 {
516 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
517 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
518 VmError::Runtime(format!(
519 "Import error: failed to cache loaded module '{}'",
520 synthetic.display()
521 ))
522 })?;
523 let mut exports = BTreeMap::new();
524 let export_names: Vec<String> = if loaded.public_names.is_empty() {
525 loaded.functions.keys().cloned().collect()
526 } else {
527 loaded.public_names.iter().cloned().collect()
528 };
529 for name in export_names {
530 let Some(closure) = loaded.functions.get(&name) else {
531 return Err(VmError::Runtime(format!(
532 "Import error: exported function '{name}' is missing from {}",
533 synthetic.display()
534 )));
535 };
536 exports.insert(name, Arc::clone(closure));
537 }
538 return Ok(exports);
539 }
540
541 let base = self
542 .source_dir
543 .clone()
544 .unwrap_or_else(|| PathBuf::from("."));
545 let file_path = resolve_module_import_path(&base, import_path);
546 self.load_module_exports(&file_path).await
547 }
548}
549
550#[cfg(test)]
551mod tests {
552
553 use std::sync::{Mutex, MutexGuard, OnceLock};
554
555 use super::*;
556
557 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
558
559 fn cache_test_guard() -> MutexGuard<'static, ()> {
560 CACHE_TEST_LOCK
561 .get_or_init(|| Mutex::new(()))
562 .lock()
563 .unwrap()
564 }
565
566 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
567 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
568 stdlib_module_artifact_cache_ptr(module, source)
569 }
570
571 #[test]
572 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
573 let _guard = cache_test_guard();
574 reset_stdlib_module_artifact_cache();
575 let runtime = tokio::runtime::Builder::new_current_thread()
576 .enable_all()
577 .build()
578 .expect("runtime builds");
579
580 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
581 .block_on(async {
582 let mut first_vm = Vm::new();
583 let first_exports = first_vm
584 .load_module_exports_from_import("std/agent/prompts")
585 .await
586 .expect("first stdlib import succeeds");
587 let first_state = first_exports
588 .get("render_agent_prompt")
589 .expect("first export exists")
590 .module_state()
591 .expect("first module state stays live while VM owns module");
592 let first_state_weak = Arc::downgrade(&first_state);
593 let first_state_ptr = Arc::as_ptr(&first_state);
594
595 let mut second_vm = Vm::new();
596 let second_exports = second_vm
597 .load_module_exports_from_import("std/agent/prompts")
598 .await
599 .expect("second stdlib import succeeds");
600 let second_state = second_exports
601 .get("render_agent_prompt")
602 .expect("second export exists")
603 .module_state()
604 .expect("second module state stays live while VM owns module");
605 let second_state_weak = Arc::downgrade(&second_state);
606
607 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
608 (
609 first_exports,
610 second_exports,
611 first_state_weak,
612 second_state_weak,
613 )
614 });
615 let first_cached =
616 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
617 assert_eq!(
618 cached_stdlib_module_ptr("agent/prompts"),
619 Some(first_cached)
620 );
621
622 let first = first_exports
623 .get("render_agent_prompt")
624 .expect("first export exists");
625 let second = second_exports
626 .get("render_agent_prompt")
627 .expect("second export exists");
628
629 assert!(!Arc::ptr_eq(first, second));
630 assert!(!Arc::ptr_eq(&first.func, &second.func));
631 assert!(!Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
632 assert!(first.module_state().is_none());
633 assert!(second.module_state().is_none());
634 assert!(first_state_weak.upgrade().is_none());
635 assert!(second_state_weak.upgrade().is_none());
636 }
637
638 #[test]
639 fn stdlib_artifact_cache_is_process_wide_across_threads() {
640 let _guard = cache_test_guard();
641 reset_stdlib_module_artifact_cache();
642
643 let handle = std::thread::spawn(|| {
644 let runtime = tokio::runtime::Builder::new_current_thread()
645 .enable_all()
646 .build()
647 .expect("runtime builds");
648 runtime.block_on(async {
649 let mut vm = Vm::new();
650 vm.load_module_exports_from_import("std/agent/prompts")
651 .await
652 .expect("thread stdlib import succeeds");
653 });
654 });
655 handle.join().expect("thread joins");
656 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
657 .expect("thread import cached stdlib artifact");
658
659 let runtime = tokio::runtime::Builder::new_current_thread()
660 .enable_all()
661 .build()
662 .expect("runtime builds");
663 runtime.block_on(async {
664 let mut vm = Vm::new();
665 vm.load_module_exports_from_import("std/agent/prompts")
666 .await
667 .expect("main-thread stdlib import succeeds");
668 });
669 assert_eq!(
670 cached_stdlib_module_ptr("agent/prompts"),
671 Some(thread_cached)
672 );
673 }
674
675 #[test]
676 fn module_closures_release_state_after_vm_drop() {
677 let runtime = tokio::runtime::Builder::new_current_thread()
678 .enable_all()
679 .build()
680 .expect("runtime builds");
681
682 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
683 let mut vm = Vm::new();
684 let loaded = vm
685 .load_module_from_source(
686 PathBuf::from("<test>/module_cycle.harn"),
687 r#"
688var payload = "x" * 1024
689
690pub fn touch() {
691 return len(payload)
692}
693"#,
694 )
695 .await
696 .expect("module loads");
697 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
698 let closure_weak = Arc::downgrade(&closure);
699 let registry_weak = Arc::downgrade(&loaded._module_functions);
700 let state_weak = Arc::downgrade(&loaded._module_state);
701
702 drop(closure);
703 drop(loaded);
704 drop(vm);
705
706 (closure_weak, registry_weak, state_weak)
707 });
708
709 assert!(
710 closure_weak.upgrade().is_none(),
711 "module closure should drop with its VM"
712 );
713 assert!(
714 registry_weak.upgrade().is_none(),
715 "module function registry should drop with its VM"
716 );
717 assert!(
718 state_weak.upgrade().is_none(),
719 "module state should drop with its VM"
720 );
721 }
722}