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(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(Debug, Serialize, Deserialize)]
36pub struct ModuleArtifact {
37    pub imports: Vec<ModuleImportSpec>,
38    /// Cached bytecode that materializes exported type aliases after imports
39    /// are bound and before value initialization runs.
40    pub type_schema_init_chunk: Option<CachedChunk>,
41    pub init_chunk: Option<CachedChunk>,
42    pub functions: BTreeMap<String, CachedCompiledFunction>,
43    pub public_names: HashSet<String>,
44    /// Names of top-level `pub const` / `pub let` value bindings. Their values
45    /// are not known at compile time (they are produced by replaying
46    /// [`init_chunk`](Self::init_chunk)); the runtime reads each name out of
47    /// the instantiated module env and binds it into importers. Disjoint from
48    /// [`functions`](Self::functions) and [`public_type_names`](Self::public_type_names).
49    pub public_value_names: HashSet<String>,
50    /// Names of erased public type declarations (`type`, `enum`, and
51    /// `interface`). They carry no runtime value of their own, but importers
52    /// may still name them in selective imports. Public structs are excluded:
53    /// they export a real constructor through [`functions`](Self::functions).
54    pub public_type_names: HashSet<String>,
55}
56
57impl ModuleArtifact {
58    /// Bind relocatable cached bytecode to the source path used by this load.
59    ///
60    /// Module artifacts may move beside their source (`harn precompile`) or
61    /// inside a package. Source paths are diagnostic/debug context, not a
62    /// compilation input, so deserialize once and stamp every nested chunk at
63    /// the load boundary instead of duplicating otherwise-identical artifacts.
64    pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
65        let source_file = source_path.display().to_string();
66        if let Some(chunk) = &mut self.type_schema_init_chunk {
67            bind_chunk_source_file(chunk, &source_file);
68        }
69        if let Some(chunk) = &mut self.init_chunk {
70            bind_chunk_source_file(chunk, &source_file);
71        }
72        for function in self.functions.values_mut() {
73            bind_chunk_source_file(&mut function.chunk, &source_file);
74        }
75    }
76}
77
78fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
79    chunk.source_file = Some(source_file.to_string());
80    for function in &mut chunk.functions {
81        bind_chunk_source_file(&mut function.chunk, source_file);
82    }
83}
84
85/// Compile a parsed `.harn` module into the serializable artifact shape.
86/// Pure compilation — no I/O, no execution. Used by both the runtime
87/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
88/// `harn precompile` CLI subcommand.
89pub fn compile_module_artifact(
90    program: &[harn_parser::SNode],
91    module_source_file: Option<String>,
92) -> Result<ModuleArtifact, VmError> {
93    let imports = program
94        .iter()
95        .filter_map(|node| match &node.node {
96            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
97                path: path.clone(),
98                selected_names: None,
99                is_pub: *is_pub,
100            }),
101            harn_parser::Node::SelectiveImport {
102                names,
103                path,
104                is_pub,
105            } => Some(ModuleImportSpec {
106                path: path.clone(),
107                selected_names: Some(names.clone()),
108                is_pub: *is_pub,
109            }),
110            _ => None,
111        })
112        .collect();
113
114    let init_nodes: Vec<harn_parser::SNode> = program
115        .iter()
116        .filter(|sn| {
117            let inner = match &sn.node {
118                harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
119                _ => sn,
120            };
121            matches!(
122                &inner.node,
123                harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
124            )
125        })
126        .cloned()
127        .collect();
128    let init_chunk = if init_nodes.is_empty() {
129        None
130    } else {
131        let mut compiler = crate::Compiler::new();
132        compiler.seed_module_catalog(program);
133        Some(
134            compiler
135                .compile(&init_nodes)
136                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
137                .freeze_for_cache(),
138        )
139    };
140
141    let mut functions = BTreeMap::new();
142    let mut public_names = HashSet::new();
143    let mut public_value_names = HashSet::new();
144    let mut public_type_names = HashSet::new();
145    for node in program {
146        let inner = match &node.node {
147            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
148            _ => node,
149        };
150        match &inner.node {
151            harn_parser::Node::TypeDecl {
152                name, is_pub: true, ..
153            }
154            | harn_parser::Node::EnumDecl {
155                name, is_pub: true, ..
156            }
157            | harn_parser::Node::InterfaceDecl { name, .. } => {
158                public_type_names.insert(name.clone());
159                continue;
160            }
161            harn_parser::Node::StructDecl {
162                name,
163                fields,
164                is_pub,
165                ..
166            } => {
167                let constructor = crate::Compiler::new()
168                    .compile_struct_constructor(name, fields)
169                    .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
170                functions.insert(name.clone(), constructor.freeze_for_cache());
171                if *is_pub {
172                    public_names.insert(name.clone());
173                }
174                continue;
175            }
176            _ => {}
177        }
178        // `pub const` / `pub let`: record the exported value-binding names. The
179        // value itself is produced when the init chunk is replayed at
180        // instantiation time; the runtime reads it out of the module env then.
181        if let harn_parser::Node::ConstBinding {
182            pattern,
183            is_pub: true,
184            ..
185        }
186        | harn_parser::Node::LetBinding {
187            pattern,
188            is_pub: true,
189            ..
190        } = &inner.node
191        {
192            collect_binding_identifier_names(pattern, &mut public_value_names);
193            continue;
194        }
195        if let harn_parser::Node::Pipeline {
196            name,
197            params,
198            body,
199            extends,
200            is_pub,
201            ..
202        } = &inner.node
203        {
204            let pipeline = crate::Compiler::new()
205                .compile_pipeline_callable(program, name, params, body, extends.as_deref())
206                .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
207            functions.insert(name.clone(), pipeline.freeze_for_cache());
208            if *is_pub {
209                public_names.insert(name.clone());
210            }
211            continue;
212        }
213        let harn_parser::Node::FnDecl {
214            name,
215            type_params,
216            params,
217            body,
218            is_pub,
219            ..
220        } = &inner.node
221        else {
222            continue;
223        };
224
225        let mut compiler = crate::Compiler::new();
226        compiler.seed_module_catalog(program);
227        let func_chunk = compiler
228            .compile_fn_body(type_params, params, body, module_source_file.clone())
229            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
230        functions.insert(name.clone(), func_chunk.freeze_for_cache());
231        if *is_pub {
232            public_names.insert(name.clone());
233        }
234    }
235
236    let type_schema_init_chunk =
237        crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
238            .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
239            .map(|chunk| chunk.freeze_for_cache());
240
241    Ok(ModuleArtifact {
242        imports,
243        type_schema_init_chunk,
244        init_chunk,
245        functions,
246        public_names,
247        public_value_names,
248        public_type_names,
249    })
250}
251
252/// Collect every plain-identifier name bound by a binding pattern (recursing
253/// into list/dict/pair destructures). Used to enumerate the value names a
254/// `pub const` / `pub let` contributes to a module's public surface.
255fn collect_binding_identifier_names(
256    pattern: &harn_parser::BindingPattern,
257    out: &mut HashSet<String>,
258) {
259    use harn_parser::BindingPattern;
260    match pattern {
261        BindingPattern::Identifier(name) => {
262            out.insert(name.clone());
263        }
264        BindingPattern::Pair(first, second) => {
265            out.insert(first.clone());
266            out.insert(second.clone());
267        }
268        BindingPattern::List(elements) => {
269            for element in elements {
270                out.insert(element.name.clone());
271            }
272        }
273        BindingPattern::Dict(fields) => {
274            for field in fields {
275                out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
276            }
277        }
278    }
279}
280
281/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
282/// caller already has the raw source bytes and wants the artifact in one
283/// step.
284pub fn compile_module_artifact_from_source(
285    source_path: &Path,
286    source: &str,
287) -> Result<ModuleArtifact, VmError> {
288    let mut lexer = harn_lexer::Lexer::new(source);
289    let tokens = lexer.tokenize().map_err(|e| {
290        VmError::Runtime(format!(
291            "Import lex error in {}: {e}",
292            source_path.display()
293        ))
294    })?;
295    let mut parser = harn_parser::Parser::new(tokens);
296    let program = parser.parse().map_err(|e| {
297        VmError::Runtime(format!(
298            "Import parse error in {}: {e}",
299            source_path.display()
300        ))
301    })?;
302    compile_module_artifact(&program, Some(source_path.display().to_string()))
303}
304
305#[cfg(test)]
306mod tests {
307    use std::path::Path;
308
309    use harn_lexer::Lexer;
310    use harn_parser::Parser;
311
312    use super::{compile_module_artifact, compile_module_artifact_from_source};
313    use crate::chunk::Constant;
314
315    #[test]
316    fn module_init_schema_of_uses_full_program_aliases() {
317        let source = r"
318pub type Item = {id: string}
319const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
320";
321        let mut lexer = Lexer::new(source);
322        let tokens = lexer.tokenize().unwrap();
323        let mut parser = Parser::new(tokens);
324        let program = parser.parse().unwrap();
325        let artifact = compile_module_artifact(&program, None).unwrap();
326        let constants = &artifact.init_chunk.expect("init chunk").constants;
327        let strings = constants
328            .iter()
329            .filter_map(|constant| match constant {
330                Constant::String(value) => Some(value.as_str()),
331                _ => None,
332            })
333            .collect::<Vec<_>>();
334        assert!(strings.contains(&"id"), "{strings:?}");
335        assert!(!strings.contains(&"Item"), "{strings:?}");
336    }
337
338    #[test]
339    fn type_only_modules_use_a_separate_schema_initializer() {
340        let source = r"
341pub type UserShape = {name: string, active?: bool}
342pub type UserList = list<UserShape>
343";
344
345        let artifact =
346            compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
347                .expect("module compiles");
348
349        assert!(
350            artifact.init_chunk.is_none(),
351            "erased type aliases must not inflate module init bytecode"
352        );
353        assert!(artifact.public_type_names.contains("UserShape"));
354        assert!(artifact.public_type_names.contains("UserList"));
355        assert!(artifact.type_schema_init_chunk.is_some());
356    }
357
358    #[test]
359    fn schema_initializer_keeps_imported_alias_lookup_and_source() {
360        let source = r#"
361import { External } from "./external"
362pub type Wrapped = {value: External}
363"#;
364        let source_path = Path::new("<test>/wrapped.harn");
365        let artifact =
366            compile_module_artifact_from_source(source_path, source).expect("module compiles");
367        let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
368        assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
369        assert!(chunk
370            .constants
371            .iter()
372            .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
373    }
374}