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