Skip to main content

harn_vm/vm/
modules.rs

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(&registry)),
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    /// Execute an import, reading and running the file's declarations.
389    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    /// Process top-level declarations from an imported module.
461    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            // Route top-level `var`/`let` bindings into a shared
494            // `module_state` rather than `module_env`. If they appeared in
495            // `module_env` (captured by each closure's lexical snapshot),
496            // every call's per-invocation env clone would shadow them and
497            // writes would land in a per-call copy discarded on return.
498            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                    // Save frame state so run_chunk_entry's top-level
517                    // frame-pop doesn't restore self.env.
518                    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                // Imports may carry `@deprecated` / `@test` etc. on top-level
542                // fn decls; transparently peel the wrapper before pattern
543                // matching the FnDecl shape.
544                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(&registry)),
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                // Publish into module_state so sibling fns can be read
578                // as VALUES (e.g. `{handler: other_fn}` or as callbacks).
579                // Closures captured module_env BEFORE fn decls were added,
580                // so their static env alone can't resolve sibling fns.
581                // Direct calls use the module_functions late-binding path;
582                // value reads rely on this module_state entry.
583                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            // Re-export pass: for every `pub import ...` declaration in
595            // the module, surface the imported closures in this module's
596            // `functions`/`public_names` so callers that import this
597            // module see the re-exported names.
598            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    /// Return the path key that `execute_import` would use to cache the
660    /// LoadedModule for this import string. Used by the re-export pass to
661    /// look up the already-loaded source module after `execute_import`
662    /// has populated [`Vm::module_cache`].
663    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    /// Load a module file and return the exported function closures that
676    /// would be visible to a wildcard import.
677    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    /// Load synthetic source keyed by a synthetic module path and return
727    /// the exported function closures that a wildcard import would expose.
728    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    /// Load a module by import path (`std/foo`, relative module path, or
758    /// package import) and return the exported function closures that a
759    /// wildcard import would expose.
760    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}