1use 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(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(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
60impl ModuleArtifact {
61 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
68 let source_file = source_path.display().to_string();
69 if let Some(chunk) = &mut self.init_chunk {
70 bind_chunk_source_file(chunk, &source_file);
71 }
72 for function in self.functions.values_mut() {
73 bind_chunk_source_file(&mut function.chunk, &source_file);
74 }
75 }
76}
77
78fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
79 chunk.source_file = Some(source_file.to_string());
80 for function in &mut chunk.functions {
81 bind_chunk_source_file(&mut function.chunk, source_file);
82 }
83}
84
85pub fn compile_module_artifact(
90 program: &[harn_parser::SNode],
91 module_source_file: Option<String>,
92) -> Result<ModuleArtifact, VmError> {
93 let imports = program
94 .iter()
95 .filter_map(|node| match &node.node {
96 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
97 path: path.clone(),
98 selected_names: None,
99 is_pub: *is_pub,
100 }),
101 harn_parser::Node::SelectiveImport {
102 names,
103 path,
104 is_pub,
105 } => Some(ModuleImportSpec {
106 path: path.clone(),
107 selected_names: Some(names.clone()),
108 is_pub: *is_pub,
109 }),
110 _ => None,
111 })
112 .collect();
113
114 let init_nodes: Vec<harn_parser::SNode> = program
115 .iter()
116 .filter(|sn| {
117 let inner = match &sn.node {
118 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
119 _ => sn,
120 };
121 matches!(
122 &inner.node,
123 harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
124 )
125 })
126 .cloned()
127 .collect();
128 let init_chunk = if init_nodes.is_empty() {
129 None
130 } else {
131 let mut compiler = crate::Compiler::new();
132 compiler.collect_type_aliases(program);
133 Some(
134 compiler
135 .compile(&init_nodes)
136 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
137 .freeze_for_cache(),
138 )
139 };
140
141 let mut functions = BTreeMap::new();
142 let mut public_names = HashSet::new();
143 let mut public_value_names = HashSet::new();
144 let mut public_type_names = HashSet::new();
145 for node in program {
146 let inner = match &node.node {
147 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
148 _ => node,
149 };
150 match &inner.node {
151 harn_parser::Node::TypeDecl {
152 name, is_pub: true, ..
153 }
154 | harn_parser::Node::EnumDecl {
155 name, is_pub: true, ..
156 }
157 | harn_parser::Node::InterfaceDecl { name, .. } => {
158 public_type_names.insert(name.clone());
159 continue;
160 }
161 harn_parser::Node::StructDecl {
162 name,
163 fields,
164 is_pub,
165 ..
166 } => {
167 let constructor = crate::Compiler::new()
168 .compile_struct_constructor(name, fields)
169 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
170 functions.insert(name.clone(), constructor.freeze_for_cache());
171 if *is_pub {
172 public_names.insert(name.clone());
173 }
174 continue;
175 }
176 _ => {}
177 }
178 if let harn_parser::Node::ConstBinding {
182 pattern,
183 is_pub: true,
184 ..
185 }
186 | harn_parser::Node::LetBinding {
187 pattern,
188 is_pub: true,
189 ..
190 } = &inner.node
191 {
192 collect_binding_identifier_names(pattern, &mut public_value_names);
193 continue;
194 }
195 if let harn_parser::Node::Pipeline {
196 name,
197 params,
198 body,
199 extends,
200 is_pub,
201 ..
202 } = &inner.node
203 {
204 let pipeline = crate::Compiler::new()
205 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
206 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
207 functions.insert(name.clone(), pipeline.freeze_for_cache());
208 if *is_pub {
209 public_names.insert(name.clone());
210 }
211 continue;
212 }
213 let harn_parser::Node::FnDecl {
214 name,
215 type_params,
216 params,
217 body,
218 is_pub,
219 ..
220 } = &inner.node
221 else {
222 continue;
223 };
224
225 let mut compiler = crate::Compiler::new();
226 compiler.collect_type_aliases(program);
227 let func_chunk = compiler
228 .compile_fn_body(type_params, params, body, module_source_file.clone())
229 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
230 functions.insert(name.clone(), func_chunk.freeze_for_cache());
231 if *is_pub {
232 public_names.insert(name.clone());
233 }
234 }
235
236 let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
237 .into_iter()
238 .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
239 .collect();
240
241 Ok(ModuleArtifact {
242 imports,
243 init_chunk,
244 functions,
245 public_names,
246 public_value_names,
247 public_type_names,
248 public_type_schemas,
249 })
250}
251
252fn collect_binding_identifier_names(
256 pattern: &harn_parser::BindingPattern,
257 out: &mut HashSet<String>,
258) {
259 use harn_parser::BindingPattern;
260 match pattern {
261 BindingPattern::Identifier(name) => {
262 out.insert(name.clone());
263 }
264 BindingPattern::Pair(first, second) => {
265 out.insert(first.clone());
266 out.insert(second.clone());
267 }
268 BindingPattern::List(elements) => {
269 for element in elements {
270 out.insert(element.name.clone());
271 }
272 }
273 BindingPattern::Dict(fields) => {
274 for field in fields {
275 out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
276 }
277 }
278 }
279}
280
281pub fn compile_module_artifact_from_source(
285 source_path: &Path,
286 source: &str,
287) -> Result<ModuleArtifact, VmError> {
288 let mut lexer = harn_lexer::Lexer::new(source);
289 let tokens = lexer.tokenize().map_err(|e| {
290 VmError::Runtime(format!(
291 "Import lex error in {}: {e}",
292 source_path.display()
293 ))
294 })?;
295 let mut parser = harn_parser::Parser::new(tokens);
296 let program = parser.parse().map_err(|e| {
297 VmError::Runtime(format!(
298 "Import parse error in {}: {e}",
299 source_path.display()
300 ))
301 })?;
302 compile_module_artifact(&program, Some(source_path.display().to_string()))
303}
304
305#[cfg(test)]
306mod tests {
307 use std::path::Path;
308
309 use harn_lexer::Lexer;
310 use harn_parser::Parser;
311
312 use super::{compile_module_artifact, compile_module_artifact_from_source};
313 use crate::chunk::Constant;
314
315 #[test]
316 fn module_init_schema_of_uses_full_program_aliases() {
317 let source = r"
318pub type Item = {id: string}
319const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
320";
321 let mut lexer = Lexer::new(source);
322 let tokens = lexer.tokenize().unwrap();
323 let mut parser = Parser::new(tokens);
324 let program = parser.parse().unwrap();
325 let artifact = compile_module_artifact(&program, None).unwrap();
326 let constants = &artifact.init_chunk.expect("init chunk").constants;
327 let strings = constants
328 .iter()
329 .filter_map(|constant| match constant {
330 Constant::String(value) => Some(value.as_str()),
331 _ => None,
332 })
333 .collect::<Vec<_>>();
334 assert!(strings.contains(&"id"), "{strings:?}");
335 assert!(!strings.contains(&"Item"), "{strings:?}");
336 }
337
338 #[test]
339 fn type_only_modules_export_schemas_without_init_bytecode() {
340 let source = r"
341pub type UserShape = {name: string, active?: bool}
342pub type UserList = list<UserShape>
343";
344
345 let artifact =
346 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
347 .expect("module compiles");
348
349 assert!(
350 artifact.init_chunk.is_none(),
351 "erased type aliases must not inflate module init bytecode"
352 );
353 assert!(artifact.public_type_names.contains("UserShape"));
354 assert!(artifact.public_type_names.contains("UserList"));
355 assert!(artifact.public_type_schemas.contains_key("UserShape"));
356 assert!(artifact.public_type_schemas.contains_key("UserList"));
357 }
358}