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 async fn load_module_from_source(
135 &mut self,
136 synthetic: PathBuf,
137 source: &str,
138 ) -> Result<LoadedModule, VmError> {
139 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
140 return Ok(loaded);
141 }
142 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
143
144 let artifact = compile_module_artifact_from_source(&synthetic, source)?;
145
146 self.imported_paths.push(synthetic.clone());
147 let loaded = self.instantiate_module(None, &artifact).await?;
148 self.imported_paths.pop();
149 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
150 Ok(loaded)
151 }
152
153 async fn load_stdlib_module_from_source(
154 &mut self,
155 module: &str,
156 synthetic: PathBuf,
157 source: &'static str,
158 ) -> Result<LoadedModule, VmError> {
159 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
160 return Ok(loaded);
161 }
162 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
163
164 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
165 self.imported_paths.push(synthetic.clone());
166 let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
167 self.imported_paths.pop();
168 Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
169 Ok(loaded)
170 }
171
172 async fn instantiate_stdlib_module(
173 &mut self,
174 artifact: &ModuleArtifact,
175 ) -> Result<LoadedModule, VmError> {
176 self.instantiate_module(None, artifact).await
177 }
178
179 async fn instantiate_module(
187 &mut self,
188 module_source_dir: Option<PathBuf>,
189 artifact: &ModuleArtifact,
190 ) -> Result<LoadedModule, VmError> {
191 let caller_env = self.env.clone();
192 let old_source_dir = self.source_dir.clone();
193 self.env = VmEnv::new();
194 self.source_dir = module_source_dir.clone();
195
196 for import in &artifact.imports {
197 self.execute_import(&import.path, import.selected_names.as_deref())
198 .await?;
199 }
200
201 let module_state: crate::value::ModuleState = {
202 let mut init_env = self.env.clone();
203 if let Some(init_chunk) = &artifact.init_chunk {
204 let fresh_init_chunk = Chunk::from_cached(init_chunk);
205 let saved_env = std::mem::replace(&mut self.env, init_env);
206 let saved_frames = std::mem::take(&mut self.frames);
207 let saved_handlers = std::mem::take(&mut self.exception_handlers);
208 let saved_iterators = std::mem::take(&mut self.iterators);
209 let saved_deadlines = std::mem::take(&mut self.deadlines);
210 let active_context = crate::step_runtime::take_active_context();
221 let init_result = self.run_chunk(std::sync::Arc::new(fresh_init_chunk)).await;
222 crate::step_runtime::restore_active_context(active_context);
223 init_env = std::mem::replace(&mut self.env, saved_env);
224 self.frames = saved_frames;
225 self.exception_handlers = saved_handlers;
226 self.iterators = saved_iterators;
227 self.deadlines = saved_deadlines;
228 init_result?;
229 }
230 Arc::new(crate::value::VmMutex::new(init_env))
231 };
232
233 let module_env = self.env.clone();
234 let registry: ModuleFunctionRegistry =
235 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
236 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
237 let mut public_names = artifact.public_names.clone();
238 let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
244 {
245 let state = module_state.lock();
246 for name in &artifact.public_value_names {
247 if let Some(value) = state.get(name) {
248 public_values.insert(name.clone(), value);
249 public_names.insert(name.clone());
250 }
251 }
252 }
253 let mut public_type_names = artifact.public_type_names.clone();
254 let mut public_type_schemas: BTreeMap<String, VmValue> = artifact
255 .public_type_schemas
256 .iter()
257 .filter_map(|(name, json)| {
258 let parsed = serde_json::from_str::<serde_json::Value>(json).ok()?;
259 Some((name.clone(), crate::schema::json_to_vm_value(&parsed)))
260 })
261 .collect();
262
263 for (name, compiled) in &artifact.functions {
264 let closure = Arc::new(VmClosure {
265 func: Arc::new(CompiledFunction::from_cached(compiled)),
266 env: module_env.clone(),
267 source_dir: module_source_dir.clone(),
268 module_functions: Some(Arc::downgrade(®istry)),
269 module_state: Some(Arc::downgrade(&module_state)),
270 retained_module_scope: None,
271 });
272 registry.lock().insert(name.clone(), Arc::clone(&closure));
273 self.env
274 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
275 module_state
276 .lock()
277 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
278 functions.insert(name.clone(), Arc::clone(&closure));
279 }
280
281 for import in artifact.imports.iter().filter(|import| import.is_pub) {
282 let cache_key = self.cache_key_for_import(&import.path);
283 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
284 if self.imported_paths.contains(&cache_key) {
291 return Err(VmError::Runtime(format!(
292 "Re-export error: cannot `pub import` from '{}' because it forms an \
293 import cycle with this module (its public surface is still being \
294 built). Use a plain `import` here, or re-export from a module that is \
295 not part of the cycle.",
296 import.path
297 )));
298 }
299 return Err(VmError::Runtime(format!(
300 "Re-export error: imported module '{}' was not loaded",
301 import.path
302 )));
303 };
304 let names_to_reexport: Vec<String> = match &import.selected_names {
305 Some(names) => names.clone(),
306 None => loaded
310 .public_names
311 .iter()
312 .chain(loaded.public_type_names.iter())
313 .cloned()
314 .collect(),
315 };
316 for name in names_to_reexport {
317 let Some(closure) = loaded.functions.get(&name) else {
318 if let Some(value) = loaded.public_values.get(&name) {
321 public_values.insert(name.clone(), value.clone());
322 public_names.insert(name);
323 continue;
324 }
325 if loaded.public_type_names.contains(&name) {
329 if let Some(schema) = loaded.public_type_schemas.get(&name) {
330 public_type_schemas.insert(name.clone(), schema.clone());
331 }
332 public_type_names.insert(name);
333 continue;
334 }
335 return Err(VmError::Runtime(format!(
336 "Re-export error: '{name}' is not exported by '{}'",
337 import.path
338 )));
339 };
340 if let Some(existing) = functions.get(&name) {
341 if !Arc::ptr_eq(existing, closure) {
342 return Err(VmError::Runtime(format!(
343 "Re-export collision: '{name}' is defined here and also \
344 re-exported from '{}'",
345 import.path
346 )));
347 }
348 }
349 functions.insert(name.clone(), Arc::clone(closure));
350 public_names.insert(name);
351 }
352 }
353
354 self.env = caller_env;
355 self.source_dir = old_source_dir;
356
357 Ok(LoadedModule {
358 functions,
359 public_names,
360 public_values,
361 public_type_names,
362 public_type_schemas,
363 _module_functions: registry,
364 _module_state: module_state,
365 })
366 }
367
368 fn export_loaded_module(
369 &mut self,
370 module_path: &Path,
371 loaded: &LoadedModule,
372 selected_names: Option<&[String]>,
373 ) -> Result<(), VmError> {
374 let module_name = module_path.display().to_string();
375 let export_names: Vec<String> = if let Some(names) = selected_names {
376 for name in names {
382 if !loaded.public_names.contains(name) && !loaded.public_type_names.contains(name) {
383 let hint = if loaded.functions.contains_key(name) {
384 " — it is defined there but not `pub`; mark it `pub` to export it"
385 } else {
386 ""
387 };
388 return Err(VmError::Runtime(format!(
389 "Import error: '{name}' is not exported by {module_name}{hint}"
390 )));
391 }
392 }
393 names.to_vec()
394 } else {
395 loaded
398 .public_names
399 .iter()
400 .chain(loaded.public_type_names.iter())
401 .cloned()
402 .collect()
403 };
404
405 for name in export_names {
406 if loaded.public_type_names.contains(&name) && !loaded.functions.contains_key(&name) {
412 if let Some(schema) = loaded.public_type_schemas.get(&name) {
413 self.env.define(&name, schema.clone(), false)?;
414 }
415 continue;
416 }
417 if let Some(value) = loaded.public_values.get(&name) {
419 if self.env.get(&name).is_some() {
420 return Err(VmError::Runtime(format!(
421 "Import collision: '{name}' is already defined when importing \
422 {module_name}. Use selective imports to disambiguate: \
423 import {{ {name} }} from \"...\""
424 )));
425 }
426 self.env.define(&name, value.clone(), false)?;
427 continue;
428 }
429 let Some(closure) = loaded.functions.get(&name) else {
430 return Err(VmError::Runtime(format!(
431 "Import error: '{name}' is not defined in {module_name}"
432 )));
433 };
434 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
435 return Err(VmError::Runtime(format!(
436 "Import collision: '{name}' is already defined when importing {module_name}. \
437 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
438 )));
439 }
440 self.env
441 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
442 }
443 Ok(())
444 }
445
446 pub(super) fn execute_import<'a>(
448 &'a mut self,
449 path: &'a str,
450 selected_names: Option<&'a [String]>,
451 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
452 Box::pin(async move {
453 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
454
455 let stdlib_module = path
456 .strip_prefix("std/")
457 .or_else(|| (path == "observability").then_some("observability"));
458 if let Some(module) = stdlib_module {
459 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
460 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
461 if self.imported_paths.contains(&synthetic) {
462 return Ok(());
463 }
464 let loaded = self
465 .load_stdlib_module_from_source(module, synthetic.clone(), source)
466 .await?;
467 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
468 return Ok(());
469 }
470 return Err(VmError::Runtime(format!(
471 "Unknown stdlib module: std/{module}"
472 )));
473 }
474
475 let base = self
476 .source_dir
477 .clone()
478 .unwrap_or_else(|| PathBuf::from("."));
479 let file_path = resolve_module_import_path(&base, path);
480
481 let canonical = file_path
482 .canonicalize()
483 .unwrap_or_else(|_| file_path.clone());
484 if self.imported_paths.contains(&canonical) {
485 if let Some(importer) = self.imported_paths.last().cloned() {
493 if importer != canonical {
494 self.deferred_cyclic_imports.push(DeferredCyclicImport {
495 importer,
496 target: canonical.clone(),
497 selected_names: selected_names.map(<[String]>::to_vec),
498 });
499 }
500 }
501 return Ok(());
502 }
503 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
504 return self.export_loaded_module(&canonical, &loaded, selected_names);
505 }
506 self.imported_paths.push(canonical.clone());
507
508 let source = std::fs::read_to_string(&file_path).map_err(|e| {
509 VmError::Runtime(format!(
514 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
515 file_path.display(),
516 base.display()
517 ))
518 })?;
519 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
520 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
521
522 let lookup = bytecode_cache::load_module(&file_path, &source);
525 let artifact = if let Some(artifact) = lookup.artifact {
526 artifact
527 } else {
528 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
529 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
530 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
531 eprintln!(
532 "[harn] module cache write skipped for {}: {err}",
533 file_path.display()
534 );
535 }
536 }
537 compiled
538 };
539
540 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
541 let loaded = self
542 .instantiate_module(module_source_dir, &artifact)
543 .await?;
544 self.imported_paths.pop();
545 Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
546 self.export_loaded_module(&canonical, &loaded, selected_names)?;
547
548 if self.imported_paths.is_empty() {
552 self.flush_deferred_cyclic_imports()?;
553 }
554
555 Ok(())
556 })
557 }
558
559 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
568 if self.deferred_cyclic_imports.is_empty() {
569 return Ok(());
570 }
571 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
572 let mut still_pending = Vec::new();
573 for import in deferred {
574 let (Some(importer), Some(target)) = (
575 self.module_cache.get(&import.importer).cloned(),
576 self.module_cache.get(&import.target).cloned(),
577 ) else {
578 still_pending.push(import);
582 continue;
583 };
584
585 let export_names: Vec<String> = match &import.selected_names {
586 Some(names) => names.clone(),
587 None if !target.public_names.is_empty() => {
588 target.public_names.iter().cloned().collect()
589 }
590 None => target.functions.keys().cloned().collect(),
591 };
592
593 let mut module_state = importer._module_state.lock();
594 for name in export_names {
595 if module_state.get(&name).is_some() {
598 continue;
599 }
600 if let Some(closure) = target.functions.get(&name) {
601 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
602 } else if let Some(value) = target.public_values.get(&name) {
603 module_state.define(&name, value.clone(), false)?;
605 } else {
606 return Err(VmError::Runtime(format!(
607 "Import error: '{name}' is not defined in {}",
608 import.target.display()
609 )));
610 }
611 }
612 }
613 self.deferred_cyclic_imports = still_pending;
614 Ok(())
615 }
616
617 fn cache_key_for_import(&self, path: &str) -> PathBuf {
622 if let Some(module) = path
623 .strip_prefix("std/")
624 .or_else(|| (path == "observability").then_some("observability"))
625 {
626 return PathBuf::from(format!("<stdlib>/{module}.harn"));
627 }
628 let base = self
629 .source_dir
630 .clone()
631 .unwrap_or_else(|| PathBuf::from("."));
632 let file_path = resolve_module_import_path(&base, path);
633 file_path.canonicalize().unwrap_or(file_path)
634 }
635
636 pub async fn load_module_exports(
639 &mut self,
640 path: &Path,
641 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
642 let path_str = path.to_string_lossy().into_owned();
643 self.execute_import(&path_str, None).await?;
644
645 let mut file_path = if path.is_absolute() {
646 path.to_path_buf()
647 } else {
648 self.source_dir
649 .clone()
650 .unwrap_or_else(|| PathBuf::from("."))
651 .join(path)
652 };
653 if !file_path.exists() && file_path.extension().is_none() {
654 file_path.set_extension("harn");
655 }
656
657 let canonical = file_path
658 .canonicalize()
659 .unwrap_or_else(|_| file_path.clone());
660 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
661 VmError::Runtime(format!(
662 "Import error: failed to cache loaded module '{}'",
663 canonical.display()
664 ))
665 })?;
666
667 let export_names: Vec<String> = if loaded.public_names.is_empty() {
668 loaded.functions.keys().cloned().collect()
669 } else {
670 loaded.public_names.iter().cloned().collect()
671 };
672
673 let mut exports = BTreeMap::new();
674 for name in export_names {
675 let Some(closure) = loaded.functions.get(&name) else {
676 return Err(VmError::Runtime(format!(
677 "Import error: exported function '{name}' is missing from {}",
678 canonical.display()
679 )));
680 };
681 exports.insert(name, Arc::clone(closure));
682 }
683
684 Ok(exports)
685 }
686
687 pub async fn load_module_exports_from_source(
690 &mut self,
691 source_key: impl Into<PathBuf>,
692 source: &str,
693 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
694 let synthetic = source_key.into();
695 let loaded = self
696 .load_module_from_source(synthetic.clone(), source)
697 .await?;
698 let export_names: Vec<String> = if loaded.public_names.is_empty() {
699 loaded.functions.keys().cloned().collect()
700 } else {
701 loaded.public_names.iter().cloned().collect()
702 };
703
704 let mut exports = BTreeMap::new();
705 for name in export_names {
706 let Some(closure) = loaded.functions.get(&name) else {
707 return Err(VmError::Runtime(format!(
708 "Import error: exported function '{name}' is missing from {}",
709 synthetic.display()
710 )));
711 };
712 exports.insert(name, Arc::clone(closure));
713 }
714
715 Ok(exports)
716 }
717
718 pub async fn load_module_exports_from_import(
722 &mut self,
723 import_path: &str,
724 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
725 self.execute_import(import_path, None).await?;
726
727 if let Some(module) = import_path
728 .strip_prefix("std/")
729 .or_else(|| (import_path == "observability").then_some("observability"))
730 {
731 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
732 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
733 VmError::Runtime(format!(
734 "Import error: failed to cache loaded module '{}'",
735 synthetic.display()
736 ))
737 })?;
738 let mut exports = BTreeMap::new();
739 let export_names: Vec<String> = if loaded.public_names.is_empty() {
740 loaded.functions.keys().cloned().collect()
741 } else {
742 loaded.public_names.iter().cloned().collect()
743 };
744 for name in export_names {
745 let Some(closure) = loaded.functions.get(&name) else {
746 return Err(VmError::Runtime(format!(
747 "Import error: exported function '{name}' is missing from {}",
748 synthetic.display()
749 )));
750 };
751 exports.insert(name, Arc::clone(closure));
752 }
753 return Ok(exports);
754 }
755
756 let base = self
757 .source_dir
758 .clone()
759 .unwrap_or_else(|| PathBuf::from("."));
760 let file_path = resolve_module_import_path(&base, import_path);
761 self.load_module_exports(&file_path).await
762 }
763}
764
765#[cfg(test)]
766mod tests {
767
768 use std::sync::{Mutex, MutexGuard, OnceLock};
769
770 use super::*;
771
772 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
773
774 fn cache_test_guard() -> MutexGuard<'static, ()> {
775 CACHE_TEST_LOCK
776 .get_or_init(|| Mutex::new(()))
777 .lock()
778 .unwrap()
779 }
780
781 fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
782 let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
783 stdlib_module_artifact_cache_ptr(module, source)
784 }
785
786 #[test]
787 fn module_function_can_use_local_type_alias_as_schema_value() {
788 let runtime = tokio::runtime::Builder::new_current_thread()
789 .enable_all()
790 .build()
791 .expect("runtime builds");
792
793 let result = runtime.block_on(async {
794 let mut vm = Vm::new();
795 crate::stdlib::register_vm_stdlib(&mut vm);
796 let loaded = vm
797 .load_module_from_source(
798 PathBuf::from("<test>/schema_alias_module.harn"),
799 r#"
800fn accepts_schema(schema) {
801 return schema_report({name: "Ada"}, schema).ok
802}
803
804type UserShape = {name: string}
805
806pub fn works() {
807 return accepts_schema(UserShape)
808}
809"#,
810 )
811 .await
812 .expect("module loads");
813 let closure = Arc::clone(loaded.functions.get("works").expect("works export exists"));
814 vm.call_closure_pub(&closure, &[])
815 .await
816 .expect("module closure executes")
817 });
818
819 assert!(matches!(result, VmValue::Bool(true)), "{result:?}");
820 }
821
822 #[test]
823 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
824 let _guard = cache_test_guard();
825 reset_stdlib_module_artifact_cache();
826 let runtime = tokio::runtime::Builder::new_current_thread()
827 .enable_all()
828 .build()
829 .expect("runtime builds");
830
831 let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
832 .block_on(async {
833 let mut first_vm = Vm::new();
834 let first_exports = first_vm
835 .load_module_exports_from_import("std/agent/prompts")
836 .await
837 .expect("first stdlib import succeeds");
838 let first_state = first_exports
839 .get("render_agent_prompt")
840 .expect("first export exists")
841 .module_state()
842 .expect("first module state stays live while VM owns module");
843 let first_state_weak = Arc::downgrade(&first_state);
844 let first_state_ptr = Arc::as_ptr(&first_state);
845
846 let mut second_vm = Vm::new();
847 let second_exports = second_vm
848 .load_module_exports_from_import("std/agent/prompts")
849 .await
850 .expect("second stdlib import succeeds");
851 let second_state = second_exports
852 .get("render_agent_prompt")
853 .expect("second export exists")
854 .module_state()
855 .expect("second module state stays live while VM owns module");
856 let second_state_weak = Arc::downgrade(&second_state);
857
858 assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
859 (
860 first_exports,
861 second_exports,
862 first_state_weak,
863 second_state_weak,
864 )
865 });
866 let first_cached =
867 cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
868 assert_eq!(
869 cached_stdlib_module_ptr("agent/prompts"),
870 Some(first_cached)
871 );
872
873 let first = first_exports
874 .get("render_agent_prompt")
875 .expect("first export exists");
876 let second = second_exports
877 .get("render_agent_prompt")
878 .expect("second export exists");
879
880 assert!(!Arc::ptr_eq(first, second));
881 assert!(!Arc::ptr_eq(&first.func, &second.func));
882 assert!(!Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
883 assert!(first.module_state().is_none());
884 assert!(second.module_state().is_none());
885 assert!(first_state_weak.upgrade().is_none());
886 assert!(second_state_weak.upgrade().is_none());
887 }
888
889 #[test]
890 fn stdlib_artifact_cache_is_process_wide_across_threads() {
891 let _guard = cache_test_guard();
892 reset_stdlib_module_artifact_cache();
893
894 let handle = std::thread::spawn(|| {
895 let runtime = tokio::runtime::Builder::new_current_thread()
896 .enable_all()
897 .build()
898 .expect("runtime builds");
899 runtime.block_on(async {
900 let mut vm = Vm::new();
901 vm.load_module_exports_from_import("std/agent/prompts")
902 .await
903 .expect("thread stdlib import succeeds");
904 });
905 });
906 handle.join().expect("thread joins");
907 let thread_cached = cached_stdlib_module_ptr("agent/prompts")
908 .expect("thread import cached stdlib artifact");
909
910 let runtime = tokio::runtime::Builder::new_current_thread()
911 .enable_all()
912 .build()
913 .expect("runtime builds");
914 runtime.block_on(async {
915 let mut vm = Vm::new();
916 vm.load_module_exports_from_import("std/agent/prompts")
917 .await
918 .expect("main-thread stdlib import succeeds");
919 });
920 assert_eq!(
921 cached_stdlib_module_ptr("agent/prompts"),
922 Some(thread_cached)
923 );
924 }
925
926 #[test]
927 fn module_closures_release_state_after_vm_drop() {
928 let runtime = tokio::runtime::Builder::new_current_thread()
929 .enable_all()
930 .build()
931 .expect("runtime builds");
932
933 let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
934 let mut vm = Vm::new();
935 let loaded = vm
936 .load_module_from_source(
937 PathBuf::from("<test>/module_cycle.harn"),
938 r#"
939let payload = "x" * 1024
940
941pub fn touch() {
942 return len(payload)
943}
944"#,
945 )
946 .await
947 .expect("module loads");
948 let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
949 let closure_weak = Arc::downgrade(&closure);
950 let registry_weak = Arc::downgrade(&loaded._module_functions);
951 let state_weak = Arc::downgrade(&loaded._module_state);
952
953 drop(closure);
954 drop(loaded);
955 drop(vm);
956
957 (closure_weak, registry_weak, state_weak)
958 });
959
960 assert!(
961 closure_weak.upgrade().is_none(),
962 "module closure should drop with its VM"
963 );
964 assert!(
965 registry_weak.upgrade().is_none(),
966 "module function registry should drop with its VM"
967 );
968 assert!(
969 state_weak.upgrade().is_none(),
970 "module state should drop with its VM"
971 );
972 }
973}