1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashSet};
3use std::future::Future;
4use std::hash::{Hash, Hasher};
5use std::path::{Path, PathBuf};
6use std::pin::Pin;
7use std::rc::Rc;
8use std::sync::{Arc, Mutex, OnceLock};
9
10use crate::chunk::{CachedChunk, CachedCompiledFunction, Chunk, CompiledFunction};
11use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
12
13use super::{ScopeSpan, Vm};
14
15#[derive(Clone)]
16struct ModuleImportSpec {
17 path: String,
18 selected_names: Option<Vec<String>>,
19 is_pub: bool,
20}
21
22#[derive(Clone)]
23struct CompiledStdlibModule {
24 imports: Vec<ModuleImportSpec>,
25 init_chunk: Option<CachedChunk>,
26 functions: BTreeMap<String, CachedCompiledFunction>,
27 public_names: HashSet<String>,
28}
29
30static STDLIB_MODULE_ARTIFACT_CACHE: OnceLock<Mutex<BTreeMap<String, Arc<CompiledStdlibModule>>>> =
31 OnceLock::new();
32
33fn stdlib_module_artifact_cache() -> &'static Mutex<BTreeMap<String, Arc<CompiledStdlibModule>>> {
34 STDLIB_MODULE_ARTIFACT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
35}
36
37#[cfg(test)]
38fn reset_stdlib_module_artifact_cache() {
39 stdlib_module_artifact_cache().lock().unwrap().clear();
40}
41
42#[cfg(test)]
43fn stdlib_module_artifact_cache_len() -> usize {
44 stdlib_module_artifact_cache().lock().unwrap().len()
45}
46
47#[derive(Clone)]
48pub(crate) struct LoadedModule {
49 pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
50 pub(crate) public_names: HashSet<String>,
51}
52
53pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
54 let synthetic_current_file = base.join("__harn_import_base__.harn");
55 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
56 return resolved;
57 }
58
59 let mut file_path = base.join(path);
60
61 if !file_path.exists() && file_path.extension().is_none() {
62 file_path.set_extension("harn");
63 }
64
65 file_path
66}
67
68fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
69 let mut hasher = std::collections::hash_map::DefaultHasher::new();
70 module.hash(&mut hasher);
71 source.hash(&mut hasher);
72 format!("{module}:{:016x}", hasher.finish())
73}
74
75fn stdlib_module_artifact(
76 module: &str,
77 synthetic: &Path,
78 source: &str,
79) -> Result<Arc<CompiledStdlibModule>, VmError> {
80 let key = stdlib_artifact_cache_key(module, source);
81 {
82 let cache = stdlib_module_artifact_cache().lock().unwrap();
83 if let Some(cached) = cache.get(&key) {
84 return Ok(Arc::clone(cached));
85 }
86 }
87
88 let compiled = Arc::new(compile_stdlib_module_artifact(synthetic, source)?);
89 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
90 if let Some(cached) = cache.get(&key) {
91 return Ok(Arc::clone(cached));
92 }
93 cache.insert(key, Arc::clone(&compiled));
94 Ok(compiled)
95}
96
97fn compile_stdlib_module_artifact(
98 synthetic: &Path,
99 source: &str,
100) -> Result<CompiledStdlibModule, VmError> {
101 let mut lexer = harn_lexer::Lexer::new(source);
102 let tokens = lexer.tokenize().map_err(|e| {
103 VmError::Runtime(format!("Import lex error in {}: {e}", synthetic.display()))
104 })?;
105 let mut parser = harn_parser::Parser::new(tokens);
106 let program = parser.parse().map_err(|e| {
107 VmError::Runtime(format!(
108 "Import parse error in {}: {e}",
109 synthetic.display()
110 ))
111 })?;
112
113 let imports = program
114 .iter()
115 .filter_map(|node| match &node.node {
116 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
117 path: path.clone(),
118 selected_names: None,
119 is_pub: *is_pub,
120 }),
121 harn_parser::Node::SelectiveImport {
122 names,
123 path,
124 is_pub,
125 } => Some(ModuleImportSpec {
126 path: path.clone(),
127 selected_names: Some(names.clone()),
128 is_pub: *is_pub,
129 }),
130 _ => None,
131 })
132 .collect();
133
134 let init_nodes: Vec<harn_parser::SNode> = program
135 .iter()
136 .filter(|sn| {
137 matches!(
138 &sn.node,
139 harn_parser::Node::VarBinding { .. } | harn_parser::Node::LetBinding { .. }
140 )
141 })
142 .cloned()
143 .collect();
144 let init_chunk = if init_nodes.is_empty() {
145 None
146 } else {
147 Some(
148 crate::Compiler::new()
149 .compile(&init_nodes)
150 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
151 .freeze_for_cache(),
152 )
153 };
154
155 let mut functions = BTreeMap::new();
156 let mut public_names = HashSet::new();
157 let module_source_file = Some(synthetic.display().to_string());
158 for node in &program {
159 let inner = match &node.node {
160 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
161 _ => node,
162 };
163 let harn_parser::Node::FnDecl {
164 name,
165 type_params,
166 params,
167 body,
168 is_pub,
169 ..
170 } = &inner.node
171 else {
172 continue;
173 };
174
175 let mut compiler = crate::Compiler::new();
176 let func_chunk = compiler
177 .compile_fn_body(type_params, params, body, module_source_file.clone())
178 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
179 functions.insert(name.clone(), func_chunk.freeze_for_cache());
180 if *is_pub {
181 public_names.insert(name.clone());
182 }
183 }
184
185 Ok(CompiledStdlibModule {
186 imports,
187 init_chunk,
188 functions,
189 public_names,
190 })
191}
192
193impl Vm {
194 async fn load_module_from_source(
195 &mut self,
196 synthetic: PathBuf,
197 source: &str,
198 ) -> Result<LoadedModule, VmError> {
199 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
200 return Ok(loaded);
201 }
202 self.source_cache
203 .insert(synthetic.clone(), source.to_string());
204
205 let mut lexer = harn_lexer::Lexer::new(source);
206 let tokens = lexer.tokenize().map_err(|e| {
207 VmError::Runtime(format!("Import lex error in {}: {e}", synthetic.display()))
208 })?;
209 let mut parser = harn_parser::Parser::new(tokens);
210 let program = parser.parse().map_err(|e| {
211 VmError::Runtime(format!(
212 "Import parse error in {}: {e}",
213 synthetic.display()
214 ))
215 })?;
216
217 self.imported_paths.push(synthetic.clone());
218 let loaded = self
219 .import_declarations(&program, None, Some(&synthetic))
220 .await?;
221 self.imported_paths.pop();
222 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 self.source_cache
236 .insert(synthetic.clone(), source.to_string());
237
238 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
239 self.imported_paths.push(synthetic.clone());
240 let loaded = self
241 .instantiate_stdlib_module(&synthetic, artifact.as_ref())
242 .await?;
243 self.imported_paths.pop();
244 self.module_cache.insert(synthetic, loaded.clone());
245 Ok(loaded)
246 }
247
248 async fn instantiate_stdlib_module(
249 &mut self,
250 _synthetic: &Path,
251 artifact: &CompiledStdlibModule,
252 ) -> Result<LoadedModule, VmError> {
253 let caller_env = self.env.clone();
254 let old_source_dir = self.source_dir.clone();
255 self.env = VmEnv::new();
256 self.source_dir = None;
257
258 for import in &artifact.imports {
259 self.execute_import(&import.path, import.selected_names.as_deref())
260 .await?;
261 }
262
263 let module_state: crate::value::ModuleState = {
264 let mut init_env = self.env.clone();
265 if let Some(init_chunk) = &artifact.init_chunk {
266 let fresh_init_chunk = Chunk::from_cached(init_chunk);
267 let saved_env = std::mem::replace(&mut self.env, init_env);
268 let saved_frames = std::mem::take(&mut self.frames);
269 let saved_handlers = std::mem::take(&mut self.exception_handlers);
270 let saved_iterators = std::mem::take(&mut self.iterators);
271 let saved_deadlines = std::mem::take(&mut self.deadlines);
272 let init_result = self.run_chunk(&fresh_init_chunk).await;
273 init_env = std::mem::replace(&mut self.env, saved_env);
274 self.frames = saved_frames;
275 self.exception_handlers = saved_handlers;
276 self.iterators = saved_iterators;
277 self.deadlines = saved_deadlines;
278 init_result?;
279 }
280 Rc::new(RefCell::new(init_env))
281 };
282
283 let module_env = self.env.clone();
284 let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
285 let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
286 let mut public_names = artifact.public_names.clone();
287
288 for (name, compiled) in &artifact.functions {
289 let closure = Rc::new(VmClosure {
290 func: Rc::new(CompiledFunction::from_cached(compiled)),
291 env: module_env.clone(),
292 source_dir: None,
293 module_functions: Some(Rc::clone(®istry)),
294 module_state: Some(Rc::clone(&module_state)),
295 });
296 registry
297 .borrow_mut()
298 .insert(name.clone(), Rc::clone(&closure));
299 self.env
300 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
301 module_state
302 .borrow_mut()
303 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
304 functions.insert(name.clone(), Rc::clone(&closure));
305 }
306
307 for import in artifact.imports.iter().filter(|import| import.is_pub) {
308 let cache_key = self.cache_key_for_import(&import.path);
309 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
310 return Err(VmError::Runtime(format!(
311 "Re-export error: imported module '{}' was not loaded",
312 import.path
313 )));
314 };
315 let names_to_reexport: Vec<String> = match &import.selected_names {
316 Some(names) => names.clone(),
317 None => {
318 if loaded.public_names.is_empty() {
319 loaded.functions.keys().cloned().collect()
320 } else {
321 loaded.public_names.iter().cloned().collect()
322 }
323 }
324 };
325 for name in names_to_reexport {
326 let Some(closure) = loaded.functions.get(&name) else {
327 return Err(VmError::Runtime(format!(
328 "Re-export error: '{name}' is not exported by '{}'",
329 import.path
330 )));
331 };
332 if let Some(existing) = functions.get(&name) {
333 if !Rc::ptr_eq(existing, closure) {
334 return Err(VmError::Runtime(format!(
335 "Re-export collision: '{name}' is defined here and also \
336 re-exported from '{}'",
337 import.path
338 )));
339 }
340 }
341 functions.insert(name.clone(), Rc::clone(closure));
342 public_names.insert(name);
343 }
344 }
345
346 self.env = caller_env;
347 self.source_dir = old_source_dir;
348
349 Ok(LoadedModule {
350 functions,
351 public_names,
352 })
353 }
354
355 fn export_loaded_module(
356 &mut self,
357 module_path: &Path,
358 loaded: &LoadedModule,
359 selected_names: Option<&[String]>,
360 ) -> Result<(), VmError> {
361 let export_names: Vec<String> = if let Some(names) = selected_names {
362 names.to_vec()
363 } else if !loaded.public_names.is_empty() {
364 loaded.public_names.iter().cloned().collect()
365 } else {
366 loaded.functions.keys().cloned().collect()
367 };
368
369 let module_name = module_path.display().to_string();
370 for name in export_names {
371 let Some(closure) = loaded.functions.get(&name) else {
372 return Err(VmError::Runtime(format!(
373 "Import error: '{name}' is not defined in {module_name}"
374 )));
375 };
376 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
377 return Err(VmError::Runtime(format!(
378 "Import collision: '{name}' is already defined when importing {module_name}. \
379 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
380 )));
381 }
382 self.env
383 .define(&name, VmValue::Closure(Rc::clone(closure)), false)?;
384 }
385 Ok(())
386 }
387
388 pub(super) fn execute_import<'a>(
390 &'a mut self,
391 path: &'a str,
392 selected_names: Option<&'a [String]>,
393 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
394 Box::pin(async move {
395 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
396
397 if let Some(module) = path.strip_prefix("std/") {
398 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
399 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
400 if self.imported_paths.contains(&synthetic) {
401 return Ok(());
402 }
403 let loaded = self
404 .load_stdlib_module_from_source(module, synthetic.clone(), source)
405 .await?;
406 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
407 return Ok(());
408 }
409 return Err(VmError::Runtime(format!(
410 "Unknown stdlib module: std/{module}"
411 )));
412 }
413
414 let base = self
415 .source_dir
416 .clone()
417 .unwrap_or_else(|| PathBuf::from("."));
418 let file_path = resolve_module_import_path(&base, path);
419
420 let canonical = file_path
421 .canonicalize()
422 .unwrap_or_else(|_| file_path.clone());
423 if self.imported_paths.contains(&canonical) {
424 return Ok(());
425 }
426 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
427 return self.export_loaded_module(&canonical, &loaded, selected_names);
428 }
429 self.imported_paths.push(canonical.clone());
430
431 let source = std::fs::read_to_string(&file_path).map_err(|e| {
432 VmError::Runtime(format!(
433 "Import error: cannot read '{}': {e}",
434 file_path.display()
435 ))
436 })?;
437 self.source_cache.insert(canonical.clone(), source.clone());
438 self.source_cache.insert(file_path.clone(), source.clone());
439
440 let mut lexer = harn_lexer::Lexer::new(&source);
441 let tokens = lexer
442 .tokenize()
443 .map_err(|e| VmError::Runtime(format!("Import lex error: {e}")))?;
444 let mut parser = harn_parser::Parser::new(tokens);
445 let program = parser
446 .parse()
447 .map_err(|e| VmError::Runtime(format!("Import parse error: {e}")))?;
448
449 let loaded = self
450 .import_declarations(&program, Some(&file_path), Some(&file_path))
451 .await?;
452 self.imported_paths.pop();
453 self.module_cache.insert(canonical.clone(), loaded.clone());
454 self.export_loaded_module(&canonical, &loaded, selected_names)?;
455
456 Ok(())
457 })
458 }
459
460 fn import_declarations<'a>(
462 &'a mut self,
463 program: &'a [harn_parser::SNode],
464 file_path: Option<&'a Path>,
465 debug_source_file: Option<&'a Path>,
466 ) -> Pin<Box<dyn Future<Output = Result<LoadedModule, VmError>> + 'a>> {
467 Box::pin(async move {
468 let caller_env = self.env.clone();
469 let old_source_dir = self.source_dir.clone();
470 self.env = VmEnv::new();
471 if let Some(fp) = file_path {
472 if let Some(parent) = fp.parent() {
473 self.source_dir = Some(parent.to_path_buf());
474 }
475 }
476
477 for node in program {
478 match &node.node {
479 harn_parser::Node::ImportDecl { path: sub_path, .. } => {
480 self.execute_import(sub_path, None).await?;
481 }
482 harn_parser::Node::SelectiveImport {
483 names,
484 path: sub_path,
485 ..
486 } => {
487 self.execute_import(sub_path, Some(names)).await?;
488 }
489 _ => {}
490 }
491 }
492
493 let module_state: crate::value::ModuleState = {
499 let mut init_env = self.env.clone();
500 let init_nodes: Vec<harn_parser::SNode> = program
501 .iter()
502 .filter(|sn| {
503 matches!(
504 &sn.node,
505 harn_parser::Node::VarBinding { .. }
506 | harn_parser::Node::LetBinding { .. }
507 )
508 })
509 .cloned()
510 .collect();
511 if !init_nodes.is_empty() {
512 let init_compiler = crate::Compiler::new();
513 let init_chunk = init_compiler
514 .compile(&init_nodes)
515 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?;
516 let saved_env = std::mem::replace(&mut self.env, init_env);
519 let saved_frames = std::mem::take(&mut self.frames);
520 let saved_handlers = std::mem::take(&mut self.exception_handlers);
521 let saved_iterators = std::mem::take(&mut self.iterators);
522 let saved_deadlines = std::mem::take(&mut self.deadlines);
523 let init_result = self.run_chunk(&init_chunk).await;
524 init_env = std::mem::replace(&mut self.env, saved_env);
525 self.frames = saved_frames;
526 self.exception_handlers = saved_handlers;
527 self.iterators = saved_iterators;
528 self.deadlines = saved_deadlines;
529 init_result?;
530 }
531 Rc::new(RefCell::new(init_env))
532 };
533
534 let module_env = self.env.clone();
535 let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
536 let source_dir = file_path.and_then(|fp| fp.parent().map(|p| p.to_path_buf()));
537 let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
538 let mut public_names: HashSet<String> = HashSet::new();
539
540 for node in program {
541 let inner = match &node.node {
545 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
546 _ => node,
547 };
548 let harn_parser::Node::FnDecl {
549 name,
550 type_params,
551 params,
552 body,
553 is_pub,
554 ..
555 } = &inner.node
556 else {
557 continue;
558 };
559
560 let mut compiler = crate::Compiler::new();
561 let module_source_file = debug_source_file.map(|p| p.display().to_string());
562 let func_chunk = compiler
563 .compile_fn_body(type_params, params, body, module_source_file)
564 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
565 let closure = Rc::new(VmClosure {
566 func: Rc::new(func_chunk),
567 env: module_env.clone(),
568 source_dir: source_dir.clone(),
569 module_functions: Some(Rc::clone(®istry)),
570 module_state: Some(Rc::clone(&module_state)),
571 });
572 registry
573 .borrow_mut()
574 .insert(name.clone(), Rc::clone(&closure));
575 self.env
576 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
577 module_state.borrow_mut().define(
584 name,
585 VmValue::Closure(Rc::clone(&closure)),
586 false,
587 )?;
588 functions.insert(name.clone(), Rc::clone(&closure));
589 if *is_pub {
590 public_names.insert(name.clone());
591 }
592 }
593
594 for node in program {
599 let (sub_path, selective_names, is_pub_import) = match &node.node {
600 harn_parser::Node::ImportDecl {
601 path: sub_path,
602 is_pub,
603 } => (sub_path.clone(), None, *is_pub),
604 harn_parser::Node::SelectiveImport {
605 names,
606 path: sub_path,
607 is_pub,
608 } => (sub_path.clone(), Some(names.clone()), *is_pub),
609 _ => continue,
610 };
611 if !is_pub_import {
612 continue;
613 }
614 let cache_key = self.cache_key_for_import(&sub_path);
615 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
616 return Err(VmError::Runtime(format!(
617 "Re-export error: imported module '{sub_path}' was not loaded"
618 )));
619 };
620 let names_to_reexport: Vec<String> = match selective_names {
621 Some(names) => names,
622 None => {
623 if loaded.public_names.is_empty() {
624 loaded.functions.keys().cloned().collect()
625 } else {
626 loaded.public_names.iter().cloned().collect()
627 }
628 }
629 };
630 for name in names_to_reexport {
631 let Some(closure) = loaded.functions.get(&name) else {
632 return Err(VmError::Runtime(format!(
633 "Re-export error: '{name}' is not exported by '{sub_path}'"
634 )));
635 };
636 if let Some(existing) = functions.get(&name) {
637 if !Rc::ptr_eq(existing, closure) {
638 return Err(VmError::Runtime(format!(
639 "Re-export collision: '{name}' is defined here and also \
640 re-exported from '{sub_path}'"
641 )));
642 }
643 }
644 functions.insert(name.clone(), Rc::clone(closure));
645 public_names.insert(name);
646 }
647 }
648
649 self.env = caller_env;
650 self.source_dir = old_source_dir;
651
652 Ok(LoadedModule {
653 functions,
654 public_names,
655 })
656 })
657 }
658
659 fn cache_key_for_import(&self, path: &str) -> PathBuf {
664 if let Some(module) = path.strip_prefix("std/") {
665 return PathBuf::from(format!("<stdlib>/{module}.harn"));
666 }
667 let base = self
668 .source_dir
669 .clone()
670 .unwrap_or_else(|| PathBuf::from("."));
671 let file_path = resolve_module_import_path(&base, path);
672 file_path.canonicalize().unwrap_or(file_path)
673 }
674
675 pub async fn load_module_exports(
678 &mut self,
679 path: &Path,
680 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
681 let path_str = path.to_string_lossy().into_owned();
682 self.execute_import(&path_str, None).await?;
683
684 let mut file_path = if path.is_absolute() {
685 path.to_path_buf()
686 } else {
687 self.source_dir
688 .clone()
689 .unwrap_or_else(|| PathBuf::from("."))
690 .join(path)
691 };
692 if !file_path.exists() && file_path.extension().is_none() {
693 file_path.set_extension("harn");
694 }
695
696 let canonical = file_path
697 .canonicalize()
698 .unwrap_or_else(|_| file_path.clone());
699 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
700 VmError::Runtime(format!(
701 "Import error: failed to cache loaded module '{}'",
702 canonical.display()
703 ))
704 })?;
705
706 let export_names: Vec<String> = if loaded.public_names.is_empty() {
707 loaded.functions.keys().cloned().collect()
708 } else {
709 loaded.public_names.iter().cloned().collect()
710 };
711
712 let mut exports = BTreeMap::new();
713 for name in export_names {
714 let Some(closure) = loaded.functions.get(&name) else {
715 return Err(VmError::Runtime(format!(
716 "Import error: exported function '{name}' is missing from {}",
717 canonical.display()
718 )));
719 };
720 exports.insert(name, Rc::clone(closure));
721 }
722
723 Ok(exports)
724 }
725
726 pub async fn load_module_exports_from_source(
729 &mut self,
730 source_key: impl Into<PathBuf>,
731 source: &str,
732 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
733 let synthetic = source_key.into();
734 let loaded = self
735 .load_module_from_source(synthetic.clone(), source)
736 .await?;
737 let export_names: Vec<String> = if loaded.public_names.is_empty() {
738 loaded.functions.keys().cloned().collect()
739 } else {
740 loaded.public_names.iter().cloned().collect()
741 };
742
743 let mut exports = BTreeMap::new();
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, Rc::clone(closure));
752 }
753
754 Ok(exports)
755 }
756
757 pub async fn load_module_exports_from_import(
761 &mut self,
762 import_path: &str,
763 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
764 self.execute_import(import_path, None).await?;
765
766 if let Some(module) = import_path.strip_prefix("std/") {
767 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
768 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
769 VmError::Runtime(format!(
770 "Import error: failed to cache loaded module '{}'",
771 synthetic.display()
772 ))
773 })?;
774 let mut exports = BTreeMap::new();
775 let export_names: Vec<String> = if loaded.public_names.is_empty() {
776 loaded.functions.keys().cloned().collect()
777 } else {
778 loaded.public_names.iter().cloned().collect()
779 };
780 for name in export_names {
781 let Some(closure) = loaded.functions.get(&name) else {
782 return Err(VmError::Runtime(format!(
783 "Import error: exported function '{name}' is missing from {}",
784 synthetic.display()
785 )));
786 };
787 exports.insert(name, Rc::clone(closure));
788 }
789 return Ok(exports);
790 }
791
792 let base = self
793 .source_dir
794 .clone()
795 .unwrap_or_else(|| PathBuf::from("."));
796 let file_path = resolve_module_import_path(&base, import_path);
797 self.load_module_exports(&file_path).await
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use std::rc::Rc;
804 use std::sync::{Mutex, MutexGuard, OnceLock};
805
806 use super::*;
807
808 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
809
810 fn cache_test_guard() -> MutexGuard<'static, ()> {
811 CACHE_TEST_LOCK
812 .get_or_init(|| Mutex::new(()))
813 .lock()
814 .unwrap()
815 }
816
817 #[test]
818 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
819 let _guard = cache_test_guard();
820 reset_stdlib_module_artifact_cache();
821 let runtime = tokio::runtime::Builder::new_current_thread()
822 .enable_all()
823 .build()
824 .expect("runtime builds");
825
826 let first_exports = runtime.block_on(async {
827 let mut first_vm = Vm::new();
828 first_vm
829 .load_module_exports_from_import("std/agent/prompts")
830 .await
831 .expect("first stdlib import succeeds")
832 });
833 assert_eq!(stdlib_module_artifact_cache_len(), 1);
834
835 let second_exports = runtime.block_on(async {
836 let mut second_vm = Vm::new();
837 second_vm
838 .load_module_exports_from_import("std/agent/prompts")
839 .await
840 .expect("second stdlib import succeeds")
841 });
842 assert_eq!(stdlib_module_artifact_cache_len(), 1);
843
844 let first = first_exports
845 .get("render_agent_prompt")
846 .expect("first export exists");
847 let second = second_exports
848 .get("render_agent_prompt")
849 .expect("second export exists");
850
851 assert!(!Rc::ptr_eq(first, second));
852 assert!(!Rc::ptr_eq(&first.func, &second.func));
853 assert!(!Rc::ptr_eq(&first.func.chunk, &second.func.chunk));
854 assert!(!Rc::ptr_eq(
855 first.module_state.as_ref().expect("first module state"),
856 second.module_state.as_ref().expect("second module state")
857 ));
858 }
859
860 #[test]
861 fn stdlib_artifact_cache_is_process_wide_across_threads() {
862 let _guard = cache_test_guard();
863 reset_stdlib_module_artifact_cache();
864
865 let handle = std::thread::spawn(|| {
866 let runtime = tokio::runtime::Builder::new_current_thread()
867 .enable_all()
868 .build()
869 .expect("runtime builds");
870 runtime.block_on(async {
871 let mut vm = Vm::new();
872 vm.load_module_exports_from_import("std/agent/prompts")
873 .await
874 .expect("thread stdlib import succeeds");
875 });
876 });
877 handle.join().expect("thread joins");
878 assert_eq!(stdlib_module_artifact_cache_len(), 1);
879
880 let runtime = tokio::runtime::Builder::new_current_thread()
881 .enable_all()
882 .build()
883 .expect("runtime builds");
884 runtime.block_on(async {
885 let mut vm = Vm::new();
886 vm.load_module_exports_from_import("std/agent/prompts")
887 .await
888 .expect("main-thread stdlib import succeeds");
889 });
890 assert_eq!(stdlib_module_artifact_cache_len(), 1);
891 }
892}