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::module_artifact::compile_module_artifact_from_source;
10use crate::prepared_module::PreparedModuleArtifact;
11use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
12
13use super::{ScopeSpan, Vm};
14
15static STDLIB_MODULE_ARTIFACT_CACHE: OnceLock<
16 Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>>,
17> = OnceLock::new();
18
19fn stdlib_module_artifact_cache() -> &'static Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>> {
20 STDLIB_MODULE_ARTIFACT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
21}
22
23#[cfg(test)]
24fn reset_stdlib_module_artifact_cache() {
25 stdlib_module_artifact_cache().lock().unwrap().clear();
26}
27
28#[cfg(test)]
29fn stdlib_module_artifact_cache_ptr(module: &str, source: &str) -> Option<usize> {
30 let key = stdlib_artifact_cache_key(module, source);
31 stdlib_module_artifact_cache()
32 .lock()
33 .unwrap()
34 .get(&key)
35 .map(|artifact| Arc::as_ptr(artifact) as usize)
36}
37
38#[derive(Clone)]
39pub(crate) struct LoadedModule {
40 pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
41 pub(crate) public_names: HashSet<String>,
42 pub(crate) public_values: BTreeMap<String, VmValue>,
47 pub(crate) public_type_names: HashSet<String>,
50 pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
54 pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
55 pub(crate) _module_state: crate::value::ModuleState,
56}
57
58#[derive(Clone, Debug)]
64pub(crate) struct DeferredCyclicImport {
65 pub(crate) importer: PathBuf,
67 pub(crate) target: PathBuf,
69 pub(crate) selected_names: Option<Vec<String>>,
71}
72
73pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
74 let synthetic_current_file = base.join("__harn_import_base__.harn");
75 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
76 return resolved;
77 }
78
79 let mut file_path = base.join(path);
80
81 if !file_path.exists() && file_path.extension().is_none() {
82 file_path.set_extension("harn");
83 }
84
85 file_path
86}
87
88fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
89 let mut hasher = std::collections::hash_map::DefaultHasher::new();
90 module.hash(&mut hasher);
91 source.hash(&mut hasher);
92 format!("{module}:{:016x}", hasher.finish())
93}
94
95fn stdlib_module_artifact(
96 module: &str,
97 synthetic: &Path,
98 source: &'static str,
99) -> Result<Arc<PreparedModuleArtifact>, VmError> {
100 let key = stdlib_artifact_cache_key(module, source);
101 {
102 let cache = stdlib_module_artifact_cache().lock().unwrap();
103 if let Some(cached) = cache.get(&key) {
104 return Ok(Arc::clone(cached));
105 }
106 }
107
108 let lookup = bytecode_cache::load_module(synthetic, source);
113 let artifact = if let Some(artifact) = lookup.artifact {
114 artifact
115 } else {
116 let compiled = compile_module_artifact_from_source(synthetic, source)?;
117 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
118 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
119 eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
120 }
121 }
122 compiled
123 };
124
125 let compiled = Arc::new(PreparedModuleArtifact::from_cached(artifact));
126 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
127 if let Some(cached) = cache.get(&key) {
128 return Ok(Arc::clone(cached));
129 }
130 cache.insert(key, Arc::clone(&compiled));
131 Ok(compiled)
132}
133
134impl Vm {
135 pub async fn resolve_callable(
139 &mut self,
140 callable: &crate::value::VmCallable,
141 ) -> Result<Arc<crate::value::VmClosure>, VmError> {
142 self.ensure_execution_available()?;
143 match callable {
144 crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
145 crate::value::VmCallable::Lazy(lazy) => {
146 let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
147 let resolution = {
148 let mut modules = self.lazy_callable_modules.lock();
149 Arc::clone(
150 modules
151 .entry(cache_key)
152 .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new())),
153 )
154 };
155 let resolved = resolution
156 .get_or_try_init(|| async {
157 let exports = self.load_module_exports(&module_path).await?;
158 let exports = exports
159 .into_iter()
160 .map(|(name, closure)| (name, closure.retained_for_host_registry()))
161 .collect();
162 Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
167 exports,
168 retained_module_graph: Arc::clone(&self.module_cache),
169 }))
170 })
171 .await?;
172 resolved
173 .exports
174 .get(&lazy.function_name)
175 .cloned()
176 .ok_or_else(|| {
177 VmError::Runtime(format!(
178 "function '{}' is not exported by module '{}'",
179 lazy.function_name,
180 lazy.module_path.display()
181 ))
182 })
183 }
184 }
185 }
186
187 fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
188 let mut module_path = if lazy.module_path.is_absolute() {
189 lazy.module_path.clone()
190 } else {
191 self.source_dir
192 .clone()
193 .unwrap_or_else(|| PathBuf::from("."))
194 .join(&lazy.module_path)
195 };
196 if !module_path.exists() && module_path.extension().is_none() {
197 module_path.set_extension("harn");
198 }
199 let cache_key = module_path
200 .canonicalize()
201 .unwrap_or_else(|_| module_path.clone());
202 (cache_key, module_path)
203 }
204
205 async fn load_module_from_source(
206 &mut self,
207 synthetic: PathBuf,
208 source: &str,
209 ) -> Result<LoadedModule, VmError> {
210 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
211 return Ok(loaded);
212 }
213 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
214
215 let artifact = PreparedModuleArtifact::from_cached(compile_module_artifact_from_source(
216 &synthetic, source,
217 )?);
218
219 self.imported_paths.push(synthetic.clone());
220 let loaded = self.instantiate_module(None, &artifact).await?;
221 self.imported_paths.pop();
222 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
223 Ok(loaded)
224 }
225
226 async fn load_stdlib_module_from_source(
227 &mut self,
228 module: &str,
229 synthetic: PathBuf,
230 source: &'static str,
231 ) -> Result<LoadedModule, VmError> {
232 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
233 return Ok(loaded);
234 }
235 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
236
237 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
238 self.imported_paths.push(synthetic.clone());
239 let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
240 self.imported_paths.pop();
241 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
242 Ok(loaded)
243 }
244
245 async fn instantiate_stdlib_module(
246 &mut self,
247 artifact: &PreparedModuleArtifact,
248 ) -> Result<LoadedModule, VmError> {
249 self.instantiate_module(None, artifact).await
250 }
251
252 async fn instantiate_module(
260 &mut self,
261 module_source_dir: Option<PathBuf>,
262 artifact: &PreparedModuleArtifact,
263 ) -> Result<LoadedModule, VmError> {
264 let caller_env = self.env.clone();
265 let old_source_dir = self.source_dir.clone();
266 self.env = VmEnv::new();
267 self.source_dir = module_source_dir.clone();
268
269 for import in &artifact.imports {
270 self.execute_import(&import.path, import.selected_names.as_deref())
271 .await?;
272 }
273
274 let module_state: crate::value::ModuleState = {
275 let mut init_env = self.env.clone();
276 if let Some(init_chunk) = &artifact.init_chunk {
277 let saved_env = std::mem::replace(&mut self.env, init_env);
278 let saved_frames = std::mem::take(&mut self.frames);
279 let saved_handlers = std::mem::take(&mut self.exception_handlers);
280 let saved_iterators = std::mem::take(&mut self.iterators);
281 let saved_deadlines = std::mem::take(&mut self.deadlines);
282 let active_context = crate::step_runtime::take_active_context();
293 let init_result = self.run_chunk(Arc::clone(init_chunk)).await;
294 crate::step_runtime::restore_active_context(active_context);
295 init_env = std::mem::replace(&mut self.env, saved_env);
296 self.frames = saved_frames;
297 self.exception_handlers = saved_handlers;
298 self.iterators = saved_iterators;
299 self.deadlines = saved_deadlines;
300 init_result?;
301 }
302 Arc::new(crate::value::VmMutex::new(init_env))
303 };
304
305 let module_env = self.env.clone();
306 let registry: ModuleFunctionRegistry =
307 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
308 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
309 let mut public_names = artifact.public_names.clone();
310 let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
316 {
317 let state = module_state.lock();
318 for name in &artifact.public_value_names {
319 if let Some(value) = state.get(name) {
320 public_values.insert(name.clone(), value);
321 public_names.insert(name.clone());
322 }
323 }
324 }
325 let mut public_type_names = artifact.public_type_names.clone();
326 let mut public_type_schemas: BTreeMap<String, VmValue> = artifact
327 .public_type_schemas
328 .iter()
329 .filter_map(|(name, json)| {
330 let parsed = serde_json::from_str::<serde_json::Value>(json).ok()?;
331 Some((name.clone(), crate::schema::json_to_vm_value(&parsed)))
332 })
333 .collect();
334
335 for (name, compiled) in &artifact.functions {
336 let closure = Arc::new(VmClosure {
337 func: Arc::clone(compiled),
338 env: module_env.clone(),
339 source_dir: module_source_dir.clone(),
340 module_functions: Some(Arc::downgrade(®istry)),
341 module_state: Some(Arc::downgrade(&module_state)),
342 retained_module_scope: None,
343 });
344 registry.lock().insert(name.clone(), Arc::clone(&closure));
345 self.env
346 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
347 module_state
348 .lock()
349 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
350 functions.insert(name.clone(), Arc::clone(&closure));
351 }
352
353 for import in artifact.imports.iter().filter(|import| import.is_pub) {
354 let cache_key = self.cache_key_for_import(&import.path);
355 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
356 if self.imported_paths.contains(&cache_key) {
363 return Err(VmError::Runtime(format!(
364 "Re-export error: cannot `pub import` from '{}' because it forms an \
365 import cycle with this module (its public surface is still being \
366 built). Use a plain `import` here, or re-export from a module that is \
367 not part of the cycle.",
368 import.path
369 )));
370 }
371 return Err(VmError::Runtime(format!(
372 "Re-export error: imported module '{}' was not loaded",
373 import.path
374 )));
375 };
376 let names_to_reexport: Vec<String> = match &import.selected_names {
377 Some(names) => names.clone(),
378 None => loaded
382 .public_names
383 .iter()
384 .chain(loaded.public_type_names.iter())
385 .cloned()
386 .collect(),
387 };
388 for name in names_to_reexport {
389 let Some(closure) = loaded.functions.get(&name) else {
390 if let Some(value) = loaded.public_values.get(&name) {
393 public_values.insert(name.clone(), value.clone());
394 public_names.insert(name);
395 continue;
396 }
397 if loaded.public_type_names.contains(&name) {
401 if let Some(schema) = loaded.public_type_schemas.get(&name) {
402 public_type_schemas.insert(name.clone(), schema.clone());
403 }
404 public_type_names.insert(name);
405 continue;
406 }
407 return Err(VmError::Runtime(format!(
408 "Re-export error: '{name}' is not exported by '{}'",
409 import.path
410 )));
411 };
412 if let Some(existing) = functions.get(&name) {
413 if !Arc::ptr_eq(existing, closure) {
414 return Err(VmError::Runtime(format!(
415 "Re-export collision: '{name}' is defined here and also \
416 re-exported from '{}'",
417 import.path
418 )));
419 }
420 }
421 functions.insert(name.clone(), Arc::clone(closure));
422 public_names.insert(name);
423 }
424 }
425
426 self.env = caller_env;
427 self.source_dir = old_source_dir;
428
429 Ok(LoadedModule {
430 functions,
431 public_names,
432 public_values,
433 public_type_names,
434 public_type_schemas,
435 _module_functions: registry,
436 _module_state: module_state,
437 })
438 }
439
440 fn export_loaded_module(
441 &mut self,
442 module_path: &Path,
443 loaded: &LoadedModule,
444 selected_names: Option<&[String]>,
445 ) -> Result<(), VmError> {
446 let module_name = module_path.display().to_string();
447 let export_names: Vec<String> = if let Some(names) = selected_names {
448 for name in names {
454 if !loaded.public_names.contains(name) && !loaded.public_type_names.contains(name) {
455 let hint = if loaded.functions.contains_key(name) {
456 " — it is defined there but not `pub`; mark it `pub` to export it"
457 } else {
458 ""
459 };
460 return Err(VmError::Runtime(format!(
461 "Import error: '{name}' is not exported by {module_name}{hint}"
462 )));
463 }
464 }
465 names.to_vec()
466 } else {
467 loaded
470 .public_names
471 .iter()
472 .chain(loaded.public_type_names.iter())
473 .cloned()
474 .collect()
475 };
476
477 for name in export_names {
478 if loaded.public_type_names.contains(&name) && !loaded.functions.contains_key(&name) {
484 if let Some(schema) = loaded.public_type_schemas.get(&name) {
485 self.env.define(&name, schema.clone(), false)?;
486 }
487 continue;
488 }
489 if let Some(value) = loaded.public_values.get(&name) {
491 if self.env.get(&name).is_some() {
492 return Err(VmError::Runtime(format!(
493 "Import collision: '{name}' is already defined when importing \
494 {module_name}. Use selective imports to disambiguate: \
495 import {{ {name} }} from \"...\""
496 )));
497 }
498 self.env.define(&name, value.clone(), false)?;
499 continue;
500 }
501 let Some(closure) = loaded.functions.get(&name) else {
502 return Err(VmError::Runtime(format!(
503 "Import error: '{name}' is not defined in {module_name}"
504 )));
505 };
506 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
507 return Err(VmError::Runtime(format!(
508 "Import collision: '{name}' is already defined when importing {module_name}. \
509 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
510 )));
511 }
512 self.env
513 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
514 }
515 Ok(())
516 }
517
518 pub(super) fn execute_import<'a>(
520 &'a mut self,
521 path: &'a str,
522 selected_names: Option<&'a [String]>,
523 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
524 Box::pin(async move {
525 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
526
527 let stdlib_module = path
528 .strip_prefix("std/")
529 .or_else(|| (path == "observability").then_some("observability"));
530 if let Some(module) = stdlib_module {
531 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
532 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
533 if self.imported_paths.contains(&synthetic) {
534 return Ok(());
535 }
536 let loaded = self
537 .load_stdlib_module_from_source(module, synthetic.clone(), source)
538 .await?;
539 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
540 return Ok(());
541 }
542 return Err(VmError::Runtime(format!(
543 "Unknown stdlib module: std/{module}"
544 )));
545 }
546
547 let base = self
548 .source_dir
549 .clone()
550 .unwrap_or_else(|| PathBuf::from("."));
551 let file_path = resolve_module_import_path(&base, path);
552
553 let canonical = file_path
554 .canonicalize()
555 .unwrap_or_else(|_| file_path.clone());
556 if self.imported_paths.contains(&canonical) {
557 if let Some(importer) = self.imported_paths.last().cloned() {
565 if importer != canonical {
566 self.deferred_cyclic_imports.push(DeferredCyclicImport {
567 importer,
568 target: canonical.clone(),
569 selected_names: selected_names.map(<[String]>::to_vec),
570 });
571 }
572 }
573 return Ok(());
574 }
575 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
576 return self.export_loaded_module(&canonical, &loaded, selected_names);
577 }
578 self.imported_paths.push(canonical.clone());
579
580 let source = std::fs::read_to_string(&file_path).map_err(|e| {
581 VmError::Runtime(format!(
586 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
587 file_path.display(),
588 base.display()
589 ))
590 })?;
591 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
592 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
593
594 let prepared = if bytecode_cache::cache_enabled() {
595 self.prepared_module_cache.get(&canonical, &source)
596 } else {
597 None
598 };
599 let artifact = if let Some(prepared) = prepared {
600 prepared
601 } else {
602 let lookup = bytecode_cache::load_module(&file_path, &source);
606 let cached = if let Some(artifact) = lookup.artifact {
607 artifact
608 } else {
609 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
610 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
611 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
612 eprintln!(
613 "[harn] module cache write skipped for {}: {err}",
614 file_path.display()
615 );
616 }
617 }
618 compiled
619 };
620 let mut prepared = Arc::new(PreparedModuleArtifact::from_cached(cached));
621 if bytecode_cache::cache_enabled() {
622 prepared =
623 self.prepared_module_cache
624 .insert(canonical.clone(), &source, prepared);
625 }
626 prepared
627 };
628
629 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
630 let loaded = self
631 .instantiate_module(module_source_dir, artifact.as_ref())
632 .await?;
633 self.imported_paths.pop();
634 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
635 self.export_loaded_module(&canonical, &loaded, selected_names)?;
636
637 if self.imported_paths.is_empty() {
641 self.flush_deferred_cyclic_imports()?;
642 }
643
644 Ok(())
645 })
646 }
647
648 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
657 if self.deferred_cyclic_imports.is_empty() {
658 return Ok(());
659 }
660 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
661 let mut still_pending = Vec::new();
662 for import in deferred {
663 let (Some(importer), Some(target)) = (
664 self.module_cache.get(&import.importer).cloned(),
665 self.module_cache.get(&import.target).cloned(),
666 ) else {
667 still_pending.push(import);
671 continue;
672 };
673
674 let export_names: Vec<String> = match &import.selected_names {
675 Some(names) => names.clone(),
676 None if !target.public_names.is_empty() => {
677 target.public_names.iter().cloned().collect()
678 }
679 None => target.functions.keys().cloned().collect(),
680 };
681
682 let mut module_state = importer._module_state.lock();
683 for name in export_names {
684 if module_state.get(&name).is_some() {
687 continue;
688 }
689 if let Some(closure) = target.functions.get(&name) {
690 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
691 } else if let Some(value) = target.public_values.get(&name) {
692 module_state.define(&name, value.clone(), false)?;
694 } else {
695 return Err(VmError::Runtime(format!(
696 "Import error: '{name}' is not defined in {}",
697 import.target.display()
698 )));
699 }
700 }
701 }
702 self.deferred_cyclic_imports = still_pending;
703 Ok(())
704 }
705
706 fn cache_key_for_import(&self, path: &str) -> PathBuf {
711 if let Some(module) = path
712 .strip_prefix("std/")
713 .or_else(|| (path == "observability").then_some("observability"))
714 {
715 return PathBuf::from(format!("<stdlib>/{module}.harn"));
716 }
717 let base = self
718 .source_dir
719 .clone()
720 .unwrap_or_else(|| PathBuf::from("."));
721 let file_path = resolve_module_import_path(&base, path);
722 file_path.canonicalize().unwrap_or(file_path)
723 }
724
725 pub async fn load_module_exports(
728 &mut self,
729 path: &Path,
730 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
731 self.ensure_execution_available()?;
732 let path_str = path.to_string_lossy().into_owned();
733 self.execute_import(&path_str, None).await?;
734
735 let mut file_path = if path.is_absolute() {
736 path.to_path_buf()
737 } else {
738 self.source_dir
739 .clone()
740 .unwrap_or_else(|| PathBuf::from("."))
741 .join(path)
742 };
743 if !file_path.exists() && file_path.extension().is_none() {
744 file_path.set_extension("harn");
745 }
746
747 let canonical = file_path
748 .canonicalize()
749 .unwrap_or_else(|_| file_path.clone());
750 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
751 VmError::Runtime(format!(
752 "Import error: failed to cache loaded module '{}'",
753 canonical.display()
754 ))
755 })?;
756
757 let export_names: Vec<String> = if loaded.public_names.is_empty() {
758 loaded.functions.keys().cloned().collect()
759 } else {
760 loaded.public_names.iter().cloned().collect()
761 };
762
763 let mut exports = BTreeMap::new();
764 for name in export_names {
765 let Some(closure) = loaded.functions.get(&name) else {
766 return Err(VmError::Runtime(format!(
767 "Import error: exported function '{name}' is missing from {}",
768 canonical.display()
769 )));
770 };
771 exports.insert(name, Arc::clone(closure));
772 }
773
774 Ok(exports)
775 }
776
777 pub async fn load_module_exports_from_source(
780 &mut self,
781 source_key: impl Into<PathBuf>,
782 source: &str,
783 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
784 self.ensure_execution_available()?;
785 let synthetic = source_key.into();
786 let loaded = self
787 .load_module_from_source(synthetic.clone(), source)
788 .await?;
789 let export_names: Vec<String> = if loaded.public_names.is_empty() {
790 loaded.functions.keys().cloned().collect()
791 } else {
792 loaded.public_names.iter().cloned().collect()
793 };
794
795 let mut exports = BTreeMap::new();
796 for name in export_names {
797 let Some(closure) = loaded.functions.get(&name) else {
798 return Err(VmError::Runtime(format!(
799 "Import error: exported function '{name}' is missing from {}",
800 synthetic.display()
801 )));
802 };
803 exports.insert(name, Arc::clone(closure));
804 }
805
806 Ok(exports)
807 }
808
809 pub async fn load_module_exports_from_import(
813 &mut self,
814 import_path: &str,
815 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
816 self.ensure_execution_available()?;
817 self.execute_import(import_path, None).await?;
818
819 if let Some(module) = import_path
820 .strip_prefix("std/")
821 .or_else(|| (import_path == "observability").then_some("observability"))
822 {
823 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
824 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
825 VmError::Runtime(format!(
826 "Import error: failed to cache loaded module '{}'",
827 synthetic.display()
828 ))
829 })?;
830 let mut exports = BTreeMap::new();
831 let export_names: Vec<String> = if loaded.public_names.is_empty() {
832 loaded.functions.keys().cloned().collect()
833 } else {
834 loaded.public_names.iter().cloned().collect()
835 };
836 for name in export_names {
837 let Some(closure) = loaded.functions.get(&name) else {
838 return Err(VmError::Runtime(format!(
839 "Import error: exported function '{name}' is missing from {}",
840 synthetic.display()
841 )));
842 };
843 exports.insert(name, Arc::clone(closure));
844 }
845 return Ok(exports);
846 }
847
848 let base = self
849 .source_dir
850 .clone()
851 .unwrap_or_else(|| PathBuf::from("."));
852 let file_path = resolve_module_import_path(&base, import_path);
853 self.load_module_exports(&file_path).await
854 }
855}
856
857#[cfg(test)]
858mod tests {
859
860 use std::sync::{Mutex, MutexGuard, OnceLock};
861
862 use super::*;
863
864 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
865
866 fn cache_test_guard() -> MutexGuard<'static, ()> {
867 CACHE_TEST_LOCK
868 .get_or_init(|| Mutex::new(()))
869 .lock()
870 .unwrap()
871 }
872
873 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
874 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
875 stdlib_module_artifact_cache_ptr(module, source)
876 }
877
878 #[test]
879 fn module_function_can_use_local_type_alias_as_schema_value() {
880 let runtime = tokio::runtime::Builder::new_current_thread()
881 .enable_all()
882 .build()
883 .expect("runtime builds");
884
885 let result = runtime.block_on(async {
886 let mut vm = Vm::new();
887 crate::stdlib::register_vm_stdlib(&mut vm);
888 let loaded = vm
889 .load_module_from_source(
890 PathBuf::from("<test>/schema_alias_module.harn"),
891 r#"
892fn accepts_schema(schema) {
893 return schema_report({name: "Ada"}, schema).ok
894}
895
896type UserShape = {name: string}
897
898pub fn works() {
899 return accepts_schema(UserShape)
900}
901"#,
902 )
903 .await
904 .expect("module loads");
905 let closure = Arc::clone(loaded.functions.get("works").expect("works export exists"));
906 vm.call_closure_pub(&closure, &[])
907 .await
908 .expect("module closure executes")
909 });
910
911 assert!(matches!(result, VmValue::Bool(true)), "{result:?}");
912 }
913
914 #[test]
915 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
916 let _guard = cache_test_guard();
917 reset_stdlib_module_artifact_cache();
918 let runtime = tokio::runtime::Builder::new_current_thread()
919 .enable_all()
920 .build()
921 .expect("runtime builds");
922
923 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
924 .block_on(async {
925 let mut first_vm = Vm::new();
926 let first_exports = first_vm
927 .load_module_exports_from_import("std/agent/prompts")
928 .await
929 .expect("first stdlib import succeeds");
930 let first_state = first_exports
931 .get("render_agent_prompt")
932 .expect("first export exists")
933 .module_state()
934 .expect("first module state stays live while VM owns module");
935 let first_state_weak = Arc::downgrade(&first_state);
936 let first_state_ptr = Arc::as_ptr(&first_state);
937
938 let mut second_vm = Vm::new();
939 let second_exports = second_vm
940 .load_module_exports_from_import("std/agent/prompts")
941 .await
942 .expect("second stdlib import succeeds");
943 let second_state = second_exports
944 .get("render_agent_prompt")
945 .expect("second export exists")
946 .module_state()
947 .expect("second module state stays live while VM owns module");
948 let second_state_weak = Arc::downgrade(&second_state);
949
950 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
951 (
952 first_exports,
953 second_exports,
954 first_state_weak,
955 second_state_weak,
956 )
957 });
958 let first_cached =
959 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
960 assert_eq!(
961 cached_stdlib_module_ptr("agent/prompts"),
962 Some(first_cached)
963 );
964
965 let first = first_exports
966 .get("render_agent_prompt")
967 .expect("first export exists");
968 let second = second_exports
969 .get("render_agent_prompt")
970 .expect("second export exists");
971
972 assert!(!Arc::ptr_eq(first, second));
973 assert!(Arc::ptr_eq(&first.func, &second.func));
974 assert!(Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
975 assert!(first.module_state().is_none());
976 assert!(second.module_state().is_none());
977 assert!(first_state_weak.upgrade().is_none());
978 assert!(second_state_weak.upgrade().is_none());
979 }
980
981 #[test]
982 fn prepared_user_module_reuses_code_with_fresh_mutable_state() {
983 let runtime = tokio::runtime::Builder::new_current_thread()
984 .enable_all()
985 .build()
986 .expect("runtime builds");
987 let temp = tempfile::tempdir().expect("tempdir");
988 let module = temp.path().join("counter.harn");
989 std::fs::write(
990 &module,
991 r"
992let count = 0
993
994pub fn increment() {
995 count = count + 1
996 return count
997}
998",
999 )
1000 .expect("write module");
1001 let cache = crate::PreparedModuleCache::default();
1002
1003 runtime.block_on(async {
1004 let mut first_vm = Vm::new();
1005 first_vm.set_prepared_module_cache(cache.clone());
1006 let first_exports = first_vm
1007 .load_module_exports(&module)
1008 .await
1009 .expect("first module load succeeds");
1010 let first = first_exports.get("increment").expect("first export");
1011 assert!(matches!(
1012 first_vm.call_closure_pub(first, &[]).await,
1013 Ok(VmValue::Int(1))
1014 ));
1015 assert!(matches!(
1016 first_vm.call_closure_pub(first, &[]).await,
1017 Ok(VmValue::Int(2))
1018 ));
1019
1020 let mut second_vm = Vm::new();
1021 second_vm.set_prepared_module_cache(cache.clone());
1022 let second_exports = second_vm
1023 .load_module_exports(&module)
1024 .await
1025 .expect("second module load succeeds");
1026 let second = second_exports.get("increment").expect("second export");
1027
1028 assert!(!Arc::ptr_eq(first, second));
1029 assert!(Arc::ptr_eq(&first.func, &second.func));
1030 assert!(Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
1031 assert_ne!(
1032 Arc::as_ptr(&first.module_state().expect("first state")),
1033 Arc::as_ptr(&second.module_state().expect("second state"))
1034 );
1035 assert!(matches!(
1036 second_vm.call_closure_pub(second, &[]).await,
1037 Ok(VmValue::Int(1))
1038 ));
1039 });
1040
1041 let stats = cache.stats();
1042 assert_eq!(stats.insertions, 1);
1043 assert_eq!(stats.hits, 1);
1044 assert_eq!(stats.entries, 1);
1045 }
1046
1047 #[test]
1048 fn prepared_importer_reloads_changed_dependency() {
1049 let runtime = tokio::runtime::Builder::new_current_thread()
1050 .enable_all()
1051 .build()
1052 .expect("runtime builds");
1053 let temp = tempfile::tempdir().expect("tempdir");
1054 let module = temp.path().join("reader.harn");
1055 let dependency = temp.path().join("value.harn");
1056 std::fs::write(
1057 &module,
1058 "import { value } from \"./value\"\npub fn read() { return value() }\n",
1059 )
1060 .expect("write importer");
1061 std::fs::write(&dependency, "pub fn value() { return 1 }\n").expect("write dependency");
1062 let cache = crate::PreparedModuleCache::default();
1063
1064 runtime.block_on(async {
1065 let mut first_vm = Vm::new();
1066 first_vm.set_prepared_module_cache(cache.clone());
1067 let first_exports = first_vm
1068 .load_module_exports(&module)
1069 .await
1070 .expect("first module load succeeds");
1071 let first = first_exports.get("read").expect("first export");
1072 let first_result = first_vm.call_closure_pub(first, &[]).await;
1073 assert!(
1074 matches!(first_result, Ok(VmValue::Int(1))),
1075 "unexpected first dependency result: {first_result:?}"
1076 );
1077
1078 std::fs::write(&dependency, "pub fn value() { return 2 }\n")
1079 .expect("rewrite dependency");
1080
1081 let mut second_vm = Vm::new();
1082 second_vm.set_prepared_module_cache(cache.clone());
1083 let second_exports = second_vm
1084 .load_module_exports(&module)
1085 .await
1086 .expect("second module load succeeds");
1087 let second = second_exports.get("read").expect("second export");
1088 let second_result = second_vm.call_closure_pub(second, &[]).await;
1089 assert!(
1090 matches!(second_result, Ok(VmValue::Int(2))),
1091 "unexpected refreshed dependency result: {second_result:?}"
1092 );
1093 });
1094
1095 let stats = cache.stats();
1096 assert_eq!(stats.hits, 1);
1097 assert_eq!(stats.insertions, 3);
1098 assert_eq!(stats.entries, 3);
1099 }
1100
1101 #[test]
1102 fn stdlib_artifact_cache_is_process_wide_across_threads() {
1103 let _guard = cache_test_guard();
1104 reset_stdlib_module_artifact_cache();
1105
1106 let handle = std::thread::spawn(|| {
1107 let runtime = tokio::runtime::Builder::new_current_thread()
1108 .enable_all()
1109 .build()
1110 .expect("runtime builds");
1111 runtime.block_on(async {
1112 let mut vm = Vm::new();
1113 vm.load_module_exports_from_import("std/agent/prompts")
1114 .await
1115 .expect("thread stdlib import succeeds");
1116 });
1117 });
1118 handle.join().expect("thread joins");
1119 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
1120 .expect("thread import cached stdlib artifact");
1121
1122 let runtime = tokio::runtime::Builder::new_current_thread()
1123 .enable_all()
1124 .build()
1125 .expect("runtime builds");
1126 runtime.block_on(async {
1127 let mut vm = Vm::new();
1128 vm.load_module_exports_from_import("std/agent/prompts")
1129 .await
1130 .expect("main-thread stdlib import succeeds");
1131 });
1132 assert_eq!(
1133 cached_stdlib_module_ptr("agent/prompts"),
1134 Some(thread_cached)
1135 );
1136 }
1137
1138 #[test]
1139 fn module_closures_release_state_after_vm_drop() {
1140 let runtime = tokio::runtime::Builder::new_current_thread()
1141 .enable_all()
1142 .build()
1143 .expect("runtime builds");
1144
1145 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
1146 let mut vm = Vm::new();
1147 let loaded = vm
1148 .load_module_from_source(
1149 PathBuf::from("<test>/module_cycle.harn"),
1150 r#"
1151let payload = "x" * 1024
1152
1153pub fn touch() {
1154 return len(payload)
1155}
1156"#,
1157 )
1158 .await
1159 .expect("module loads");
1160 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
1161 let closure_weak = Arc::downgrade(&closure);
1162 let registry_weak = Arc::downgrade(&loaded._module_functions);
1163 let state_weak = Arc::downgrade(&loaded._module_state);
1164
1165 drop(closure);
1166 drop(loaded);
1167 drop(vm);
1168
1169 (closure_weak, registry_weak, state_weak)
1170 });
1171
1172 assert!(
1173 closure_weak.upgrade().is_none(),
1174 "module closure should drop with its VM"
1175 );
1176 assert!(
1177 registry_weak.upgrade().is_none(),
1178 "module function registry should drop with its VM"
1179 );
1180 assert!(
1181 state_weak.upgrade().is_none(),
1182 "module state should drop with its VM"
1183 );
1184 }
1185}