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    /// Names of top-level `pub const` / `pub let` value bindings. Their values
42    /// are not known at compile time (they are produced by replaying
43    /// [`init_chunk`](Self::init_chunk)); the runtime reads each name out of
44    /// the instantiated module env and binds it into importers. Disjoint from
45    /// [`functions`](Self::functions) and [`public_type_names`](Self::public_type_names).
46    pub public_value_names: HashSet<String>,
47    /// Names of `pub type` aliases. Type aliases are erased at runtime — they
48    /// carry no value of their own — but importers may still name them in a
49    /// selective `import { T } from "..."` (for annotations and
50    /// schema-as-type use), so the loader must accept these names.
51    pub public_type_names: HashSet<String>,
52    /// JSON-Schema lowering (serialized as canonical JSON text) for each
53    /// `pub type` alias whose body can be expressed as a schema. The loader
54    /// binds the imported name to this dict so expression-position uses such
55    /// as `output_schema: ImportedAlias` see the same value a local alias
56    /// lowers to at compile time. Subset of [`public_type_names`](Self::public_type_names).
57    pub public_type_schemas: BTreeMap<String, String>,
58}
59
60/// Compile a parsed `.harn` module into the serializable artifact shape.
61/// Pure compilation — no I/O, no execution. Used by both the runtime
62/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
63/// `harn precompile` CLI subcommand.
64pub fn compile_module_artifact(
65    program: &[harn_parser::SNode],
66    module_source_file: Option<String>,
67) -> Result<ModuleArtifact, VmError> {
68    let imports = program
69        .iter()
70        .filter_map(|node| match &node.node {
71            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
72                path: path.clone(),
73                selected_names: None,
74                is_pub: *is_pub,
75            }),
76            harn_parser::Node::SelectiveImport {
77                names,
78                path,
79                is_pub,
80            } => Some(ModuleImportSpec {
81                path: path.clone(),
82                selected_names: Some(names.clone()),
83                is_pub: *is_pub,
84            }),
85            _ => None,
86        })
87        .collect();
88
89    let init_nodes: Vec<harn_parser::SNode> = program
90        .iter()
91        .filter(|sn| {
92            let inner = match &sn.node {
93                harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
94                _ => sn,
95            };
96            matches!(
97                &inner.node,
98                harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
99            )
100        })
101        .cloned()
102        .collect();
103    let init_chunk = if init_nodes.is_empty() {
104        None
105    } else {
106        Some(
107            crate::Compiler::new()
108                .compile(&init_nodes)
109                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
110                .freeze_for_cache(),
111        )
112    };
113
114    let mut functions = BTreeMap::new();
115    let mut public_names = HashSet::new();
116    let mut public_value_names = HashSet::new();
117    let mut public_type_names = HashSet::new();
118    for node in program {
119        let inner = match &node.node {
120            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
121            _ => node,
122        };
123        if let harn_parser::Node::TypeDecl {
124            name, is_pub: true, ..
125        } = &inner.node
126        {
127            public_type_names.insert(name.clone());
128            continue;
129        }
130        // `pub const` / `pub let`: record the exported value-binding names. The
131        // value itself is produced when the init chunk is replayed at
132        // instantiation time; the runtime reads it out of the module env then.
133        if let harn_parser::Node::ConstBinding {
134            pattern,
135            is_pub: true,
136            ..
137        }
138        | harn_parser::Node::LetBinding {
139            pattern,
140            is_pub: true,
141            ..
142        } = &inner.node
143        {
144            collect_binding_identifier_names(pattern, &mut public_value_names);
145            continue;
146        }
147        let harn_parser::Node::FnDecl {
148            name,
149            type_params,
150            params,
151            body,
152            is_pub,
153            ..
154        } = &inner.node
155        else {
156            continue;
157        };
158
159        let mut compiler = crate::Compiler::new();
160        compiler.collect_type_aliases(program);
161        let func_chunk = compiler
162            .compile_fn_body(type_params, params, body, module_source_file.clone())
163            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
164        functions.insert(name.clone(), func_chunk.freeze_for_cache());
165        if *is_pub {
166            public_names.insert(name.clone());
167        }
168    }
169
170    let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
171        .into_iter()
172        .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
173        .collect();
174
175    Ok(ModuleArtifact {
176        imports,
177        init_chunk,
178        functions,
179        public_names,
180        public_value_names,
181        public_type_names,
182        public_type_schemas,
183    })
184}
185
186/// Collect every plain-identifier name bound by a binding pattern (recursing
187/// into list/dict/pair destructures). Used to enumerate the value names a
188/// `pub const` / `pub let` contributes to a module's public surface.
189fn collect_binding_identifier_names(
190    pattern: &harn_parser::BindingPattern,
191    out: &mut HashSet<String>,
192) {
193    use harn_parser::BindingPattern;
194    match pattern {
195        BindingPattern::Identifier(name) => {
196            out.insert(name.clone());
197        }
198        BindingPattern::Pair(first, second) => {
199            out.insert(first.clone());
200            out.insert(second.clone());
201        }
202        BindingPattern::List(elements) => {
203            for element in elements {
204                out.insert(element.name.clone());
205            }
206        }
207        BindingPattern::Dict(fields) => {
208            for field in fields {
209                out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
210            }
211        }
212    }
213}
214
215/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
216/// caller already has the raw source bytes and wants the artifact in one
217/// step.
218pub fn compile_module_artifact_from_source(
219    source_path: &Path,
220    source: &str,
221) -> Result<ModuleArtifact, VmError> {
222    let mut lexer = harn_lexer::Lexer::new(source);
223    let tokens = lexer.tokenize().map_err(|e| {
224        VmError::Runtime(format!(
225            "Import lex error in {}: {e}",
226            source_path.display()
227        ))
228    })?;
229    let mut parser = harn_parser::Parser::new(tokens);
230    let program = parser.parse().map_err(|e| {
231        VmError::Runtime(format!(
232            "Import parse error in {}: {e}",
233            source_path.display()
234        ))
235    })?;
236    compile_module_artifact(&program, Some(source_path.display().to_string()))
237}
238
239#[cfg(test)]
240mod tests {
241    use std::path::Path;
242
243    use super::compile_module_artifact_from_source;
244
245    #[test]
246    fn type_only_modules_export_schemas_without_init_bytecode() {
247        let source = r"
248pub type UserShape = {name: string, active?: bool}
249pub type UserList = list<UserShape>
250";
251
252        let artifact =
253            compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
254                .expect("module compiles");
255
256        assert!(
257            artifact.init_chunk.is_none(),
258            "erased type aliases must not inflate module init bytecode"
259        );
260        assert!(artifact.public_type_names.contains("UserShape"));
261        assert!(artifact.public_type_names.contains("UserList"));
262        assert!(artifact.public_type_schemas.contains_key("UserShape"));
263        assert!(artifact.public_type_schemas.contains_key("UserList"));
264    }
265}