1use std::collections::{BTreeMap, HashSet};
13use std::path::{Path, PathBuf};
14use std::sync::{Mutex, OnceLock};
15
16use harn_modules::{public_declarations, DefKind};
17use serde::{Deserialize, Serialize};
18
19use crate::chunk::{CachedChunk, CachedCompiledFunction};
20use crate::value::VmError;
21
22type ImportedEnumCache = BTreeMap<PathBuf, ([u8; 32], Vec<String>)>;
23
24fn imported_enum_cache() -> &'static Mutex<ImportedEnumCache> {
25 static CACHE: OnceLock<Mutex<ImportedEnumCache>> = OnceLock::new();
26 CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
27}
28
29#[derive(Debug, Serialize, Deserialize)]
33pub struct ModuleImportSpec {
34 pub path: String,
35 pub selected_names: Option<Vec<String>>,
36 #[serde(default)]
39 pub namespace_alias: Option<String>,
40 pub is_pub: bool,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
49pub struct ModuleArtifact {
50 pub imports: Vec<ModuleImportSpec>,
51 pub type_schema_init_chunk: Option<CachedChunk>,
54 pub init_chunk: Option<CachedChunk>,
55 pub functions: BTreeMap<String, CachedCompiledFunction>,
56 pub public_exports: BTreeMap<String, DefKind>,
61 pub public_value_names: HashSet<String>,
65 pub public_type_names: HashSet<String>,
70}
71
72impl ModuleArtifact {
73 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
80 let source_file = source_path.display().to_string();
81 if let Some(chunk) = &mut self.type_schema_init_chunk {
82 bind_chunk_source_file(chunk, &source_file);
83 }
84 if let Some(chunk) = &mut self.init_chunk {
85 bind_chunk_source_file(chunk, &source_file);
86 }
87 for function in self.functions.values_mut() {
88 bind_chunk_source_file(&mut function.chunk, &source_file);
89 }
90 }
91}
92
93fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
94 chunk.source_file = Some(source_file.to_string());
95 for function in &mut chunk.functions {
96 bind_chunk_source_file(&mut function.chunk, source_file);
97 }
98}
99
100pub fn compile_module_artifact(
105 program: &[harn_parser::SNode],
106 module_source_file: Option<String>,
107) -> Result<ModuleArtifact, VmError> {
108 let imported_enum_candidates = module_source_file
109 .as_deref()
110 .filter(|_| needs_imported_enum_candidates(program))
111 .and_then(|path| {
112 harn_modules::build(&[Path::new(path).to_path_buf()])
113 .imported_names_by_kind_for_file(Path::new(path), DefKind::Enum)
114 })
115 .unwrap_or_default();
116 compile_module_artifact_with_imported_enums(
117 program,
118 module_source_file,
119 &imported_enum_candidates.into_iter().collect::<Vec<_>>(),
120 )
121}
122
123fn compile_module_artifact_with_imported_enums(
124 program: &[harn_parser::SNode],
125 module_source_file: Option<String>,
126 imported_enum_candidates: &[String],
127) -> Result<ModuleArtifact, VmError> {
128 let imports = program
129 .iter()
130 .filter_map(|node| match &node.node {
131 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
132 path: path.clone(),
133 selected_names: None,
134 namespace_alias: None,
135 is_pub: *is_pub,
136 }),
137 harn_parser::Node::SelectiveImport {
138 names,
139 path,
140 is_pub,
141 } => Some(ModuleImportSpec {
142 path: path.clone(),
143 selected_names: Some(names.clone()),
144 namespace_alias: None,
145 is_pub: *is_pub,
146 }),
147 harn_parser::Node::NamespaceImport {
148 alias,
149 path,
150 is_pub,
151 } => Some(ModuleImportSpec {
152 path: path.clone(),
153 selected_names: None,
154 namespace_alias: Some(alias.clone()),
155 is_pub: *is_pub,
156 }),
157 _ => None,
158 })
159 .collect();
160
161 let init_nodes: Vec<harn_parser::SNode> = program
162 .iter()
163 .filter(|sn| {
164 let inner = match &sn.node {
165 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
166 _ => sn,
167 };
168 matches!(
169 &inner.node,
170 harn_parser::Node::LetBinding { .. }
171 | harn_parser::Node::ConstBinding { .. }
172 | harn_parser::Node::EnumDecl { is_pub: true, .. }
179 | harn_parser::Node::ToolDecl { .. }
180 | harn_parser::Node::SkillDecl { .. }
181 | harn_parser::Node::EvalPackDecl { .. }
182 )
183 })
184 .cloned()
185 .collect();
186 let init_chunk = if init_nodes.is_empty() {
187 None
188 } else {
189 let compiler = crate::Compiler::new();
190 Some(
191 compiler
192 .compile_module_init(program, &init_nodes, imported_enum_candidates)
193 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
194 .freeze_for_cache(),
195 )
196 };
197
198 let public_exports: BTreeMap<String, DefKind> = program
199 .iter()
200 .flat_map(public_declarations)
201 .map(|export| (export.name, export.kind))
202 .collect();
203 let public_value_names = public_exports
204 .iter()
205 .filter(|(_, kind)| {
206 matches!(
207 kind,
208 DefKind::Variable
209 | DefKind::Enum
210 | DefKind::Tool
211 | DefKind::Skill
212 | DefKind::EvalPack
213 )
214 })
215 .map(|(name, _)| name.clone())
216 .collect();
217 let public_type_names = public_exports
218 .iter()
219 .filter(|(_, kind)| !kind.has_runtime_value())
220 .map(|(name, _)| name.clone())
221 .collect();
222
223 let mut functions = BTreeMap::new();
224 for node in program {
225 let inner = match &node.node {
226 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
227 _ => node,
228 };
229 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
230 let constructor = crate::Compiler::new()
235 .compile_struct_constructor(name, fields)
236 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
237 functions.insert(name.clone(), constructor.freeze_for_cache());
238 continue;
239 }
240 if let harn_parser::Node::Pipeline {
241 name,
242 params,
243 body,
244 extends,
245 ..
246 } = &inner.node
247 {
248 let mut compiler = crate::Compiler::new();
249 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
250 let pipeline = compiler
251 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
252 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
253 functions.insert(name.clone(), pipeline.freeze_for_cache());
254 continue;
255 }
256 let harn_parser::Node::FnDecl {
257 name,
258 type_params,
259 params,
260 body,
261 ..
262 } = &inner.node
263 else {
264 continue;
265 };
266
267 let mut compiler = crate::Compiler::new();
268 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
269 compiler.prepare_module_context(program);
270 let func_chunk = compiler
271 .compile_fn_body(type_params, params, body, module_source_file.clone())
272 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
273 functions.insert(name.clone(), func_chunk.freeze_for_cache());
274 }
275
276 let type_schema_init_chunk =
277 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
278 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
279 .map(|chunk| chunk.freeze_for_cache());
280
281 Ok(ModuleArtifact {
282 imports,
283 type_schema_init_chunk,
284 init_chunk,
285 functions,
286 public_exports,
287 public_value_names,
288 public_type_names,
289 })
290}
291
292pub fn compile_module_artifact_from_source(
296 source_path: &Path,
297 source: &str,
298) -> Result<ModuleArtifact, VmError> {
299 let program = parse_module_source(source_path, source)?;
300 let imported_enum_candidates =
301 imported_enum_candidates_for_program(source_path, source, &program);
302 compile_module_artifact_with_imported_enums(
303 &program,
304 Some(source_path.display().to_string()),
305 &imported_enum_candidates,
306 )
307}
308
309fn imported_enum_candidates_for_program(
314 source_path: &Path,
315 source: &str,
316 program: &[harn_parser::SNode],
317) -> Vec<String> {
318 if !needs_imported_enum_candidates(program) {
319 return Vec::new();
320 }
321 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
322 let cache_key = harn_modules::canonical_path(source_path);
323 let cacheable = is_immutable_stdlib_path(source_path);
324 if cacheable {
325 if let Some((_cached_hash, candidates)) = imported_enum_cache()
326 .lock()
327 .expect("imported enum cache lock poisoned")
328 .get(&cache_key)
329 .filter(|(cached_hash, _)| *cached_hash == source_hash)
330 {
331 return candidates.clone();
332 }
333 }
334
335 let graph = harn_modules::build_with_source(source_path, source);
340 if !cacheable {
341 return sorted_imported_enum_candidates(&graph, source_path);
342 }
343 let mut projections = Vec::new();
344 for path in graph.module_paths() {
345 let module_source = if path == cache_key {
346 Some(source.to_string())
347 } else {
348 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
349 };
350 let Some(module_source) = module_source else {
351 continue;
352 };
353 let candidates = sorted_imported_enum_candidates(&graph, &path);
354 projections.push((
355 path,
356 (
357 *blake3::hash(module_source.as_bytes()).as_bytes(),
358 candidates,
359 ),
360 ));
361 }
362 let mut cache = imported_enum_cache()
363 .lock()
364 .expect("imported enum cache lock poisoned");
365 for (path, projection) in projections {
366 if is_immutable_stdlib_path(&path) {
367 cache.insert(path, projection);
368 }
369 }
370 cache
371 .get(&cache_key)
372 .filter(|(cached_hash, _)| *cached_hash == source_hash)
373 .map(|(_, candidates)| candidates.clone())
374 .unwrap_or_default()
375}
376
377fn sorted_imported_enum_candidates(
378 graph: &harn_modules::ModuleGraph,
379 source_path: &Path,
380) -> Vec<String> {
381 let mut candidates = graph
382 .imported_names_by_kind_for_file(source_path, DefKind::Enum)
383 .unwrap_or_default()
384 .into_iter()
385 .collect::<Vec<_>>();
386 candidates.sort_unstable();
387 candidates
388}
389
390fn is_immutable_stdlib_path(path: &Path) -> bool {
391 path.to_str()
392 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
393}
394
395fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
396 harn_parser::visit::contains_identifier_enum_pattern(program)
397}
398
399fn parse_module_source(
400 source_path: &Path,
401 source: &str,
402) -> Result<Vec<harn_parser::SNode>, VmError> {
403 let mut lexer = harn_lexer::Lexer::new(source);
404 let tokens = lexer.tokenize().map_err(|e| {
405 VmError::Runtime(format!(
406 "Import lex error in {}: {e}",
407 source_path.display()
408 ))
409 })?;
410 let mut parser = harn_parser::Parser::new(tokens);
411 parser.parse().map_err(|e| {
412 VmError::Runtime(format!(
413 "Import parse error in {}: {e}",
414 source_path.display()
415 ))
416 })
417}
418
419pub fn compile_module_artifact_from_source_with_imported_enums(
424 source_path: &Path,
425 source: &str,
426 imported_enum_candidates: impl IntoIterator<Item = String>,
427) -> Result<ModuleArtifact, VmError> {
428 let program = parse_module_source(source_path, source)?;
429 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
430 compile_module_artifact_with_imported_enums(
431 &program,
432 Some(source_path.display().to_string()),
433 &imported_enum_candidates,
434 )
435}
436
437#[cfg(test)]
438mod tests {
439 use std::path::Path;
440
441 use harn_lexer::Lexer;
442 use harn_parser::Parser;
443
444 use super::{
445 compile_module_artifact, compile_module_artifact_from_source,
446 needs_imported_enum_candidates, parse_module_source,
447 };
448 use crate::chunk::Constant;
449
450 #[test]
451 fn module_init_schema_of_uses_full_program_aliases() {
452 let source = r"
453pub type Item = {id: string}
454const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
455";
456 let mut lexer = Lexer::new(source);
457 let tokens = lexer.tokenize().unwrap();
458 let mut parser = Parser::new(tokens);
459 let program = parser.parse().unwrap();
460 let artifact = compile_module_artifact(&program, None).unwrap();
461 let constants = &artifact.init_chunk.expect("init chunk").constants;
462 let strings = constants
463 .iter()
464 .filter_map(|constant| match constant {
465 Constant::String(value) => Some(value.as_str()),
466 _ => None,
467 })
468 .collect::<Vec<_>>();
469 assert!(strings.contains(&"id"), "{strings:?}");
470 assert!(!strings.contains(&"Item"), "{strings:?}");
471 }
472
473 #[test]
474 fn type_only_modules_use_a_separate_schema_initializer() {
475 let source = r"
476pub type UserShape = {name: string, active?: bool}
477pub type UserList = list<UserShape>
478";
479
480 let artifact =
481 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
482 .expect("module compiles");
483
484 assert!(
485 artifact.init_chunk.is_none(),
486 "erased type aliases must not inflate module init bytecode"
487 );
488 assert!(artifact.public_type_names.contains("UserShape"));
489 assert!(artifact.public_type_names.contains("UserList"));
490 assert!(artifact.type_schema_init_chunk.is_some());
491 }
492
493 #[test]
494 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
495 let source = r#"
496import { External } from "./external"
497pub type Wrapped = {value: External}
498"#;
499 let source_path = Path::new("<test>/wrapped.harn");
500 let artifact =
501 compile_module_artifact_from_source(source_path, source).expect("module compiles");
502 let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
503 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
504 assert!(chunk
505 .constants
506 .iter()
507 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
508 }
509
510 #[test]
511 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
512 let plain = parse_module_source(
513 Path::new("<test>/plain.harn"),
514 r#"
515import { helper } from "./support"
516pub fn run() -> int { return helper(1) }
517"#,
518 )
519 .expect("plain module parses");
520 assert!(!needs_imported_enum_candidates(&plain));
521
522 let qualified = parse_module_source(
523 Path::new("<test>/qualified.harn"),
524 r#"
525import { Status } from "./status"
526pub fn run(value: Status) {
527 match value {
528 Status.Ready -> { return 1 }
529 _ -> { return 0 }
530 }
531}
532"#,
533 )
534 .expect("qualified module parses");
535 assert!(needs_imported_enum_candidates(&qualified));
536 }
537
538 #[test]
539 fn private_declarations_do_not_expand_module_init() {
540 let artifact = compile_module_artifact_from_source(
541 Path::new("<test>/private-declarations.harn"),
542 r"
543enum PrivateStatus { Ready }
544struct PrivateConfig { value: int }
545pub fn run() { return PrivateStatus.Ready }
546",
547 )
548 .expect("private declarations compile");
549
550 assert!(artifact.init_chunk.is_none());
551 assert!(artifact.functions.contains_key("PrivateConfig"));
552 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
553 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
554 }
555}