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