Skip to main content

harn_vm/
module_artifact.rs

1//! Serializable shape of a compiled `.harn` module — the unit the
2//! on-disk module cache stores.
3//!
4//! A module is anything `import` can name: a stdlib file (`std/foo`) or
5//! a user file on disk. The artifact captures **only** the result of
6//! the parse + compile pipeline; instantiation (running the `init`
7//! chunk, creating closures bound to a fresh module env, and applying
8//! re-exports) happens fresh per process and is not cached. This split
9//! lets the cache short-circuit the expensive parse+compile while still
10//! producing the per-process state the runtime needs.
11
12use std::collections::{BTreeMap, HashSet};
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16
17use crate::chunk::{CachedChunk, CachedCompiledFunction};
18use crate::value::VmError;
19
20/// A single `import`-style declaration inside a module. Re-resolved at
21/// instantiation time so that the cached artifact does not bake in
22/// stale resolved paths.
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct ModuleImportSpec {
25    pub path: String,
26    pub selected_names: Option<Vec<String>>,
27    pub is_pub: bool,
28}
29
30/// Serializable compile artifact for one `.harn` module. The runtime
31/// turns this into a loaded module by replaying [`init_chunk`](Self::init_chunk)
32/// into a fresh env, minting closures for each entry in
33/// [`functions`](Self::functions), and re-issuing every nested
34/// [`imports`](Self::imports).
35#[derive(Clone, Debug, Serialize, Deserialize)]
36pub struct ModuleArtifact {
37    pub imports: Vec<ModuleImportSpec>,
38    pub init_chunk: Option<CachedChunk>,
39    pub functions: BTreeMap<String, CachedCompiledFunction>,
40    pub public_names: HashSet<String>,
41}
42
43/// Compile a parsed `.harn` module into the serializable artifact shape.
44/// Pure compilation — no I/O, no execution. Used by both the runtime
45/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
46/// `harn precompile` CLI subcommand.
47pub fn compile_module_artifact(
48    program: &[harn_parser::SNode],
49    module_source_file: Option<String>,
50) -> Result<ModuleArtifact, VmError> {
51    let imports = program
52        .iter()
53        .filter_map(|node| match &node.node {
54            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
55                path: path.clone(),
56                selected_names: None,
57                is_pub: *is_pub,
58            }),
59            harn_parser::Node::SelectiveImport {
60                names,
61                path,
62                is_pub,
63            } => Some(ModuleImportSpec {
64                path: path.clone(),
65                selected_names: Some(names.clone()),
66                is_pub: *is_pub,
67            }),
68            _ => None,
69        })
70        .collect();
71
72    let init_nodes: Vec<harn_parser::SNode> = program
73        .iter()
74        .filter(|sn| {
75            matches!(
76                &sn.node,
77                harn_parser::Node::VarBinding { .. }
78                    | harn_parser::Node::LetBinding { .. }
79                    | harn_parser::Node::ConstBinding { .. }
80            )
81        })
82        .cloned()
83        .collect();
84    let init_chunk = if init_nodes.is_empty() {
85        None
86    } else {
87        Some(
88            crate::Compiler::new()
89                .compile(&init_nodes)
90                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
91                .freeze_for_cache(),
92        )
93    };
94
95    let mut functions = BTreeMap::new();
96    let mut public_names = HashSet::new();
97    for node in program {
98        let inner = match &node.node {
99            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
100            _ => node,
101        };
102        let harn_parser::Node::FnDecl {
103            name,
104            type_params,
105            params,
106            body,
107            is_pub,
108            ..
109        } = &inner.node
110        else {
111            continue;
112        };
113
114        let mut compiler = crate::Compiler::new();
115        let func_chunk = compiler
116            .compile_fn_body(type_params, params, body, module_source_file.clone())
117            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
118        functions.insert(name.clone(), func_chunk.freeze_for_cache());
119        if *is_pub {
120            public_names.insert(name.clone());
121        }
122    }
123
124    Ok(ModuleArtifact {
125        imports,
126        init_chunk,
127        functions,
128        public_names,
129    })
130}
131
132/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
133/// caller already has the raw source bytes and wants the artifact in one
134/// step.
135pub fn compile_module_artifact_from_source(
136    source_path: &Path,
137    source: &str,
138) -> Result<ModuleArtifact, VmError> {
139    let mut lexer = harn_lexer::Lexer::new(source);
140    let tokens = lexer.tokenize().map_err(|e| {
141        VmError::Runtime(format!(
142            "Import lex error in {}: {e}",
143            source_path.display()
144        ))
145    })?;
146    let mut parser = harn_parser::Parser::new(tokens);
147    let program = parser.parse().map_err(|e| {
148        VmError::Runtime(format!(
149            "Import parse error in {}: {e}",
150            source_path.display()
151        ))
152    })?;
153    compile_module_artifact(&program, Some(source_path.display().to_string()))
154}