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 `pub type` aliases. Type aliases are erased at runtime — they
42    /// carry no value of their own — but importers may still name them in a
43    /// selective `import { T } from "..."` (for annotations and
44    /// schema-as-type use), so the loader must accept these names.
45    pub public_type_names: HashSet<String>,
46    /// JSON-Schema lowering (serialized as canonical JSON text) for each
47    /// `pub type` alias whose body can be expressed as a schema. The loader
48    /// binds the imported name to this dict so expression-position uses such
49    /// as `output_schema: ImportedAlias` see the same value a local alias
50    /// lowers to at compile time. Subset of [`public_type_names`](Self::public_type_names).
51    pub public_type_schemas: BTreeMap<String, String>,
52}
53
54/// Compile a parsed `.harn` module into the serializable artifact shape.
55/// Pure compilation — no I/O, no execution. Used by both the runtime
56/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
57/// `harn precompile` CLI subcommand.
58pub fn compile_module_artifact(
59    program: &[harn_parser::SNode],
60    module_source_file: Option<String>,
61) -> Result<ModuleArtifact, VmError> {
62    let imports = program
63        .iter()
64        .filter_map(|node| match &node.node {
65            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
66                path: path.clone(),
67                selected_names: None,
68                is_pub: *is_pub,
69            }),
70            harn_parser::Node::SelectiveImport {
71                names,
72                path,
73                is_pub,
74            } => Some(ModuleImportSpec {
75                path: path.clone(),
76                selected_names: Some(names.clone()),
77                is_pub: *is_pub,
78            }),
79            _ => None,
80        })
81        .collect();
82
83    let init_nodes: Vec<harn_parser::SNode> = program
84        .iter()
85        .filter(|sn| {
86            let inner = match &sn.node {
87                harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
88                _ => sn,
89            };
90            matches!(
91                &inner.node,
92                harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
93            )
94        })
95        .cloned()
96        .collect();
97    let init_chunk = if init_nodes.is_empty() {
98        None
99    } else {
100        Some(
101            crate::Compiler::new()
102                .compile(&init_nodes)
103                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
104                .freeze_for_cache(),
105        )
106    };
107
108    let mut functions = BTreeMap::new();
109    let mut public_names = HashSet::new();
110    let mut public_type_names = HashSet::new();
111    for node in program {
112        let inner = match &node.node {
113            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
114            _ => node,
115        };
116        if let harn_parser::Node::TypeDecl {
117            name, is_pub: true, ..
118        } = &inner.node
119        {
120            public_type_names.insert(name.clone());
121            continue;
122        }
123        let harn_parser::Node::FnDecl {
124            name,
125            type_params,
126            params,
127            body,
128            is_pub,
129            ..
130        } = &inner.node
131        else {
132            continue;
133        };
134
135        let mut compiler = crate::Compiler::new();
136        compiler.collect_type_aliases(program);
137        let func_chunk = compiler
138            .compile_fn_body(type_params, params, body, module_source_file.clone())
139            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
140        functions.insert(name.clone(), func_chunk.freeze_for_cache());
141        if *is_pub {
142            public_names.insert(name.clone());
143        }
144    }
145
146    let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
147        .into_iter()
148        .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
149        .collect();
150
151    Ok(ModuleArtifact {
152        imports,
153        init_chunk,
154        functions,
155        public_names,
156        public_type_names,
157        public_type_schemas,
158    })
159}
160
161/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
162/// caller already has the raw source bytes and wants the artifact in one
163/// step.
164pub fn compile_module_artifact_from_source(
165    source_path: &Path,
166    source: &str,
167) -> Result<ModuleArtifact, VmError> {
168    let mut lexer = harn_lexer::Lexer::new(source);
169    let tokens = lexer.tokenize().map_err(|e| {
170        VmError::Runtime(format!(
171            "Import lex error in {}: {e}",
172            source_path.display()
173        ))
174    })?;
175    let mut parser = harn_parser::Parser::new(tokens);
176    let program = parser.parse().map_err(|e| {
177        VmError::Runtime(format!(
178            "Import parse error in {}: {e}",
179            source_path.display()
180        ))
181    })?;
182    compile_module_artifact(&program, Some(source_path.display().to_string()))
183}
184
185#[cfg(test)]
186mod tests {
187    use std::path::Path;
188
189    use super::compile_module_artifact_from_source;
190
191    #[test]
192    fn type_only_modules_export_schemas_without_init_bytecode() {
193        let source = r"
194pub type UserShape = {name: string, active?: bool}
195pub type UserList = list<UserShape>
196";
197
198        let artifact =
199            compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
200                .expect("module compiles");
201
202        assert!(
203            artifact.init_chunk.is_none(),
204            "erased type aliases must not inflate module init bytecode"
205        );
206        assert!(artifact.public_type_names.contains("UserShape"));
207        assert!(artifact.public_type_names.contains("UserList"));
208        assert!(artifact.public_type_schemas.contains_key("UserShape"));
209        assert!(artifact.public_type_schemas.contains_key("UserList"));
210    }
211}