harn_vm/
module_artifact.rs1use 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#[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#[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 pub public_type_names: HashSet<String>,
46 pub public_type_schemas: BTreeMap<String, String>,
52}
53
54pub 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
161pub 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}