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(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 let mut compiler = crate::Compiler::new();
107 compiler.collect_type_aliases(program);
108 Some(
109 compiler
110 .compile(&init_nodes)
111 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
112 .freeze_for_cache(),
113 )
114 };
115
116 let mut functions = BTreeMap::new();
117 let mut public_names = HashSet::new();
118 let mut public_value_names = HashSet::new();
119 let mut public_type_names = HashSet::new();
120 for node in program {
121 let inner = match &node.node {
122 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
123 _ => node,
124 };
125 if let harn_parser::Node::TypeDecl {
126 name, is_pub: true, ..
127 } = &inner.node
128 {
129 public_type_names.insert(name.clone());
130 continue;
131 }
132 if let harn_parser::Node::ConstBinding {
136 pattern,
137 is_pub: true,
138 ..
139 }
140 | harn_parser::Node::LetBinding {
141 pattern,
142 is_pub: true,
143 ..
144 } = &inner.node
145 {
146 collect_binding_identifier_names(pattern, &mut public_value_names);
147 continue;
148 }
149 let harn_parser::Node::FnDecl {
150 name,
151 type_params,
152 params,
153 body,
154 is_pub,
155 ..
156 } = &inner.node
157 else {
158 continue;
159 };
160
161 let mut compiler = crate::Compiler::new();
162 compiler.collect_type_aliases(program);
163 let func_chunk = compiler
164 .compile_fn_body(type_params, params, body, module_source_file.clone())
165 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
166 functions.insert(name.clone(), func_chunk.freeze_for_cache());
167 if *is_pub {
168 public_names.insert(name.clone());
169 }
170 }
171
172 let public_type_schemas = crate::Compiler::lower_public_type_schemas(program)
173 .into_iter()
174 .map(|(name, schema)| (name, crate::stdlib::json::vm_value_to_json(&schema)))
175 .collect();
176
177 Ok(ModuleArtifact {
178 imports,
179 init_chunk,
180 functions,
181 public_names,
182 public_value_names,
183 public_type_names,
184 public_type_schemas,
185 })
186}
187
188fn collect_binding_identifier_names(
192 pattern: &harn_parser::BindingPattern,
193 out: &mut HashSet<String>,
194) {
195 use harn_parser::BindingPattern;
196 match pattern {
197 BindingPattern::Identifier(name) => {
198 out.insert(name.clone());
199 }
200 BindingPattern::Pair(first, second) => {
201 out.insert(first.clone());
202 out.insert(second.clone());
203 }
204 BindingPattern::List(elements) => {
205 for element in elements {
206 out.insert(element.name.clone());
207 }
208 }
209 BindingPattern::Dict(fields) => {
210 for field in fields {
211 out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
212 }
213 }
214 }
215}
216
217pub fn compile_module_artifact_from_source(
221 source_path: &Path,
222 source: &str,
223) -> Result<ModuleArtifact, VmError> {
224 let mut lexer = harn_lexer::Lexer::new(source);
225 let tokens = lexer.tokenize().map_err(|e| {
226 VmError::Runtime(format!(
227 "Import lex error in {}: {e}",
228 source_path.display()
229 ))
230 })?;
231 let mut parser = harn_parser::Parser::new(tokens);
232 let program = parser.parse().map_err(|e| {
233 VmError::Runtime(format!(
234 "Import parse error in {}: {e}",
235 source_path.display()
236 ))
237 })?;
238 compile_module_artifact(&program, Some(source_path.display().to_string()))
239}
240
241#[cfg(test)]
242mod tests {
243 use std::path::Path;
244
245 use harn_lexer::Lexer;
246 use harn_parser::Parser;
247
248 use super::{compile_module_artifact, compile_module_artifact_from_source};
249 use crate::chunk::Constant;
250
251 #[test]
252 fn module_init_schema_of_uses_full_program_aliases() {
253 let source = r"
254pub type Item = {id: string}
255const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
256";
257 let mut lexer = Lexer::new(source);
258 let tokens = lexer.tokenize().unwrap();
259 let mut parser = Parser::new(tokens);
260 let program = parser.parse().unwrap();
261 let artifact = compile_module_artifact(&program, None).unwrap();
262 let constants = &artifact.init_chunk.expect("init chunk").constants;
263 let strings = constants
264 .iter()
265 .filter_map(|constant| match constant {
266 Constant::String(value) => Some(value.as_str()),
267 _ => None,
268 })
269 .collect::<Vec<_>>();
270 assert!(strings.contains(&"id"), "{strings:?}");
271 assert!(!strings.contains(&"Item"), "{strings:?}");
272 }
273
274 #[test]
275 fn type_only_modules_export_schemas_without_init_bytecode() {
276 let source = r"
277pub type UserShape = {name: string, active?: bool}
278pub type UserList = list<UserShape>
279";
280
281 let artifact =
282 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
283 .expect("module compiles");
284
285 assert!(
286 artifact.init_chunk.is_none(),
287 "erased type aliases must not inflate module init bytecode"
288 );
289 assert!(artifact.public_type_names.contains("UserShape"));
290 assert!(artifact.public_type_names.contains("UserList"));
291 assert!(artifact.public_type_schemas.contains_key("UserShape"));
292 assert!(artifact.public_type_schemas.contains_key("UserList"));
293 }
294}