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 type_schema_init_chunk: Option<CachedChunk>,
41 pub init_chunk: Option<CachedChunk>,
42 pub functions: BTreeMap<String, CachedCompiledFunction>,
43 pub public_names: HashSet<String>,
44 pub public_value_names: HashSet<String>,
50 pub public_type_names: HashSet<String>,
55}
56
57impl ModuleArtifact {
58 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
65 let source_file = source_path.display().to_string();
66 if let Some(chunk) = &mut self.type_schema_init_chunk {
67 bind_chunk_source_file(chunk, &source_file);
68 }
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.seed_module_catalog(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.seed_module_catalog(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 type_schema_init_chunk =
237 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
238 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
239 .map(|chunk| chunk.freeze_for_cache());
240
241 Ok(ModuleArtifact {
242 imports,
243 type_schema_init_chunk,
244 init_chunk,
245 functions,
246 public_names,
247 public_value_names,
248 public_type_names,
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_use_a_separate_schema_initializer() {
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.type_schema_init_chunk.is_some());
356 }
357
358 #[test]
359 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
360 let source = r#"
361import { External } from "./external"
362pub type Wrapped = {value: External}
363"#;
364 let source_path = Path::new("<test>/wrapped.harn");
365 let artifact =
366 compile_module_artifact_from_source(source_path, source).expect("module compiles");
367 let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
368 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
369 assert!(chunk
370 .constants
371 .iter()
372 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
373 }
374}