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