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_value_names: HashSet<String>,
47 pub public_type_names: HashSet<String>,
52 pub public_type_schemas: BTreeMap<String, String>,
58}
59
60pub fn compile_module_artifact(
65 program: &[harn_parser::SNode],
66 module_source_file: Option<String>,
67) -> Result<ModuleArtifact, VmError> {
68 let imports = program
69 .iter()
70 .filter_map(|node| match &node.node {
71 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
72 path: path.clone(),
73 selected_names: None,
74 is_pub: *is_pub,
75 }),
76 harn_parser::Node::SelectiveImport {
77 names,
78 path,
79 is_pub,
80 } => Some(ModuleImportSpec {
81 path: path.clone(),
82 selected_names: Some(names.clone()),
83 is_pub: *is_pub,
84 }),
85 _ => None,
86 })
87 .collect();
88
89 let init_nodes: Vec<harn_parser::SNode> = program
90 .iter()
91 .filter(|sn| {
92 let inner = match &sn.node {
93 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
94 _ => sn,
95 };
96 matches!(
97 &inner.node,
98 harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
99 )
100 })
101 .cloned()
102 .collect();
103 let init_chunk = if init_nodes.is_empty() {
104 None
105 } else {
106 Some(
107 crate::Compiler::new()
108 .compile(&init_nodes)
109 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
110 .freeze_for_cache(),
111 )
112 };
113
114 let mut functions = BTreeMap::new();
115 let mut public_names = HashSet::new();
116 let mut public_value_names = HashSet::new();
117 let mut public_type_names = HashSet::new();
118 for node in program {
119 let inner = match &node.node {
120 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
121 _ => node,
122 };
123 if let harn_parser::Node::TypeDecl {
124 name, is_pub: true, ..
125 } = &inner.node
126 {
127 public_type_names.insert(name.clone());
128 continue;
129 }
130 if let harn_parser::Node::ConstBinding {
134 pattern,
135 is_pub: true,
136 ..
137 }
138 | harn_parser::Node::LetBinding {
139 pattern,
140 is_pub: true,
141 ..
142 } = &inner.node
143 {
144 collect_binding_identifier_names(pattern, &mut public_value_names);
145 continue;
146 }
147 let harn_parser::Node::FnDecl {
148 name,
149 type_params,
150 params,
151 body,
152 is_pub,
153 ..
154 } = &inner.node
155 else {
156 continue;
157 };
158
159 let mut compiler = crate::Compiler::new();
160 compiler.collect_type_aliases(program);
161 let func_chunk = compiler
162 .compile_fn_body(type_params, params, body, module_source_file.clone())
163 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
164 functions.insert(name.clone(), func_chunk.freeze_for_cache());
165 if *is_pub {
166 public_names.insert(name.clone());
167 }
168 }
169
170 let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
171 .into_iter()
172 .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
173 .collect();
174
175 Ok(ModuleArtifact {
176 imports,
177 init_chunk,
178 functions,
179 public_names,
180 public_value_names,
181 public_type_names,
182 public_type_schemas,
183 })
184}
185
186fn collect_binding_identifier_names(
190 pattern: &harn_parser::BindingPattern,
191 out: &mut HashSet<String>,
192) {
193 use harn_parser::BindingPattern;
194 match pattern {
195 BindingPattern::Identifier(name) => {
196 out.insert(name.clone());
197 }
198 BindingPattern::Pair(first, second) => {
199 out.insert(first.clone());
200 out.insert(second.clone());
201 }
202 BindingPattern::List(elements) => {
203 for element in elements {
204 out.insert(element.name.clone());
205 }
206 }
207 BindingPattern::Dict(fields) => {
208 for field in fields {
209 out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
210 }
211 }
212 }
213}
214
215pub fn compile_module_artifact_from_source(
219 source_path: &Path,
220 source: &str,
221) -> Result<ModuleArtifact, VmError> {
222 let mut lexer = harn_lexer::Lexer::new(source);
223 let tokens = lexer.tokenize().map_err(|e| {
224 VmError::Runtime(format!(
225 "Import lex error in {}: {e}",
226 source_path.display()
227 ))
228 })?;
229 let mut parser = harn_parser::Parser::new(tokens);
230 let program = parser.parse().map_err(|e| {
231 VmError::Runtime(format!(
232 "Import parse error in {}: {e}",
233 source_path.display()
234 ))
235 })?;
236 compile_module_artifact(&program, Some(source_path.display().to_string()))
237}
238
239#[cfg(test)]
240mod tests {
241 use std::path::Path;
242
243 use super::compile_module_artifact_from_source;
244
245 #[test]
246 fn type_only_modules_export_schemas_without_init_bytecode() {
247 let source = r"
248pub type UserShape = {name: string, active?: bool}
249pub type UserList = list<UserShape>
250";
251
252 let artifact =
253 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
254 .expect("module compiles");
255
256 assert!(
257 artifact.init_chunk.is_none(),
258 "erased type aliases must not inflate module init bytecode"
259 );
260 assert!(artifact.public_type_names.contains("UserShape"));
261 assert!(artifact.public_type_names.contains("UserList"));
262 assert!(artifact.public_type_schemas.contains_key("UserShape"));
263 assert!(artifact.public_type_schemas.contains_key("UserList"));
264 }
265}