Skip to main content

harn_vm/vm/
modules.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashSet};
3use std::future::Future;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::rc::Rc;
7
8use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
9
10use super::{ScopeSpan, Vm};
11
12#[derive(Clone)]
13pub(crate) struct LoadedModule {
14    pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
15    pub(crate) public_names: HashSet<String>,
16}
17
18pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
19    let synthetic_current_file = base.join("__harn_import_base__.harn");
20    if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
21        return resolved;
22    }
23
24    let mut file_path = base.join(path);
25
26    if !file_path.exists() && file_path.extension().is_none() {
27        file_path.set_extension("harn");
28    }
29
30    file_path
31}
32
33impl Vm {
34    async fn load_module_from_source(
35        &mut self,
36        synthetic: PathBuf,
37        source: &str,
38    ) -> Result<LoadedModule, VmError> {
39        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
40            return Ok(loaded);
41        }
42
43        let mut lexer = harn_lexer::Lexer::new(source);
44        let tokens = lexer.tokenize().map_err(|e| {
45            VmError::Runtime(format!("Import lex error in {}: {e}", synthetic.display()))
46        })?;
47        let mut parser = harn_parser::Parser::new(tokens);
48        let program = parser.parse().map_err(|e| {
49            VmError::Runtime(format!(
50                "Import parse error in {}: {e}",
51                synthetic.display()
52            ))
53        })?;
54
55        self.imported_paths.push(synthetic.clone());
56        let loaded = self.import_declarations(&program, None).await?;
57        self.imported_paths.pop();
58        self.module_cache.insert(synthetic, loaded.clone());
59        Ok(loaded)
60    }
61
62    fn export_loaded_module(
63        &mut self,
64        module_path: &Path,
65        loaded: &LoadedModule,
66        selected_names: Option<&[String]>,
67    ) -> Result<(), VmError> {
68        let export_names: Vec<String> = if let Some(names) = selected_names {
69            names.to_vec()
70        } else if !loaded.public_names.is_empty() {
71            loaded.public_names.iter().cloned().collect()
72        } else {
73            loaded.functions.keys().cloned().collect()
74        };
75
76        let module_name = module_path.display().to_string();
77        for name in export_names {
78            let Some(closure) = loaded.functions.get(&name) else {
79                return Err(VmError::Runtime(format!(
80                    "Import error: '{name}' is not defined in {module_name}"
81                )));
82            };
83            if let Some(VmValue::Closure(_)) = self.env.get(&name) {
84                return Err(VmError::Runtime(format!(
85                    "Import collision: '{name}' is already defined when importing {module_name}. \
86                     Use selective imports to disambiguate: import {{ {name} }} from \"...\""
87                )));
88            }
89            self.env
90                .define(&name, VmValue::Closure(Rc::clone(closure)), false)?;
91        }
92        Ok(())
93    }
94
95    /// Execute an import, reading and running the file's declarations.
96    pub(super) fn execute_import<'a>(
97        &'a mut self,
98        path: &'a str,
99        selected_names: Option<&'a [String]>,
100    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
101        Box::pin(async move {
102            let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
103
104            if let Some(module) = path.strip_prefix("std/") {
105                if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
106                    let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
107                    if self.imported_paths.contains(&synthetic) {
108                        return Ok(());
109                    }
110                    let loaded = self
111                        .load_module_from_source(synthetic.clone(), source)
112                        .await?;
113                    self.export_loaded_module(&synthetic, &loaded, selected_names)?;
114                    return Ok(());
115                }
116                return Err(VmError::Runtime(format!(
117                    "Unknown stdlib module: std/{module}"
118                )));
119            }
120
121            let base = self
122                .source_dir
123                .clone()
124                .unwrap_or_else(|| PathBuf::from("."));
125            let file_path = resolve_module_import_path(&base, path);
126
127            let canonical = file_path
128                .canonicalize()
129                .unwrap_or_else(|_| file_path.clone());
130            if self.imported_paths.contains(&canonical) {
131                return Ok(());
132            }
133            if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
134                return self.export_loaded_module(&canonical, &loaded, selected_names);
135            }
136            self.imported_paths.push(canonical.clone());
137
138            let source = std::fs::read_to_string(&file_path).map_err(|e| {
139                VmError::Runtime(format!(
140                    "Import error: cannot read '{}': {e}",
141                    file_path.display()
142                ))
143            })?;
144
145            let mut lexer = harn_lexer::Lexer::new(&source);
146            let tokens = lexer
147                .tokenize()
148                .map_err(|e| VmError::Runtime(format!("Import lex error: {e}")))?;
149            let mut parser = harn_parser::Parser::new(tokens);
150            let program = parser
151                .parse()
152                .map_err(|e| VmError::Runtime(format!("Import parse error: {e}")))?;
153
154            let loaded = self.import_declarations(&program, Some(&file_path)).await?;
155            self.imported_paths.pop();
156            self.module_cache.insert(canonical.clone(), loaded.clone());
157            self.export_loaded_module(&canonical, &loaded, selected_names)?;
158
159            Ok(())
160        })
161    }
162
163    /// Process top-level declarations from an imported module.
164    fn import_declarations<'a>(
165        &'a mut self,
166        program: &'a [harn_parser::SNode],
167        file_path: Option<&'a Path>,
168    ) -> Pin<Box<dyn Future<Output = Result<LoadedModule, VmError>> + 'a>> {
169        Box::pin(async move {
170            let caller_env = self.env.clone();
171            let old_source_dir = self.source_dir.clone();
172            self.env = VmEnv::new();
173            if let Some(fp) = file_path {
174                if let Some(parent) = fp.parent() {
175                    self.source_dir = Some(parent.to_path_buf());
176                }
177            }
178
179            for node in program {
180                match &node.node {
181                    harn_parser::Node::ImportDecl { path: sub_path, .. } => {
182                        self.execute_import(sub_path, None).await?;
183                    }
184                    harn_parser::Node::SelectiveImport {
185                        names,
186                        path: sub_path,
187                        ..
188                    } => {
189                        self.execute_import(sub_path, Some(names)).await?;
190                    }
191                    _ => {}
192                }
193            }
194
195            // Route top-level `var`/`let` bindings into a shared
196            // `module_state` rather than `module_env`. If they appeared in
197            // `module_env` (captured by each closure's lexical snapshot),
198            // every call's per-invocation env clone would shadow them and
199            // writes would land in a per-call copy discarded on return.
200            let module_state: crate::value::ModuleState = {
201                let mut init_env = self.env.clone();
202                let init_nodes: Vec<harn_parser::SNode> = program
203                    .iter()
204                    .filter(|sn| {
205                        matches!(
206                            &sn.node,
207                            harn_parser::Node::VarBinding { .. }
208                                | harn_parser::Node::LetBinding { .. }
209                        )
210                    })
211                    .cloned()
212                    .collect();
213                if !init_nodes.is_empty() {
214                    let init_compiler = crate::Compiler::new();
215                    let init_chunk = init_compiler
216                        .compile(&init_nodes)
217                        .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?;
218                    // Save frame state so run_chunk_entry's top-level
219                    // frame-pop doesn't restore self.env.
220                    let saved_env = std::mem::replace(&mut self.env, init_env);
221                    let saved_frames = std::mem::take(&mut self.frames);
222                    let saved_handlers = std::mem::take(&mut self.exception_handlers);
223                    let saved_iterators = std::mem::take(&mut self.iterators);
224                    let saved_deadlines = std::mem::take(&mut self.deadlines);
225                    let init_result = self.run_chunk(&init_chunk).await;
226                    init_env = std::mem::replace(&mut self.env, saved_env);
227                    self.frames = saved_frames;
228                    self.exception_handlers = saved_handlers;
229                    self.iterators = saved_iterators;
230                    self.deadlines = saved_deadlines;
231                    init_result?;
232                }
233                Rc::new(RefCell::new(init_env))
234            };
235
236            let module_env = self.env.clone();
237            let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
238            let source_dir = file_path.and_then(|fp| fp.parent().map(|p| p.to_path_buf()));
239            let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
240            let mut public_names: HashSet<String> = HashSet::new();
241
242            for node in program {
243                // Imports may carry `@deprecated` / `@test` etc. on top-level
244                // fn decls; transparently peel the wrapper before pattern
245                // matching the FnDecl shape.
246                let inner = match &node.node {
247                    harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
248                    _ => node,
249                };
250                let harn_parser::Node::FnDecl {
251                    name,
252                    params,
253                    body,
254                    is_pub,
255                    ..
256                } = &inner.node
257                else {
258                    continue;
259                };
260
261                let mut compiler = crate::Compiler::new();
262                let module_source_file = file_path.map(|p| p.display().to_string());
263                let func_chunk = compiler
264                    .compile_fn_body(params, body, module_source_file)
265                    .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
266                let closure = Rc::new(VmClosure {
267                    func: Rc::new(func_chunk),
268                    env: module_env.clone(),
269                    source_dir: source_dir.clone(),
270                    module_functions: Some(Rc::clone(&registry)),
271                    module_state: Some(Rc::clone(&module_state)),
272                });
273                registry
274                    .borrow_mut()
275                    .insert(name.clone(), Rc::clone(&closure));
276                self.env
277                    .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
278                // Publish into module_state so sibling fns can be read
279                // as VALUES (e.g. `{handler: other_fn}` or as callbacks).
280                // Closures captured module_env BEFORE fn decls were added,
281                // so their static env alone can't resolve sibling fns.
282                // Direct calls use the module_functions late-binding path;
283                // value reads rely on this module_state entry.
284                module_state.borrow_mut().define(
285                    name,
286                    VmValue::Closure(Rc::clone(&closure)),
287                    false,
288                )?;
289                functions.insert(name.clone(), Rc::clone(&closure));
290                if *is_pub {
291                    public_names.insert(name.clone());
292                }
293            }
294
295            // Re-export pass: for every `pub import ...` declaration in
296            // the module, surface the imported closures in this module's
297            // `functions`/`public_names` so callers that import this
298            // module see the re-exported names.
299            for node in program {
300                let (sub_path, selective_names, is_pub_import) = match &node.node {
301                    harn_parser::Node::ImportDecl {
302                        path: sub_path,
303                        is_pub,
304                    } => (sub_path.clone(), None, *is_pub),
305                    harn_parser::Node::SelectiveImport {
306                        names,
307                        path: sub_path,
308                        is_pub,
309                    } => (sub_path.clone(), Some(names.clone()), *is_pub),
310                    _ => continue,
311                };
312                if !is_pub_import {
313                    continue;
314                }
315                let cache_key = self.cache_key_for_import(&sub_path);
316                let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
317                    return Err(VmError::Runtime(format!(
318                        "Re-export error: imported module '{sub_path}' was not loaded"
319                    )));
320                };
321                let names_to_reexport: Vec<String> = match selective_names {
322                    Some(names) => names,
323                    None => {
324                        if loaded.public_names.is_empty() {
325                            loaded.functions.keys().cloned().collect()
326                        } else {
327                            loaded.public_names.iter().cloned().collect()
328                        }
329                    }
330                };
331                for name in names_to_reexport {
332                    let Some(closure) = loaded.functions.get(&name) else {
333                        return Err(VmError::Runtime(format!(
334                            "Re-export error: '{name}' is not exported by '{sub_path}'"
335                        )));
336                    };
337                    if let Some(existing) = functions.get(&name) {
338                        if !Rc::ptr_eq(existing, closure) {
339                            return Err(VmError::Runtime(format!(
340                                "Re-export collision: '{name}' is defined here and also \
341                                 re-exported from '{sub_path}'"
342                            )));
343                        }
344                    }
345                    functions.insert(name.clone(), Rc::clone(closure));
346                    public_names.insert(name);
347                }
348            }
349
350            self.env = caller_env;
351            self.source_dir = old_source_dir;
352
353            Ok(LoadedModule {
354                functions,
355                public_names,
356            })
357        })
358    }
359
360    /// Return the path key that `execute_import` would use to cache the
361    /// LoadedModule for this import string. Used by the re-export pass to
362    /// look up the already-loaded source module after `execute_import`
363    /// has populated [`Vm::module_cache`].
364    fn cache_key_for_import(&self, path: &str) -> PathBuf {
365        if let Some(module) = path.strip_prefix("std/") {
366            return PathBuf::from(format!("<stdlib>/{module}.harn"));
367        }
368        let base = self
369            .source_dir
370            .clone()
371            .unwrap_or_else(|| PathBuf::from("."));
372        let file_path = resolve_module_import_path(&base, path);
373        file_path.canonicalize().unwrap_or(file_path)
374    }
375
376    /// Load a module file and return the exported function closures that
377    /// would be visible to a wildcard import.
378    pub async fn load_module_exports(
379        &mut self,
380        path: &Path,
381    ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
382        let path_str = path.to_string_lossy().into_owned();
383        self.execute_import(&path_str, None).await?;
384
385        let mut file_path = if path.is_absolute() {
386            path.to_path_buf()
387        } else {
388            self.source_dir
389                .clone()
390                .unwrap_or_else(|| PathBuf::from("."))
391                .join(path)
392        };
393        if !file_path.exists() && file_path.extension().is_none() {
394            file_path.set_extension("harn");
395        }
396
397        let canonical = file_path
398            .canonicalize()
399            .unwrap_or_else(|_| file_path.clone());
400        let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
401            VmError::Runtime(format!(
402                "Import error: failed to cache loaded module '{}'",
403                canonical.display()
404            ))
405        })?;
406
407        let export_names: Vec<String> = if loaded.public_names.is_empty() {
408            loaded.functions.keys().cloned().collect()
409        } else {
410            loaded.public_names.iter().cloned().collect()
411        };
412
413        let mut exports = BTreeMap::new();
414        for name in export_names {
415            let Some(closure) = loaded.functions.get(&name) else {
416                return Err(VmError::Runtime(format!(
417                    "Import error: exported function '{name}' is missing from {}",
418                    canonical.display()
419                )));
420            };
421            exports.insert(name, Rc::clone(closure));
422        }
423
424        Ok(exports)
425    }
426
427    /// Load synthetic source keyed by a synthetic module path and return
428    /// the exported function closures that a wildcard import would expose.
429    pub async fn load_module_exports_from_source(
430        &mut self,
431        source_key: impl Into<PathBuf>,
432        source: &str,
433    ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
434        let synthetic = source_key.into();
435        let loaded = self
436            .load_module_from_source(synthetic.clone(), source)
437            .await?;
438        let export_names: Vec<String> = if loaded.public_names.is_empty() {
439            loaded.functions.keys().cloned().collect()
440        } else {
441            loaded.public_names.iter().cloned().collect()
442        };
443
444        let mut exports = BTreeMap::new();
445        for name in export_names {
446            let Some(closure) = loaded.functions.get(&name) else {
447                return Err(VmError::Runtime(format!(
448                    "Import error: exported function '{name}' is missing from {}",
449                    synthetic.display()
450                )));
451            };
452            exports.insert(name, Rc::clone(closure));
453        }
454
455        Ok(exports)
456    }
457
458    /// Load a module by import path (`std/foo`, relative module path, or
459    /// package import) and return the exported function closures that a
460    /// wildcard import would expose.
461    pub async fn load_module_exports_from_import(
462        &mut self,
463        import_path: &str,
464    ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
465        self.execute_import(import_path, None).await?;
466
467        if let Some(module) = import_path.strip_prefix("std/") {
468            let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
469            let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
470                VmError::Runtime(format!(
471                    "Import error: failed to cache loaded module '{}'",
472                    synthetic.display()
473                ))
474            })?;
475            let mut exports = BTreeMap::new();
476            let export_names: Vec<String> = if loaded.public_names.is_empty() {
477                loaded.functions.keys().cloned().collect()
478            } else {
479                loaded.public_names.iter().cloned().collect()
480            };
481            for name in export_names {
482                let Some(closure) = loaded.functions.get(&name) else {
483                    return Err(VmError::Runtime(format!(
484                        "Import error: exported function '{name}' is missing from {}",
485                        synthetic.display()
486                    )));
487                };
488                exports.insert(name, Rc::clone(closure));
489            }
490            return Ok(exports);
491        }
492
493        let base = self
494            .source_dir
495            .clone()
496            .unwrap_or_else(|| PathBuf::from("."));
497        let file_path = resolve_module_import_path(&base, import_path);
498        self.load_module_exports(&file_path).await
499    }
500}