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_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(&registry)),
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    /// Execute an import, reading and running the file's declarations.
392    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    /// Process top-level declarations from an imported module.
464    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            // Route top-level `var`/`let` bindings into a shared
497            // `module_state` rather than `module_env`. If they appeared in
498            // `module_env` (captured by each closure's lexical snapshot),
499            // every call's per-invocation env clone would shadow them and
500            // writes would land in a per-call copy discarded on return.
501            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                    // Save frame state so run_chunk_entry's top-level
520                    // frame-pop doesn't restore self.env.
521                    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                // Imports may carry `@deprecated` / `@test` etc. on top-level
545                // fn decls; transparently peel the wrapper before pattern
546                // matching the FnDecl shape.
547                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(&registry)),
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                // Publish into module_state so sibling fns can be read
581                // as VALUES (e.g. `{handler: other_fn}` or as callbacks).
582                // Closures captured module_env BEFORE fn decls were added,
583                // so their static env alone can't resolve sibling fns.
584                // Direct calls use the module_functions late-binding path;
585                // value reads rely on this module_state entry.
586                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            // Re-export pass: for every `pub import ...` declaration in
598            // the module, surface the imported closures in this module's
599            // `functions`/`public_names` so callers that import this
600            // module see the re-exported names.
601            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    /// Return the path key that `execute_import` would use to cache the
663    /// LoadedModule for this import string. Used by the re-export pass to
664    /// look up the already-loaded source module after `execute_import`
665    /// has populated [`Vm::module_cache`].
666    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    /// Load a module file and return the exported function closures that
679    /// would be visible to a wildcard import.
680    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    /// Load synthetic source keyed by a synthetic module path and return
730    /// the exported function closures that a wildcard import would expose.
731    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    /// Load a module by import path (`std/foo`, relative module path, or
761    /// package import) and return the exported function closures that a
762    /// wildcard import would expose.
763    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}