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