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