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