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_values: BTreeMap<String, VmValue>,
46 pub(crate) public_type_names: HashSet<String>,
49 pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
53 pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
54 pub(crate) _module_state: crate::value::ModuleState,
55}
56
57#[derive(Clone, Debug)]
63pub(crate) struct DeferredCyclicImport {
64 pub(crate) importer: PathBuf,
66 pub(crate) target: PathBuf,
68 pub(crate) selected_names: Option<Vec<String>>,
70}
71
72pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
73 let synthetic_current_file = base.join("__harn_import_base__.harn");
74 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
75 return resolved;
76 }
77
78 let mut file_path = base.join(path);
79
80 if !file_path.exists() && file_path.extension().is_none() {
81 file_path.set_extension("harn");
82 }
83
84 file_path
85}
86
87fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
88 let mut hasher = std::collections::hash_map::DefaultHasher::new();
89 module.hash(&mut hasher);
90 source.hash(&mut hasher);
91 format!("{module}:{:016x}", hasher.finish())
92}
93
94fn stdlib_module_artifact(
95 module: &str,
96 synthetic: &Path,
97 source: &'static str,
98) -> Result<Arc<ModuleArtifact>, VmError> {
99 let key = stdlib_artifact_cache_key(module, source);
100 {
101 let cache = stdlib_module_artifact_cache().lock().unwrap();
102 if let Some(cached) = cache.get(&key) {
103 return Ok(Arc::clone(cached));
104 }
105 }
106
107 let lookup = bytecode_cache::load_module(synthetic, source);
112 let artifact = if let Some(artifact) = lookup.artifact {
113 artifact
114 } else {
115 let compiled = compile_module_artifact_from_source(synthetic, source)?;
116 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
117 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
118 eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
119 }
120 }
121 compiled
122 };
123
124 let compiled = Arc::new(artifact);
125 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
126 if let Some(cached) = cache.get(&key) {
127 return Ok(Arc::clone(cached));
128 }
129 cache.insert(key, Arc::clone(&compiled));
130 Ok(compiled)
131}
132
133impl Vm {
134 pub async fn resolve_callable(
138 &mut self,
139 callable: &crate::value::VmCallable,
140 ) -> Result<Arc<crate::value::VmClosure>, VmError> {
141 self.ensure_execution_available()?;
142 match callable {
143 crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
144 crate::value::VmCallable::Lazy(lazy) => {
145 let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
146 let resolution = {
147 let mut modules = self.lazy_callable_modules.lock();
148 Arc::clone(
149 modules
150 .entry(cache_key)
151 .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new())),
152 )
153 };
154 let resolved = resolution
155 .get_or_try_init(|| async {
156 let exports = self.load_module_exports(&module_path).await?;
157 let exports = exports
158 .into_iter()
159 .map(|(name, closure)| (name, closure.retained_for_host_registry()))
160 .collect();
161 Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
166 exports,
167 retained_module_graph: Arc::clone(&self.module_cache),
168 }))
169 })
170 .await?;
171 resolved
172 .exports
173 .get(&lazy.function_name)
174 .cloned()
175 .ok_or_else(|| {
176 VmError::Runtime(format!(
177 "function '{}' is not exported by module '{}'",
178 lazy.function_name,
179 lazy.module_path.display()
180 ))
181 })
182 }
183 }
184 }
185
186 fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
187 let mut module_path = if lazy.module_path.is_absolute() {
188 lazy.module_path.clone()
189 } else {
190 self.source_dir
191 .clone()
192 .unwrap_or_else(|| PathBuf::from("."))
193 .join(&lazy.module_path)
194 };
195 if !module_path.exists() && module_path.extension().is_none() {
196 module_path.set_extension("harn");
197 }
198 let cache_key = module_path
199 .canonicalize()
200 .unwrap_or_else(|_| module_path.clone());
201 (cache_key, module_path)
202 }
203
204 async fn load_module_from_source(
205 &mut self,
206 synthetic: PathBuf,
207 source: &str,
208 ) -> Result<LoadedModule, VmError> {
209 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
210 return Ok(loaded);
211 }
212 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
213
214 let artifact = compile_module_artifact_from_source(&synthetic, source)?;
215
216 self.imported_paths.push(synthetic.clone());
217 let loaded = self.instantiate_module(None, &artifact).await?;
218 self.imported_paths.pop();
219 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
220 Ok(loaded)
221 }
222
223 async fn load_stdlib_module_from_source(
224 &mut self,
225 module: &str,
226 synthetic: PathBuf,
227 source: &'static str,
228 ) -> Result<LoadedModule, VmError> {
229 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
230 return Ok(loaded);
231 }
232 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
233
234 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
235 self.imported_paths.push(synthetic.clone());
236 let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
237 self.imported_paths.pop();
238 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
239 Ok(loaded)
240 }
241
242 async fn instantiate_stdlib_module(
243 &mut self,
244 artifact: &ModuleArtifact,
245 ) -> Result<LoadedModule, VmError> {
246 self.instantiate_module(None, artifact).await
247 }
248
249 async fn instantiate_module(
257 &mut self,
258 module_source_dir: Option<PathBuf>,
259 artifact: &ModuleArtifact,
260 ) -> Result<LoadedModule, VmError> {
261 let caller_env = self.env.clone();
262 let old_source_dir = self.source_dir.clone();
263 self.env = VmEnv::new();
264 self.source_dir = module_source_dir.clone();
265
266 for import in &artifact.imports {
267 self.execute_import(&import.path, import.selected_names.as_deref())
268 .await?;
269 }
270
271 let module_state: crate::value::ModuleState = {
272 let mut init_env = self.env.clone();
273 if let Some(init_chunk) = &artifact.init_chunk {
274 let fresh_init_chunk = Chunk::from_cached(init_chunk);
275 let saved_env = std::mem::replace(&mut self.env, init_env);
276 let saved_frames = std::mem::take(&mut self.frames);
277 let saved_handlers = std::mem::take(&mut self.exception_handlers);
278 let saved_iterators = std::mem::take(&mut self.iterators);
279 let saved_deadlines = std::mem::take(&mut self.deadlines);
280 let active_context = crate::step_runtime::take_active_context();
291 let init_result = self.run_chunk(std::sync::Arc::new(fresh_init_chunk)).await;
292 crate::step_runtime::restore_active_context(active_context);
293 init_env = std::mem::replace(&mut self.env, saved_env);
294 self.frames = saved_frames;
295 self.exception_handlers = saved_handlers;
296 self.iterators = saved_iterators;
297 self.deadlines = saved_deadlines;
298 init_result?;
299 }
300 Arc::new(crate::value::VmMutex::new(init_env))
301 };
302
303 let module_env = self.env.clone();
304 let registry: ModuleFunctionRegistry =
305 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
306 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
307 let mut public_names = artifact.public_names.clone();
308 let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
314 {
315 let state = module_state.lock();
316 for name in &artifact.public_value_names {
317 if let Some(value) = state.get(name) {
318 public_values.insert(name.clone(), value);
319 public_names.insert(name.clone());
320 }
321 }
322 }
323 let mut public_type_names = artifact.public_type_names.clone();
324 let mut public_type_schemas: BTreeMap<String, VmValue> = artifact
325 .public_type_schemas
326 .iter()
327 .filter_map(|(name, json)| {
328 let parsed = serde_json::from_str::<serde_json::Value>(json).ok()?;
329 Some((name.clone(), crate::schema::json_to_vm_value(&parsed)))
330 })
331 .collect();
332
333 for (name, compiled) in &artifact.functions {
334 let closure = Arc::new(VmClosure {
335 func: Arc::new(CompiledFunction::from_cached(compiled)),
336 env: module_env.clone(),
337 source_dir: module_source_dir.clone(),
338 module_functions: Some(Arc::downgrade(®istry)),
339 module_state: Some(Arc::downgrade(&module_state)),
340 retained_module_scope: None,
341 });
342 registry.lock().insert(name.clone(), Arc::clone(&closure));
343 self.env
344 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
345 module_state
346 .lock()
347 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
348 functions.insert(name.clone(), Arc::clone(&closure));
349 }
350
351 for import in artifact.imports.iter().filter(|import| import.is_pub) {
352 let cache_key = self.cache_key_for_import(&import.path);
353 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
354 if self.imported_paths.contains(&cache_key) {
361 return Err(VmError::Runtime(format!(
362 "Re-export error: cannot `pub import` from '{}' because it forms an \
363 import cycle with this module (its public surface is still being \
364 built). Use a plain `import` here, or re-export from a module that is \
365 not part of the cycle.",
366 import.path
367 )));
368 }
369 return Err(VmError::Runtime(format!(
370 "Re-export error: imported module '{}' was not loaded",
371 import.path
372 )));
373 };
374 let names_to_reexport: Vec<String> = match &import.selected_names {
375 Some(names) => names.clone(),
376 None => loaded
380 .public_names
381 .iter()
382 .chain(loaded.public_type_names.iter())
383 .cloned()
384 .collect(),
385 };
386 for name in names_to_reexport {
387 let Some(closure) = loaded.functions.get(&name) else {
388 if let Some(value) = loaded.public_values.get(&name) {
391 public_values.insert(name.clone(), value.clone());
392 public_names.insert(name);
393 continue;
394 }
395 if loaded.public_type_names.contains(&name) {
399 if let Some(schema) = loaded.public_type_schemas.get(&name) {
400 public_type_schemas.insert(name.clone(), schema.clone());
401 }
402 public_type_names.insert(name);
403 continue;
404 }
405 return Err(VmError::Runtime(format!(
406 "Re-export error: '{name}' is not exported by '{}'",
407 import.path
408 )));
409 };
410 if let Some(existing) = functions.get(&name) {
411 if !Arc::ptr_eq(existing, closure) {
412 return Err(VmError::Runtime(format!(
413 "Re-export collision: '{name}' is defined here and also \
414 re-exported from '{}'",
415 import.path
416 )));
417 }
418 }
419 functions.insert(name.clone(), Arc::clone(closure));
420 public_names.insert(name);
421 }
422 }
423
424 self.env = caller_env;
425 self.source_dir = old_source_dir;
426
427 Ok(LoadedModule {
428 functions,
429 public_names,
430 public_values,
431 public_type_names,
432 public_type_schemas,
433 _module_functions: registry,
434 _module_state: module_state,
435 })
436 }
437
438 fn export_loaded_module(
439 &mut self,
440 module_path: &Path,
441 loaded: &LoadedModule,
442 selected_names: Option<&[String]>,
443 ) -> Result<(), VmError> {
444 let module_name = module_path.display().to_string();
445 let export_names: Vec<String> = if let Some(names) = selected_names {
446 for name in names {
452 if !loaded.public_names.contains(name) && !loaded.public_type_names.contains(name) {
453 let hint = if loaded.functions.contains_key(name) {
454 " — it is defined there but not `pub`; mark it `pub` to export it"
455 } else {
456 ""
457 };
458 return Err(VmError::Runtime(format!(
459 "Import error: '{name}' is not exported by {module_name}{hint}"
460 )));
461 }
462 }
463 names.to_vec()
464 } else {
465 loaded
468 .public_names
469 .iter()
470 .chain(loaded.public_type_names.iter())
471 .cloned()
472 .collect()
473 };
474
475 for name in export_names {
476 if loaded.public_type_names.contains(&name) && !loaded.functions.contains_key(&name) {
482 if let Some(schema) = loaded.public_type_schemas.get(&name) {
483 self.env.define(&name, schema.clone(), false)?;
484 }
485 continue;
486 }
487 if let Some(value) = loaded.public_values.get(&name) {
489 if self.env.get(&name).is_some() {
490 return Err(VmError::Runtime(format!(
491 "Import collision: '{name}' is already defined when importing \
492 {module_name}. Use selective imports to disambiguate: \
493 import {{ {name} }} from \"...\""
494 )));
495 }
496 self.env.define(&name, value.clone(), false)?;
497 continue;
498 }
499 let Some(closure) = loaded.functions.get(&name) else {
500 return Err(VmError::Runtime(format!(
501 "Import error: '{name}' is not defined in {module_name}"
502 )));
503 };
504 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
505 return Err(VmError::Runtime(format!(
506 "Import collision: '{name}' is already defined when importing {module_name}. \
507 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
508 )));
509 }
510 self.env
511 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
512 }
513 Ok(())
514 }
515
516 pub(super) fn execute_import<'a>(
518 &'a mut self,
519 path: &'a str,
520 selected_names: Option<&'a [String]>,
521 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
522 Box::pin(async move {
523 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
524
525 let stdlib_module = path
526 .strip_prefix("std/")
527 .or_else(|| (path == "observability").then_some("observability"));
528 if let Some(module) = stdlib_module {
529 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
530 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
531 if self.imported_paths.contains(&synthetic) {
532 return Ok(());
533 }
534 let loaded = self
535 .load_stdlib_module_from_source(module, synthetic.clone(), source)
536 .await?;
537 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
538 return Ok(());
539 }
540 return Err(VmError::Runtime(format!(
541 "Unknown stdlib module: std/{module}"
542 )));
543 }
544
545 let base = self
546 .source_dir
547 .clone()
548 .unwrap_or_else(|| PathBuf::from("."));
549 let file_path = resolve_module_import_path(&base, path);
550
551 let canonical = file_path
552 .canonicalize()
553 .unwrap_or_else(|_| file_path.clone());
554 if self.imported_paths.contains(&canonical) {
555 if let Some(importer) = self.imported_paths.last().cloned() {
563 if importer != canonical {
564 self.deferred_cyclic_imports.push(DeferredCyclicImport {
565 importer,
566 target: canonical.clone(),
567 selected_names: selected_names.map(<[String]>::to_vec),
568 });
569 }
570 }
571 return Ok(());
572 }
573 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
574 return self.export_loaded_module(&canonical, &loaded, selected_names);
575 }
576 self.imported_paths.push(canonical.clone());
577
578 let source = std::fs::read_to_string(&file_path).map_err(|e| {
579 VmError::Runtime(format!(
584 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
585 file_path.display(),
586 base.display()
587 ))
588 })?;
589 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
590 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
591
592 let lookup = bytecode_cache::load_module(&file_path, &source);
595 let artifact = if let Some(artifact) = lookup.artifact {
596 artifact
597 } else {
598 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
599 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
600 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
601 eprintln!(
602 "[harn] module cache write skipped for {}: {err}",
603 file_path.display()
604 );
605 }
606 }
607 compiled
608 };
609
610 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
611 let loaded = self
612 .instantiate_module(module_source_dir, &artifact)
613 .await?;
614 self.imported_paths.pop();
615 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
616 self.export_loaded_module(&canonical, &loaded, selected_names)?;
617
618 if self.imported_paths.is_empty() {
622 self.flush_deferred_cyclic_imports()?;
623 }
624
625 Ok(())
626 })
627 }
628
629 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
638 if self.deferred_cyclic_imports.is_empty() {
639 return Ok(());
640 }
641 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
642 let mut still_pending = Vec::new();
643 for import in deferred {
644 let (Some(importer), Some(target)) = (
645 self.module_cache.get(&import.importer).cloned(),
646 self.module_cache.get(&import.target).cloned(),
647 ) else {
648 still_pending.push(import);
652 continue;
653 };
654
655 let export_names: Vec<String> = match &import.selected_names {
656 Some(names) => names.clone(),
657 None if !target.public_names.is_empty() => {
658 target.public_names.iter().cloned().collect()
659 }
660 None => target.functions.keys().cloned().collect(),
661 };
662
663 let mut module_state = importer._module_state.lock();
664 for name in export_names {
665 if module_state.get(&name).is_some() {
668 continue;
669 }
670 if let Some(closure) = target.functions.get(&name) {
671 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
672 } else if let Some(value) = target.public_values.get(&name) {
673 module_state.define(&name, value.clone(), false)?;
675 } else {
676 return Err(VmError::Runtime(format!(
677 "Import error: '{name}' is not defined in {}",
678 import.target.display()
679 )));
680 }
681 }
682 }
683 self.deferred_cyclic_imports = still_pending;
684 Ok(())
685 }
686
687 fn cache_key_for_import(&self, path: &str) -> PathBuf {
692 if let Some(module) = path
693 .strip_prefix("std/")
694 .or_else(|| (path == "observability").then_some("observability"))
695 {
696 return PathBuf::from(format!("<stdlib>/{module}.harn"));
697 }
698 let base = self
699 .source_dir
700 .clone()
701 .unwrap_or_else(|| PathBuf::from("."));
702 let file_path = resolve_module_import_path(&base, path);
703 file_path.canonicalize().unwrap_or(file_path)
704 }
705
706 pub async fn load_module_exports(
709 &mut self,
710 path: &Path,
711 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
712 self.ensure_execution_available()?;
713 let path_str = path.to_string_lossy().into_owned();
714 self.execute_import(&path_str, None).await?;
715
716 let mut file_path = if path.is_absolute() {
717 path.to_path_buf()
718 } else {
719 self.source_dir
720 .clone()
721 .unwrap_or_else(|| PathBuf::from("."))
722 .join(path)
723 };
724 if !file_path.exists() && file_path.extension().is_none() {
725 file_path.set_extension("harn");
726 }
727
728 let canonical = file_path
729 .canonicalize()
730 .unwrap_or_else(|_| file_path.clone());
731 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
732 VmError::Runtime(format!(
733 "Import error: failed to cache loaded module '{}'",
734 canonical.display()
735 ))
736 })?;
737
738 let export_names: Vec<String> = if loaded.public_names.is_empty() {
739 loaded.functions.keys().cloned().collect()
740 } else {
741 loaded.public_names.iter().cloned().collect()
742 };
743
744 let mut exports = BTreeMap::new();
745 for name in export_names {
746 let Some(closure) = loaded.functions.get(&name) else {
747 return Err(VmError::Runtime(format!(
748 "Import error: exported function '{name}' is missing from {}",
749 canonical.display()
750 )));
751 };
752 exports.insert(name, Arc::clone(closure));
753 }
754
755 Ok(exports)
756 }
757
758 pub async fn load_module_exports_from_source(
761 &mut self,
762 source_key: impl Into<PathBuf>,
763 source: &str,
764 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
765 self.ensure_execution_available()?;
766 let synthetic = source_key.into();
767 let loaded = self
768 .load_module_from_source(synthetic.clone(), source)
769 .await?;
770 let export_names: Vec<String> = if loaded.public_names.is_empty() {
771 loaded.functions.keys().cloned().collect()
772 } else {
773 loaded.public_names.iter().cloned().collect()
774 };
775
776 let mut exports = BTreeMap::new();
777 for name in export_names {
778 let Some(closure) = loaded.functions.get(&name) else {
779 return Err(VmError::Runtime(format!(
780 "Import error: exported function '{name}' is missing from {}",
781 synthetic.display()
782 )));
783 };
784 exports.insert(name, Arc::clone(closure));
785 }
786
787 Ok(exports)
788 }
789
790 pub async fn load_module_exports_from_import(
794 &mut self,
795 import_path: &str,
796 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
797 self.ensure_execution_available()?;
798 self.execute_import(import_path, None).await?;
799
800 if let Some(module) = import_path
801 .strip_prefix("std/")
802 .or_else(|| (import_path == "observability").then_some("observability"))
803 {
804 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
805 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
806 VmError::Runtime(format!(
807 "Import error: failed to cache loaded module '{}'",
808 synthetic.display()
809 ))
810 })?;
811 let mut exports = BTreeMap::new();
812 let export_names: Vec<String> = if loaded.public_names.is_empty() {
813 loaded.functions.keys().cloned().collect()
814 } else {
815 loaded.public_names.iter().cloned().collect()
816 };
817 for name in export_names {
818 let Some(closure) = loaded.functions.get(&name) else {
819 return Err(VmError::Runtime(format!(
820 "Import error: exported function '{name}' is missing from {}",
821 synthetic.display()
822 )));
823 };
824 exports.insert(name, Arc::clone(closure));
825 }
826 return Ok(exports);
827 }
828
829 let base = self
830 .source_dir
831 .clone()
832 .unwrap_or_else(|| PathBuf::from("."));
833 let file_path = resolve_module_import_path(&base, import_path);
834 self.load_module_exports(&file_path).await
835 }
836}
837
838#[cfg(test)]
839mod tests {
840
841 use std::sync::{Mutex, MutexGuard, OnceLock};
842
843 use super::*;
844
845 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
846
847 fn cache_test_guard() -> MutexGuard<'static, ()> {
848 CACHE_TEST_LOCK
849 .get_or_init(|| Mutex::new(()))
850 .lock()
851 .unwrap()
852 }
853
854 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
855 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
856 stdlib_module_artifact_cache_ptr(module, source)
857 }
858
859 #[test]
860 fn module_function_can_use_local_type_alias_as_schema_value() {
861 let runtime = tokio::runtime::Builder::new_current_thread()
862 .enable_all()
863 .build()
864 .expect("runtime builds");
865
866 let result = runtime.block_on(async {
867 let mut vm = Vm::new();
868 crate::stdlib::register_vm_stdlib(&mut vm);
869 let loaded = vm
870 .load_module_from_source(
871 PathBuf::from("<test>/schema_alias_module.harn"),
872 r#"
873fn accepts_schema(schema) {
874 return schema_report({name: "Ada"}, schema).ok
875}
876
877type UserShape = {name: string}
878
879pub fn works() {
880 return accepts_schema(UserShape)
881}
882"#,
883 )
884 .await
885 .expect("module loads");
886 let closure = Arc::clone(loaded.functions.get("works").expect("works export exists"));
887 vm.call_closure_pub(&closure, &[])
888 .await
889 .expect("module closure executes")
890 });
891
892 assert!(matches!(result, VmValue::Bool(true)), "{result:?}");
893 }
894
895 #[test]
896 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
897 let _guard = cache_test_guard();
898 reset_stdlib_module_artifact_cache();
899 let runtime = tokio::runtime::Builder::new_current_thread()
900 .enable_all()
901 .build()
902 .expect("runtime builds");
903
904 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
905 .block_on(async {
906 let mut first_vm = Vm::new();
907 let first_exports = first_vm
908 .load_module_exports_from_import("std/agent/prompts")
909 .await
910 .expect("first stdlib import succeeds");
911 let first_state = first_exports
912 .get("render_agent_prompt")
913 .expect("first export exists")
914 .module_state()
915 .expect("first module state stays live while VM owns module");
916 let first_state_weak = Arc::downgrade(&first_state);
917 let first_state_ptr = Arc::as_ptr(&first_state);
918
919 let mut second_vm = Vm::new();
920 let second_exports = second_vm
921 .load_module_exports_from_import("std/agent/prompts")
922 .await
923 .expect("second stdlib import succeeds");
924 let second_state = second_exports
925 .get("render_agent_prompt")
926 .expect("second export exists")
927 .module_state()
928 .expect("second module state stays live while VM owns module");
929 let second_state_weak = Arc::downgrade(&second_state);
930
931 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
932 (
933 first_exports,
934 second_exports,
935 first_state_weak,
936 second_state_weak,
937 )
938 });
939 let first_cached =
940 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
941 assert_eq!(
942 cached_stdlib_module_ptr("agent/prompts"),
943 Some(first_cached)
944 );
945
946 let first = first_exports
947 .get("render_agent_prompt")
948 .expect("first export exists");
949 let second = second_exports
950 .get("render_agent_prompt")
951 .expect("second export exists");
952
953 assert!(!Arc::ptr_eq(first, second));
954 assert!(!Arc::ptr_eq(&first.func, &second.func));
955 assert!(!Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
956 assert!(first.module_state().is_none());
957 assert!(second.module_state().is_none());
958 assert!(first_state_weak.upgrade().is_none());
959 assert!(second_state_weak.upgrade().is_none());
960 }
961
962 #[test]
963 fn stdlib_artifact_cache_is_process_wide_across_threads() {
964 let _guard = cache_test_guard();
965 reset_stdlib_module_artifact_cache();
966
967 let handle = std::thread::spawn(|| {
968 let runtime = tokio::runtime::Builder::new_current_thread()
969 .enable_all()
970 .build()
971 .expect("runtime builds");
972 runtime.block_on(async {
973 let mut vm = Vm::new();
974 vm.load_module_exports_from_import("std/agent/prompts")
975 .await
976 .expect("thread stdlib import succeeds");
977 });
978 });
979 handle.join().expect("thread joins");
980 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
981 .expect("thread import cached stdlib artifact");
982
983 let runtime = tokio::runtime::Builder::new_current_thread()
984 .enable_all()
985 .build()
986 .expect("runtime builds");
987 runtime.block_on(async {
988 let mut vm = Vm::new();
989 vm.load_module_exports_from_import("std/agent/prompts")
990 .await
991 .expect("main-thread stdlib import succeeds");
992 });
993 assert_eq!(
994 cached_stdlib_module_ptr("agent/prompts"),
995 Some(thread_cached)
996 );
997 }
998
999 #[test]
1000 fn module_closures_release_state_after_vm_drop() {
1001 let runtime = tokio::runtime::Builder::new_current_thread()
1002 .enable_all()
1003 .build()
1004 .expect("runtime builds");
1005
1006 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
1007 let mut vm = Vm::new();
1008 let loaded = vm
1009 .load_module_from_source(
1010 PathBuf::from("<test>/module_cycle.harn"),
1011 r#"
1012let payload = "x" * 1024
1013
1014pub fn touch() {
1015 return len(payload)
1016}
1017"#,
1018 )
1019 .await
1020 .expect("module loads");
1021 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
1022 let closure_weak = Arc::downgrade(&closure);
1023 let registry_weak = Arc::downgrade(&loaded._module_functions);
1024 let state_weak = Arc::downgrade(&loaded._module_state);
1025
1026 drop(closure);
1027 drop(loaded);
1028 drop(vm);
1029
1030 (closure_weak, registry_weak, state_weak)
1031 });
1032
1033 assert!(
1034 closure_weak.upgrade().is_none(),
1035 "module closure should drop with its VM"
1036 );
1037 assert!(
1038 registry_weak.upgrade().is_none(),
1039 "module function registry should drop with its VM"
1040 );
1041 assert!(
1042 state_weak.upgrade().is_none(),
1043 "module state should drop with its VM"
1044 );
1045 }
1046}