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 Rc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
203
204 let mut lexer = harn_lexer::Lexer::new(source);
205 let tokens = lexer.tokenize().map_err(|e| {
206 VmError::Runtime(format!("Import lex error in {}: {e}", synthetic.display()))
207 })?;
208 let mut parser = harn_parser::Parser::new(tokens);
209 let program = parser.parse().map_err(|e| {
210 VmError::Runtime(format!(
211 "Import parse error in {}: {e}",
212 synthetic.display()
213 ))
214 })?;
215
216 self.imported_paths.push(synthetic.clone());
217 let loaded = self
218 .import_declarations(&program, None, Some(&synthetic))
219 .await?;
220 self.imported_paths.pop();
221 Rc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
222 Ok(loaded)
223 }
224
225 async fn load_stdlib_module_from_source(
226 &mut self,
227 module: &str,
228 synthetic: PathBuf,
229 source: &'static str,
230 ) -> Result<LoadedModule, VmError> {
231 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
232 return Ok(loaded);
233 }
234 Rc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
235
236 let artifact = stdlib_module_artifact(module, &synthetic, source)?;
237 self.imported_paths.push(synthetic.clone());
238 let loaded = self
239 .instantiate_stdlib_module(&synthetic, artifact.as_ref())
240 .await?;
241 self.imported_paths.pop();
242 Rc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
243 Ok(loaded)
244 }
245
246 async fn instantiate_stdlib_module(
247 &mut self,
248 _synthetic: &Path,
249 artifact: &CompiledStdlibModule,
250 ) -> Result<LoadedModule, VmError> {
251 let caller_env = self.env.clone();
252 let old_source_dir = self.source_dir.clone();
253 self.env = VmEnv::new();
254 self.source_dir = None;
255
256 for import in &artifact.imports {
257 self.execute_import(&import.path, import.selected_names.as_deref())
258 .await?;
259 }
260
261 let module_state: crate::value::ModuleState = {
262 let mut init_env = self.env.clone();
263 if let Some(init_chunk) = &artifact.init_chunk {
264 let fresh_init_chunk = Chunk::from_cached(init_chunk);
265 let saved_env = std::mem::replace(&mut self.env, init_env);
266 let saved_frames = std::mem::take(&mut self.frames);
267 let saved_handlers = std::mem::take(&mut self.exception_handlers);
268 let saved_iterators = std::mem::take(&mut self.iterators);
269 let saved_deadlines = std::mem::take(&mut self.deadlines);
270 let init_result = self.run_chunk(&fresh_init_chunk).await;
271 init_env = std::mem::replace(&mut self.env, saved_env);
272 self.frames = saved_frames;
273 self.exception_handlers = saved_handlers;
274 self.iterators = saved_iterators;
275 self.deadlines = saved_deadlines;
276 init_result?;
277 }
278 Rc::new(RefCell::new(init_env))
279 };
280
281 let module_env = self.env.clone();
282 let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
283 let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
284 let mut public_names = artifact.public_names.clone();
285
286 for (name, compiled) in &artifact.functions {
287 let closure = Rc::new(VmClosure {
288 func: Rc::new(CompiledFunction::from_cached(compiled)),
289 env: module_env.clone(),
290 source_dir: None,
291 module_functions: Some(Rc::clone(®istry)),
292 module_state: Some(Rc::clone(&module_state)),
293 });
294 registry
295 .borrow_mut()
296 .insert(name.clone(), Rc::clone(&closure));
297 self.env
298 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
299 module_state
300 .borrow_mut()
301 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
302 functions.insert(name.clone(), Rc::clone(&closure));
303 }
304
305 for import in artifact.imports.iter().filter(|import| import.is_pub) {
306 let cache_key = self.cache_key_for_import(&import.path);
307 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
308 return Err(VmError::Runtime(format!(
309 "Re-export error: imported module '{}' was not loaded",
310 import.path
311 )));
312 };
313 let names_to_reexport: Vec<String> = match &import.selected_names {
314 Some(names) => names.clone(),
315 None => {
316 if loaded.public_names.is_empty() {
317 loaded.functions.keys().cloned().collect()
318 } else {
319 loaded.public_names.iter().cloned().collect()
320 }
321 }
322 };
323 for name in names_to_reexport {
324 let Some(closure) = loaded.functions.get(&name) else {
325 return Err(VmError::Runtime(format!(
326 "Re-export error: '{name}' is not exported by '{}'",
327 import.path
328 )));
329 };
330 if let Some(existing) = functions.get(&name) {
331 if !Rc::ptr_eq(existing, closure) {
332 return Err(VmError::Runtime(format!(
333 "Re-export collision: '{name}' is defined here and also \
334 re-exported from '{}'",
335 import.path
336 )));
337 }
338 }
339 functions.insert(name.clone(), Rc::clone(closure));
340 public_names.insert(name);
341 }
342 }
343
344 self.env = caller_env;
345 self.source_dir = old_source_dir;
346
347 Ok(LoadedModule {
348 functions,
349 public_names,
350 })
351 }
352
353 fn export_loaded_module(
354 &mut self,
355 module_path: &Path,
356 loaded: &LoadedModule,
357 selected_names: Option<&[String]>,
358 ) -> Result<(), VmError> {
359 let export_names: Vec<String> = if let Some(names) = selected_names {
360 names.to_vec()
361 } else if !loaded.public_names.is_empty() {
362 loaded.public_names.iter().cloned().collect()
363 } else {
364 loaded.functions.keys().cloned().collect()
365 };
366
367 let module_name = module_path.display().to_string();
368 for name in export_names {
369 let Some(closure) = loaded.functions.get(&name) else {
370 return Err(VmError::Runtime(format!(
371 "Import error: '{name}' is not defined in {module_name}"
372 )));
373 };
374 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
375 return Err(VmError::Runtime(format!(
376 "Import collision: '{name}' is already defined when importing {module_name}. \
377 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
378 )));
379 }
380 self.env
381 .define(&name, VmValue::Closure(Rc::clone(closure)), false)?;
382 }
383 Ok(())
384 }
385
386 pub(super) fn execute_import<'a>(
388 &'a mut self,
389 path: &'a str,
390 selected_names: Option<&'a [String]>,
391 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
392 Box::pin(async move {
393 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
394
395 if let Some(module) = path.strip_prefix("std/") {
396 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
397 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
398 if self.imported_paths.contains(&synthetic) {
399 return Ok(());
400 }
401 let loaded = self
402 .load_stdlib_module_from_source(module, synthetic.clone(), source)
403 .await?;
404 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
405 return Ok(());
406 }
407 return Err(VmError::Runtime(format!(
408 "Unknown stdlib module: std/{module}"
409 )));
410 }
411
412 let base = self
413 .source_dir
414 .clone()
415 .unwrap_or_else(|| PathBuf::from("."));
416 let file_path = resolve_module_import_path(&base, path);
417
418 let canonical = file_path
419 .canonicalize()
420 .unwrap_or_else(|_| file_path.clone());
421 if self.imported_paths.contains(&canonical) {
422 return Ok(());
423 }
424 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
425 return self.export_loaded_module(&canonical, &loaded, selected_names);
426 }
427 self.imported_paths.push(canonical.clone());
428
429 let source = std::fs::read_to_string(&file_path).map_err(|e| {
430 VmError::Runtime(format!(
431 "Import error: cannot read '{}': {e}",
432 file_path.display()
433 ))
434 })?;
435 Rc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
436 Rc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
437
438 let mut lexer = harn_lexer::Lexer::new(&source);
439 let tokens = lexer
440 .tokenize()
441 .map_err(|e| VmError::Runtime(format!("Import lex error: {e}")))?;
442 let mut parser = harn_parser::Parser::new(tokens);
443 let program = parser
444 .parse()
445 .map_err(|e| VmError::Runtime(format!("Import parse error: {e}")))?;
446
447 let loaded = self
448 .import_declarations(&program, Some(&file_path), Some(&file_path))
449 .await?;
450 self.imported_paths.pop();
451 Rc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
452 self.export_loaded_module(&canonical, &loaded, selected_names)?;
453
454 Ok(())
455 })
456 }
457
458 fn import_declarations<'a>(
460 &'a mut self,
461 program: &'a [harn_parser::SNode],
462 file_path: Option<&'a Path>,
463 debug_source_file: Option<&'a Path>,
464 ) -> Pin<Box<dyn Future<Output = Result<LoadedModule, VmError>> + 'a>> {
465 Box::pin(async move {
466 let caller_env = self.env.clone();
467 let old_source_dir = self.source_dir.clone();
468 self.env = VmEnv::new();
469 if let Some(fp) = file_path {
470 if let Some(parent) = fp.parent() {
471 self.source_dir = Some(parent.to_path_buf());
472 }
473 }
474
475 for node in program {
476 match &node.node {
477 harn_parser::Node::ImportDecl { path: sub_path, .. } => {
478 self.execute_import(sub_path, None).await?;
479 }
480 harn_parser::Node::SelectiveImport {
481 names,
482 path: sub_path,
483 ..
484 } => {
485 self.execute_import(sub_path, Some(names)).await?;
486 }
487 _ => {}
488 }
489 }
490
491 let module_state: crate::value::ModuleState = {
497 let mut init_env = self.env.clone();
498 let init_nodes: Vec<harn_parser::SNode> = program
499 .iter()
500 .filter(|sn| {
501 matches!(
502 &sn.node,
503 harn_parser::Node::VarBinding { .. }
504 | harn_parser::Node::LetBinding { .. }
505 )
506 })
507 .cloned()
508 .collect();
509 if !init_nodes.is_empty() {
510 let init_compiler = crate::Compiler::new();
511 let init_chunk = init_compiler
512 .compile(&init_nodes)
513 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?;
514 let saved_env = std::mem::replace(&mut self.env, init_env);
517 let saved_frames = std::mem::take(&mut self.frames);
518 let saved_handlers = std::mem::take(&mut self.exception_handlers);
519 let saved_iterators = std::mem::take(&mut self.iterators);
520 let saved_deadlines = std::mem::take(&mut self.deadlines);
521 let init_result = self.run_chunk(&init_chunk).await;
522 init_env = std::mem::replace(&mut self.env, saved_env);
523 self.frames = saved_frames;
524 self.exception_handlers = saved_handlers;
525 self.iterators = saved_iterators;
526 self.deadlines = saved_deadlines;
527 init_result?;
528 }
529 Rc::new(RefCell::new(init_env))
530 };
531
532 let module_env = self.env.clone();
533 let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
534 let source_dir = file_path.and_then(|fp| fp.parent().map(|p| p.to_path_buf()));
535 let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
536 let mut public_names: HashSet<String> = HashSet::new();
537
538 for node in program {
539 let inner = match &node.node {
543 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
544 _ => node,
545 };
546 let harn_parser::Node::FnDecl {
547 name,
548 type_params,
549 params,
550 body,
551 is_pub,
552 ..
553 } = &inner.node
554 else {
555 continue;
556 };
557
558 let mut compiler = crate::Compiler::new();
559 let module_source_file = debug_source_file.map(|p| p.display().to_string());
560 let func_chunk = compiler
561 .compile_fn_body(type_params, params, body, module_source_file)
562 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
563 let closure = Rc::new(VmClosure {
564 func: Rc::new(func_chunk),
565 env: module_env.clone(),
566 source_dir: source_dir.clone(),
567 module_functions: Some(Rc::clone(®istry)),
568 module_state: Some(Rc::clone(&module_state)),
569 });
570 registry
571 .borrow_mut()
572 .insert(name.clone(), Rc::clone(&closure));
573 self.env
574 .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
575 module_state.borrow_mut().define(
582 name,
583 VmValue::Closure(Rc::clone(&closure)),
584 false,
585 )?;
586 functions.insert(name.clone(), Rc::clone(&closure));
587 if *is_pub {
588 public_names.insert(name.clone());
589 }
590 }
591
592 for node in program {
597 let (sub_path, selective_names, is_pub_import) = match &node.node {
598 harn_parser::Node::ImportDecl {
599 path: sub_path,
600 is_pub,
601 } => (sub_path.clone(), None, *is_pub),
602 harn_parser::Node::SelectiveImport {
603 names,
604 path: sub_path,
605 is_pub,
606 } => (sub_path.clone(), Some(names.clone()), *is_pub),
607 _ => continue,
608 };
609 if !is_pub_import {
610 continue;
611 }
612 let cache_key = self.cache_key_for_import(&sub_path);
613 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
614 return Err(VmError::Runtime(format!(
615 "Re-export error: imported module '{sub_path}' was not loaded"
616 )));
617 };
618 let names_to_reexport: Vec<String> = match selective_names {
619 Some(names) => names,
620 None => {
621 if loaded.public_names.is_empty() {
622 loaded.functions.keys().cloned().collect()
623 } else {
624 loaded.public_names.iter().cloned().collect()
625 }
626 }
627 };
628 for name in names_to_reexport {
629 let Some(closure) = loaded.functions.get(&name) else {
630 return Err(VmError::Runtime(format!(
631 "Re-export error: '{name}' is not exported by '{sub_path}'"
632 )));
633 };
634 if let Some(existing) = functions.get(&name) {
635 if !Rc::ptr_eq(existing, closure) {
636 return Err(VmError::Runtime(format!(
637 "Re-export collision: '{name}' is defined here and also \
638 re-exported from '{sub_path}'"
639 )));
640 }
641 }
642 functions.insert(name.clone(), Rc::clone(closure));
643 public_names.insert(name);
644 }
645 }
646
647 self.env = caller_env;
648 self.source_dir = old_source_dir;
649
650 Ok(LoadedModule {
651 functions,
652 public_names,
653 })
654 })
655 }
656
657 fn cache_key_for_import(&self, path: &str) -> PathBuf {
662 if let Some(module) = path.strip_prefix("std/") {
663 return PathBuf::from(format!("<stdlib>/{module}.harn"));
664 }
665 let base = self
666 .source_dir
667 .clone()
668 .unwrap_or_else(|| PathBuf::from("."));
669 let file_path = resolve_module_import_path(&base, path);
670 file_path.canonicalize().unwrap_or(file_path)
671 }
672
673 pub async fn load_module_exports(
676 &mut self,
677 path: &Path,
678 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
679 let path_str = path.to_string_lossy().into_owned();
680 self.execute_import(&path_str, None).await?;
681
682 let mut file_path = if path.is_absolute() {
683 path.to_path_buf()
684 } else {
685 self.source_dir
686 .clone()
687 .unwrap_or_else(|| PathBuf::from("."))
688 .join(path)
689 };
690 if !file_path.exists() && file_path.extension().is_none() {
691 file_path.set_extension("harn");
692 }
693
694 let canonical = file_path
695 .canonicalize()
696 .unwrap_or_else(|_| file_path.clone());
697 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
698 VmError::Runtime(format!(
699 "Import error: failed to cache loaded module '{}'",
700 canonical.display()
701 ))
702 })?;
703
704 let export_names: Vec<String> = if loaded.public_names.is_empty() {
705 loaded.functions.keys().cloned().collect()
706 } else {
707 loaded.public_names.iter().cloned().collect()
708 };
709
710 let mut exports = BTreeMap::new();
711 for name in export_names {
712 let Some(closure) = loaded.functions.get(&name) else {
713 return Err(VmError::Runtime(format!(
714 "Import error: exported function '{name}' is missing from {}",
715 canonical.display()
716 )));
717 };
718 exports.insert(name, Rc::clone(closure));
719 }
720
721 Ok(exports)
722 }
723
724 pub async fn load_module_exports_from_source(
727 &mut self,
728 source_key: impl Into<PathBuf>,
729 source: &str,
730 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
731 let synthetic = source_key.into();
732 let loaded = self
733 .load_module_from_source(synthetic.clone(), source)
734 .await?;
735 let export_names: Vec<String> = if loaded.public_names.is_empty() {
736 loaded.functions.keys().cloned().collect()
737 } else {
738 loaded.public_names.iter().cloned().collect()
739 };
740
741 let mut exports = BTreeMap::new();
742 for name in export_names {
743 let Some(closure) = loaded.functions.get(&name) else {
744 return Err(VmError::Runtime(format!(
745 "Import error: exported function '{name}' is missing from {}",
746 synthetic.display()
747 )));
748 };
749 exports.insert(name, Rc::clone(closure));
750 }
751
752 Ok(exports)
753 }
754
755 pub async fn load_module_exports_from_import(
759 &mut self,
760 import_path: &str,
761 ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
762 self.execute_import(import_path, None).await?;
763
764 if let Some(module) = import_path.strip_prefix("std/") {
765 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
766 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
767 VmError::Runtime(format!(
768 "Import error: failed to cache loaded module '{}'",
769 synthetic.display()
770 ))
771 })?;
772 let mut exports = BTreeMap::new();
773 let export_names: Vec<String> = if loaded.public_names.is_empty() {
774 loaded.functions.keys().cloned().collect()
775 } else {
776 loaded.public_names.iter().cloned().collect()
777 };
778 for name in export_names {
779 let Some(closure) = loaded.functions.get(&name) else {
780 return Err(VmError::Runtime(format!(
781 "Import error: exported function '{name}' is missing from {}",
782 synthetic.display()
783 )));
784 };
785 exports.insert(name, Rc::clone(closure));
786 }
787 return Ok(exports);
788 }
789
790 let base = self
791 .source_dir
792 .clone()
793 .unwrap_or_else(|| PathBuf::from("."));
794 let file_path = resolve_module_import_path(&base, import_path);
795 self.load_module_exports(&file_path).await
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use std::rc::Rc;
802 use std::sync::{Mutex, MutexGuard, OnceLock};
803
804 use super::*;
805
806 static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
807
808 fn cache_test_guard() -> MutexGuard<'static, ()> {
809 CACHE_TEST_LOCK
810 .get_or_init(|| Mutex::new(()))
811 .lock()
812 .unwrap()
813 }
814
815 #[test]
816 fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
817 let _guard = cache_test_guard();
818 reset_stdlib_module_artifact_cache();
819 let runtime = tokio::runtime::Builder::new_current_thread()
820 .enable_all()
821 .build()
822 .expect("runtime builds");
823
824 let first_exports = runtime.block_on(async {
825 let mut first_vm = Vm::new();
826 first_vm
827 .load_module_exports_from_import("std/agent/prompts")
828 .await
829 .expect("first stdlib import succeeds")
830 });
831 assert_eq!(stdlib_module_artifact_cache_len(), 1);
832
833 let second_exports = runtime.block_on(async {
834 let mut second_vm = Vm::new();
835 second_vm
836 .load_module_exports_from_import("std/agent/prompts")
837 .await
838 .expect("second stdlib import succeeds")
839 });
840 assert_eq!(stdlib_module_artifact_cache_len(), 1);
841
842 let first = first_exports
843 .get("render_agent_prompt")
844 .expect("first export exists");
845 let second = second_exports
846 .get("render_agent_prompt")
847 .expect("second export exists");
848
849 assert!(!Rc::ptr_eq(first, second));
850 assert!(!Rc::ptr_eq(&first.func, &second.func));
851 assert!(!Rc::ptr_eq(&first.func.chunk, &second.func.chunk));
852 assert!(!Rc::ptr_eq(
853 first.module_state.as_ref().expect("first module state"),
854 second.module_state.as_ref().expect("second module state")
855 ));
856 }
857
858 #[test]
859 fn stdlib_artifact_cache_is_process_wide_across_threads() {
860 let _guard = cache_test_guard();
861 reset_stdlib_module_artifact_cache();
862
863 let handle = std::thread::spawn(|| {
864 let runtime = tokio::runtime::Builder::new_current_thread()
865 .enable_all()
866 .build()
867 .expect("runtime builds");
868 runtime.block_on(async {
869 let mut vm = Vm::new();
870 vm.load_module_exports_from_import("std/agent/prompts")
871 .await
872 .expect("thread stdlib import succeeds");
873 });
874 });
875 handle.join().expect("thread joins");
876 assert_eq!(stdlib_module_artifact_cache_len(), 1);
877
878 let runtime = tokio::runtime::Builder::new_current_thread()
879 .enable_all()
880 .build()
881 .expect("runtime builds");
882 runtime.block_on(async {
883 let mut vm = Vm::new();
884 vm.load_module_exports_from_import("std/agent/prompts")
885 .await
886 .expect("main-thread stdlib import succeeds");
887 });
888 assert_eq!(stdlib_module_artifact_cache_len(), 1);
889 }
890}