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            matches!(
87                &sn.node,
88                harn_parser::Node::VarBinding { .. }
89                    | harn_parser::Node::LetBinding { .. }
90                    | harn_parser::Node::ConstBinding { .. }
91            )
92        })
93        .cloned()
94        .collect();
95    let init_chunk = if init_nodes.is_empty() {
96        None
97    } else {
98        Some(
99            crate::Compiler::new()
100                .compile(&init_nodes)
101                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
102                .freeze_for_cache(),
103        )
104    };
105
106    let mut functions = BTreeMap::new();
107    let mut public_names = HashSet::new();
108    let mut public_type_names = HashSet::new();
109    for node in program {
110        let inner = match &node.node {
111            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
112            _ => node,
113        };
114        if let harn_parser::Node::TypeDecl {
115            name, is_pub: true, ..
116        } = &inner.node
117        {
118            public_type_names.insert(name.clone());
119            continue;
120        }
121        let harn_parser::Node::FnDecl {
122            name,
123            type_params,
124            params,
125            body,
126            is_pub,
127            ..
128        } = &inner.node
129        else {
130            continue;
131        };
132
133        let mut compiler = crate::Compiler::new();
134        let func_chunk = compiler
135            .compile_fn_body(type_params, params, body, module_source_file.clone())
136            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
137        functions.insert(name.clone(), func_chunk.freeze_for_cache());
138        if *is_pub {
139            public_names.insert(name.clone());
140        }
141    }
142
143    let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
144        .into_iter()
145        .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
146        .collect();
147
148    Ok(ModuleArtifact {
149        imports,
150        init_chunk,
151        functions,
152        public_names,
153        public_type_names,
154        public_type_schemas,
155    })
156}
157
158/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
159/// caller already has the raw source bytes and wants the artifact in one
160/// step.
161pub fn compile_module_artifact_from_source(
162    source_path: &Path,
163    source: &str,
164) -> Result<ModuleArtifact, VmError> {
165    let mut lexer = harn_lexer::Lexer::new(source);
166    let tokens = lexer.tokenize().map_err(|e| {
167        VmError::Runtime(format!(
168            "Import lex error in {}: {e}",
169            source_path.display()
170        ))
171    })?;
172    let mut parser = harn_parser::Parser::new(tokens);
173    let program = parser.parse().map_err(|e| {
174        VmError::Runtime(format!(
175            "Import parse error in {}: {e}",
176            source_path.display()
177        ))
178    })?;
179    compile_module_artifact(&program, Some(source_path.display().to_string()))
180}