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 serde::Deserialize;
9
10use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
11
12use super::{ScopeSpan, Vm};
13
14#[derive(Clone)]
15pub(crate) struct LoadedModule {
16    pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
17    pub(crate) public_names: HashSet<String>,
18}
19
20#[derive(Debug, Default, Deserialize)]
21struct PackageManifest {
22    #[serde(default)]
23    exports: BTreeMap<String, String>,
24}
25
26fn resolve_package_import(base: &Path, import_path: &str) -> Option<PathBuf> {
27    for anchor in base.ancestors() {
28        let packages_root = anchor.join(".harn/packages");
29        if !packages_root.is_dir() {
30            if anchor.join(".git").exists() {
31                break;
32            }
33            continue;
34        }
35        if let Some(path) = resolve_from_packages_root(&packages_root, import_path) {
36            return Some(path);
37        }
38        if anchor.join(".git").exists() {
39            break;
40        }
41    }
42    None
43}
44
45fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
46    let pkg_path = packages_root.join(import_path);
47    if let Some(path) = finalize_package_target(&pkg_path) {
48        return Some(path);
49    }
50
51    let (package_name, export_name) = import_path.split_once('/')?;
52    let manifest_path = packages_root.join(package_name).join("harn.toml");
53    let manifest = read_package_manifest(&manifest_path)?;
54    let rel_path = manifest.exports.get(export_name)?;
55    finalize_package_target(&packages_root.join(package_name).join(rel_path))
56}
57
58fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
59    let content = std::fs::read_to_string(path).ok()?;
60    toml::from_str::<PackageManifest>(&content).ok()
61}
62
63fn finalize_package_target(path: &Path) -> Option<PathBuf> {
64    if path.is_dir() {
65        let lib = path.join("lib.harn");
66        if lib.exists() {
67            return Some(lib);
68        }
69        return Some(path.to_path_buf());
70    }
71    if path.exists() {
72        return Some(path.to_path_buf());
73    }
74    if path.extension().is_none() {
75        let mut with_ext = path.to_path_buf();
76        with_ext.set_extension("harn");
77        if with_ext.exists() {
78            return Some(with_ext);
79        }
80    }
81    None
82}
83
84impl Vm {
85    fn export_loaded_module(
86        &mut self,
87        module_path: &Path,
88        loaded: &LoadedModule,
89        selected_names: Option<&[String]>,
90    ) -> Result<(), VmError> {
91        let export_names: Vec<String> = if let Some(names) = selected_names {
92            names.to_vec()
93        } else if !loaded.public_names.is_empty() {
94            loaded.public_names.iter().cloned().collect()
95        } else {
96            loaded.functions.keys().cloned().collect()
97        };
98
99        let module_name = module_path.display().to_string();
100        for name in export_names {
101            let Some(closure) = loaded.functions.get(&name) else {
102                return Err(VmError::Runtime(format!(
103                    "Import error: '{name}' is not defined in {module_name}"
104                )));
105            };
106            if let Some(VmValue::Closure(_)) = self.env.get(&name) {
107                return Err(VmError::Runtime(format!(
108                    "Import collision: '{name}' is already defined when importing {module_name}. \
109                     Use selective imports to disambiguate: import {{ {name} }} from \"...\""
110                )));
111            }
112            self.env
113                .define(&name, VmValue::Closure(Rc::clone(closure)), false)?;
114        }
115        Ok(())
116    }
117
118    /// Execute an import, reading and running the file's declarations.
119    pub(super) fn execute_import<'a>(
120        &'a mut self,
121        path: &'a str,
122        selected_names: Option<&'a [String]>,
123    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
124        Box::pin(async move {
125            let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
126
127            if let Some(module) = path.strip_prefix("std/") {
128                if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
129                    let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
130                    if self.imported_paths.contains(&synthetic) {
131                        return Ok(());
132                    }
133                    if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
134                        return self.export_loaded_module(&synthetic, &loaded, selected_names);
135                    }
136                    self.imported_paths.push(synthetic.clone());
137
138                    let mut lexer = harn_lexer::Lexer::new(source);
139                    let tokens = lexer.tokenize().map_err(|e| {
140                        VmError::Runtime(format!("stdlib lex error in std/{module}: {e}"))
141                    })?;
142                    let mut parser = harn_parser::Parser::new(tokens);
143                    let program = parser.parse().map_err(|e| {
144                        VmError::Runtime(format!("stdlib parse error in std/{module}: {e}"))
145                    })?;
146
147                    let loaded = self.import_declarations(&program, None).await?;
148                    self.imported_paths.pop();
149                    self.module_cache.insert(synthetic.clone(), loaded.clone());
150                    self.export_loaded_module(&synthetic, &loaded, selected_names)?;
151                    return Ok(());
152                }
153                return Err(VmError::Runtime(format!(
154                    "Unknown stdlib module: std/{module}"
155                )));
156            }
157
158            let base = self
159                .source_dir
160                .clone()
161                .unwrap_or_else(|| PathBuf::from("."));
162            let mut file_path = base.join(path);
163
164            if !file_path.exists() && file_path.extension().is_none() {
165                file_path.set_extension("harn");
166            }
167
168            if !file_path.exists() {
169                if let Some(resolved) = resolve_package_import(&base, path) {
170                    file_path = resolved;
171                }
172            }
173
174            let canonical = file_path
175                .canonicalize()
176                .unwrap_or_else(|_| file_path.clone());
177            if self.imported_paths.contains(&canonical) {
178                return Ok(());
179            }
180            if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
181                return self.export_loaded_module(&canonical, &loaded, selected_names);
182            }
183            self.imported_paths.push(canonical.clone());
184
185            let source = std::fs::read_to_string(&file_path).map_err(|e| {
186                VmError::Runtime(format!(
187                    "Import error: cannot read '{}': {e}",
188                    file_path.display()
189                ))
190            })?;
191
192            let mut lexer = harn_lexer::Lexer::new(&source);
193            let tokens = lexer
194                .tokenize()
195                .map_err(|e| VmError::Runtime(format!("Import lex error: {e}")))?;
196            let mut parser = harn_parser::Parser::new(tokens);
197            let program = parser
198                .parse()
199                .map_err(|e| VmError::Runtime(format!("Import parse error: {e}")))?;
200
201            let loaded = self.import_declarations(&program, Some(&file_path)).await?;
202            self.imported_paths.pop();
203            self.module_cache.insert(canonical.clone(), loaded.clone());
204            self.export_loaded_module(&canonical, &loaded, selected_names)?;
205
206            Ok(())
207        })
208    }
209
210    /// Process top-level declarations from an imported module.
211    fn import_declarations<'a>(
212        &'a mut self,
213        program: &'a [harn_parser::SNode],
214        file_path: Option<&'a Path>,
215    ) -> Pin<Box<dyn Future<Output = Result<LoadedModule, VmError>> + 'a>> {
216        Box::pin(async move {
217            let caller_env = self.env.clone();
218            let old_source_dir = self.source_dir.clone();
219            self.env = VmEnv::new();
220            if let Some(fp) = file_path {
221                if let Some(parent) = fp.parent() {
222                    self.source_dir = Some(parent.to_path_buf());
223                }
224            }
225
226            for node in program {
227                match &node.node {
228                    harn_parser::Node::ImportDecl { path: sub_path } => {
229                        self.execute_import(sub_path, None).await?;
230                    }
231                    harn_parser::Node::SelectiveImport {
232                        names,
233                        path: sub_path,
234                    } => {
235                        self.execute_import(sub_path, Some(names)).await?;
236                    }
237                    _ => {}
238                }
239            }
240
241            // Route top-level `var`/`let` bindings into a shared
242            // `module_state` rather than `module_env`. If they appeared in
243            // `module_env` (captured by each closure's lexical snapshot),
244            // every call's per-invocation env clone would shadow them and
245            // writes would land in a per-call copy discarded on return.
246            let module_state: crate::value::ModuleState = {
247                let mut init_env = self.env.clone();
248                let init_nodes: Vec<harn_parser::SNode> = program
249                    .iter()
250                    .filter(|sn| {
251                        matches!(
252                            &sn.node,
253                            harn_parser::Node::VarBinding { .. }
254                                | harn_parser::Node::LetBinding { .. }
255                        )
256                    })
257                    .cloned()
258                    .collect();
259                if !init_nodes.is_empty() {
260                    let init_compiler = crate::Compiler::new();
261                    let init_chunk = init_compiler
262                        .compile(&init_nodes)
263                        .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?;
264                    // Save frame state so run_chunk_entry's top-level
265                    // frame-pop doesn't restore self.env.
266                    let saved_env = std::mem::replace(&mut self.env, init_env);
267                    let saved_frames = std::mem::take(&mut self.frames);
268                    let saved_handlers = std::mem::take(&mut self.exception_handlers);
269                    let saved_iterators = std::mem::take(&mut self.iterators);
270                    let saved_deadlines = std::mem::take(&mut self.deadlines);
271                    let init_result = self.run_chunk(&init_chunk).await;
272                    init_env = std::mem::replace(&mut self.env, saved_env);
273                    self.frames = saved_frames;
274                    self.exception_handlers = saved_handlers;
275                    self.iterators = saved_iterators;
276                    self.deadlines = saved_deadlines;
277                    init_result?;
278                }
279                Rc::new(RefCell::new(init_env))
280            };
281
282            let module_env = self.env.clone();
283            let registry: ModuleFunctionRegistry = Rc::new(RefCell::new(BTreeMap::new()));
284            let source_dir = file_path.and_then(|fp| fp.parent().map(|p| p.to_path_buf()));
285            let mut functions: BTreeMap<String, Rc<VmClosure>> = BTreeMap::new();
286            let mut public_names: HashSet<String> = HashSet::new();
287
288            for node in program {
289                // Imports may carry `@deprecated` / `@test` etc. on top-level
290                // fn decls; transparently peel the wrapper before pattern
291                // matching the FnDecl shape.
292                let inner = match &node.node {
293                    harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
294                    _ => node,
295                };
296                let harn_parser::Node::FnDecl {
297                    name,
298                    params,
299                    body,
300                    is_pub,
301                    ..
302                } = &inner.node
303                else {
304                    continue;
305                };
306
307                let mut compiler = crate::Compiler::new();
308                let module_source_file = file_path.map(|p| p.display().to_string());
309                let func_chunk = compiler
310                    .compile_fn_body(params, body, module_source_file)
311                    .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
312                let closure = Rc::new(VmClosure {
313                    func: func_chunk,
314                    env: module_env.clone(),
315                    source_dir: source_dir.clone(),
316                    module_functions: Some(Rc::clone(&registry)),
317                    module_state: Some(Rc::clone(&module_state)),
318                });
319                registry
320                    .borrow_mut()
321                    .insert(name.clone(), Rc::clone(&closure));
322                self.env
323                    .define(name, VmValue::Closure(Rc::clone(&closure)), false)?;
324                // Publish into module_state so sibling fns can be read
325                // as VALUES (e.g. `{handler: other_fn}` or as callbacks).
326                // Closures captured module_env BEFORE fn decls were added,
327                // so their static env alone can't resolve sibling fns.
328                // Direct calls use the module_functions late-binding path;
329                // value reads rely on this module_state entry.
330                module_state.borrow_mut().define(
331                    name,
332                    VmValue::Closure(Rc::clone(&closure)),
333                    false,
334                )?;
335                functions.insert(name.clone(), Rc::clone(&closure));
336                if *is_pub {
337                    public_names.insert(name.clone());
338                }
339            }
340
341            self.env = caller_env;
342            self.source_dir = old_source_dir;
343
344            Ok(LoadedModule {
345                functions,
346                public_names,
347            })
348        })
349    }
350
351    /// Load a module file and return the exported function closures that
352    /// would be visible to a wildcard import.
353    pub async fn load_module_exports(
354        &mut self,
355        path: &Path,
356    ) -> Result<BTreeMap<String, Rc<VmClosure>>, VmError> {
357        let path_str = path.to_string_lossy().into_owned();
358        self.execute_import(&path_str, None).await?;
359
360        let mut file_path = if path.is_absolute() {
361            path.to_path_buf()
362        } else {
363            self.source_dir
364                .clone()
365                .unwrap_or_else(|| PathBuf::from("."))
366                .join(path)
367        };
368        if !file_path.exists() && file_path.extension().is_none() {
369            file_path.set_extension("harn");
370        }
371
372        let canonical = file_path
373            .canonicalize()
374            .unwrap_or_else(|_| file_path.clone());
375        let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
376            VmError::Runtime(format!(
377                "Import error: failed to cache loaded module '{}'",
378                canonical.display()
379            ))
380        })?;
381
382        let export_names: Vec<String> = if loaded.public_names.is_empty() {
383            loaded.functions.keys().cloned().collect()
384        } else {
385            loaded.public_names.iter().cloned().collect()
386        };
387
388        let mut exports = BTreeMap::new();
389        for name in export_names {
390            let Some(closure) = loaded.functions.get(&name) else {
391                return Err(VmError::Runtime(format!(
392                    "Import error: exported function '{name}' is missing from {}",
393                    canonical.display()
394                )));
395            };
396            exports.insert(name, Rc::clone(closure));
397        }
398
399        Ok(exports)
400    }
401}