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
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31pub enum ModuleProvenance {
32 #[default]
33 User,
34 PrivilegedWire,
35}
36
37fn imported_enum_cache() -> &'static Mutex<ImportedEnumCache> {
38 static CACHE: OnceLock<Mutex<ImportedEnumCache>> = OnceLock::new();
39 CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
40}
41
42#[derive(Debug, Serialize, Deserialize)]
46pub struct ModuleImportSpec {
47 pub path: String,
48 pub selected_names: Option<Vec<String>>,
49 #[serde(default)]
52 pub namespace_alias: Option<String>,
53 pub is_pub: bool,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
62pub struct ModuleArtifact {
63 #[serde(default)]
64 pub provenance: ModuleProvenance,
65 pub imports: Vec<ModuleImportSpec>,
66 pub type_schema_init_chunk: Option<CachedChunk>,
69 pub init_chunk: Option<CachedChunk>,
70 pub functions: BTreeMap<String, CachedCompiledFunction>,
71 pub public_exports: BTreeMap<String, DefKind>,
76 pub public_value_names: HashSet<String>,
80 pub public_type_names: HashSet<String>,
85}
86
87impl ModuleArtifact {
88 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
95 let source_file = source_path.display().to_string();
96 if let Some(chunk) = &mut self.type_schema_init_chunk {
97 bind_chunk_source_file(chunk, &source_file);
98 }
99 if let Some(chunk) = &mut self.init_chunk {
100 bind_chunk_source_file(chunk, &source_file);
101 }
102 for function in self.functions.values_mut() {
103 bind_chunk_source_file(&mut function.chunk, &source_file);
104 }
105 }
106}
107
108fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
109 chunk.source_file = Some(source_file.to_string());
110 for function in &mut chunk.functions {
111 bind_chunk_source_file(&mut function.chunk, source_file);
112 }
113}
114
115pub fn compile_module_artifact(
120 program: &[harn_parser::SNode],
121 module_source_file: Option<String>,
122) -> Result<ModuleArtifact, VmError> {
123 let imported_enum_candidates = module_source_file
124 .as_deref()
125 .filter(|_| needs_imported_enum_candidates(program))
126 .and_then(|path| {
127 harn_modules::build(&[Path::new(path).to_path_buf()])
128 .imported_names_by_kind_for_file(Path::new(path), DefKind::Enum)
129 })
130 .unwrap_or_default();
131 compile_module_artifact_with_imported_enums(
132 program,
133 module_source_file,
134 &imported_enum_candidates.into_iter().collect::<Vec<_>>(),
135 )
136}
137
138fn compile_module_artifact_with_imported_enums(
139 program: &[harn_parser::SNode],
140 module_source_file: Option<String>,
141 imported_enum_candidates: &[String],
142) -> Result<ModuleArtifact, VmError> {
143 compile_module_artifact_with_provenance(
144 program,
145 module_source_file,
146 imported_enum_candidates,
147 ModuleProvenance::User,
148 )
149}
150
151fn compile_module_artifact_with_provenance(
152 program: &[harn_parser::SNode],
153 module_source_file: Option<String>,
154 imported_enum_candidates: &[String],
155 provenance: ModuleProvenance,
156) -> Result<ModuleArtifact, VmError> {
157 let imports: Vec<ModuleImportSpec> = program
158 .iter()
159 .filter_map(|node| match &node.node {
160 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
161 path: path.clone(),
162 selected_names: None,
163 namespace_alias: None,
164 is_pub: *is_pub,
165 }),
166 harn_parser::Node::SelectiveImport {
167 names,
168 path,
169 is_pub,
170 } => Some(ModuleImportSpec {
171 path: path.clone(),
172 selected_names: Some(names.clone()),
173 namespace_alias: None,
174 is_pub: *is_pub,
175 }),
176 harn_parser::Node::NamespaceImport {
177 alias,
178 path,
179 is_pub,
180 } => Some(ModuleImportSpec {
181 path: path.clone(),
182 selected_names: None,
183 namespace_alias: Some(alias.clone()),
184 is_pub: *is_pub,
185 }),
186 _ => None,
187 })
188 .collect();
189
190 if provenance == ModuleProvenance::PrivilegedWire {
191 validate_privileged_wire_surface(program, &imports)?;
192 }
193
194 let compiler = || match provenance {
195 ModuleProvenance::User => crate::Compiler::new(),
196 ModuleProvenance::PrivilegedWire => {
197 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
198 }
199 };
200
201 let init_nodes: Vec<harn_parser::SNode> = program
202 .iter()
203 .filter(|sn| {
204 let inner = match &sn.node {
205 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
206 _ => sn,
207 };
208 matches!(
209 &inner.node,
210 harn_parser::Node::LetBinding { .. }
211 | harn_parser::Node::ConstBinding { .. }
212 | harn_parser::Node::EnumDecl { is_pub: true, .. }
219 | harn_parser::Node::ToolDecl { .. }
220 | harn_parser::Node::SkillDecl { .. }
221 | harn_parser::Node::EvalPackDecl { .. }
222 )
223 })
224 .cloned()
225 .collect();
226 let init_chunk = if init_nodes.is_empty() {
227 None
228 } else {
229 let compiler = compiler();
230 Some(
231 compiler
232 .compile_module_init(program, &init_nodes, imported_enum_candidates)
233 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
234 .freeze_for_cache(),
235 )
236 };
237
238 let public_exports: BTreeMap<String, DefKind> = program
239 .iter()
240 .flat_map(public_declarations)
241 .map(|export| (export.name, export.kind))
242 .collect();
243 let public_value_names = public_exports
244 .iter()
245 .filter(|(_, kind)| {
246 matches!(
247 kind,
248 DefKind::Variable
249 | DefKind::Enum
250 | DefKind::Tool
251 | DefKind::Skill
252 | DefKind::EvalPack
253 )
254 })
255 .map(|(name, _)| name.clone())
256 .collect();
257 let public_type_names = public_exports
258 .iter()
259 .filter(|(_, kind)| !kind.has_runtime_value())
260 .map(|(name, _)| name.clone())
261 .collect();
262
263 let mut functions = BTreeMap::new();
264 for node in program {
265 let inner = match &node.node {
266 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
267 _ => node,
268 };
269 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
270 let constructor = compiler()
275 .compile_struct_constructor(name, fields)
276 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
277 functions.insert(name.clone(), constructor.freeze_for_cache());
278 continue;
279 }
280 if let harn_parser::Node::Pipeline {
281 name,
282 params,
283 body,
284 extends,
285 ..
286 } = &inner.node
287 {
288 let mut compiler = compiler();
289 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
290 let pipeline = compiler
291 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
292 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
293 functions.insert(name.clone(), pipeline.freeze_for_cache());
294 continue;
295 }
296 let harn_parser::Node::FnDecl {
297 name,
298 type_params,
299 params,
300 body,
301 ..
302 } = &inner.node
303 else {
304 continue;
305 };
306
307 let mut compiler = compiler();
308 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
309 compiler.prepare_module_context(program);
310 let func_chunk = compiler
311 .compile_fn_body(type_params, params, body, module_source_file.clone())
312 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
313 functions.insert(name.clone(), func_chunk.freeze_for_cache());
314 }
315
316 let type_schema_init_chunk =
317 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
318 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
319 .map(|chunk| chunk.freeze_for_cache());
320
321 Ok(ModuleArtifact {
322 provenance,
323 imports,
324 type_schema_init_chunk,
325 init_chunk,
326 functions,
327 public_exports,
328 public_value_names,
329 public_type_names,
330 })
331}
332
333fn validate_privileged_wire_surface(
334 program: &[harn_parser::SNode],
335 imports: &[ModuleImportSpec],
336) -> Result<(), VmError> {
337 if imports.iter().any(|import| import.is_pub) {
338 return Err(VmError::Runtime(
339 "Privileged wire modules cannot re-export imports".to_string(),
340 ));
341 }
342 for export in program.iter().flat_map(public_declarations) {
343 if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
344 return Err(VmError::Runtime(format!(
345 "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
346 export.name, export.kind
347 )));
348 }
349 }
350 Ok(())
351}
352
353pub fn compile_module_artifact_from_source(
357 source_path: &Path,
358 source: &str,
359) -> Result<ModuleArtifact, VmError> {
360 let program = parse_module_source(source_path, source)?;
361 let imported_enum_candidates =
362 imported_enum_candidates_for_program(source_path, source, &program);
363 compile_module_artifact_with_imported_enums(
364 &program,
365 Some(source_path.display().to_string()),
366 &imported_enum_candidates,
367 )
368}
369
370pub fn compile_privileged_wire_module_artifact_from_source(
379 source_path: &Path,
380 source: &str,
381) -> Result<ModuleArtifact, VmError> {
382 let program = parse_module_source(source_path, source)?;
383 let imported_enum_candidates =
384 imported_enum_candidates_for_program(source_path, source, &program);
385 compile_module_artifact_with_provenance(
386 &program,
387 Some(source_path.display().to_string()),
388 &imported_enum_candidates,
389 ModuleProvenance::PrivilegedWire,
390 )
391}
392
393fn imported_enum_candidates_for_program(
398 source_path: &Path,
399 source: &str,
400 program: &[harn_parser::SNode],
401) -> Vec<String> {
402 if !needs_imported_enum_candidates(program) {
403 return Vec::new();
404 }
405 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
406 let cache_key = harn_modules::canonical_path(source_path);
407 let cacheable = is_immutable_stdlib_path(source_path);
408 if cacheable {
409 if let Some((_cached_hash, candidates)) = imported_enum_cache()
410 .lock()
411 .expect("imported enum cache lock poisoned")
412 .get(&cache_key)
413 .filter(|(cached_hash, _)| *cached_hash == source_hash)
414 {
415 return candidates.clone();
416 }
417 }
418
419 let graph = harn_modules::build_with_source(source_path, source);
424 if !cacheable {
425 return sorted_imported_enum_candidates(&graph, source_path);
426 }
427 let mut projections = Vec::new();
428 for path in graph.module_paths() {
429 let module_source = if path == cache_key {
430 Some(source.to_string())
431 } else {
432 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
433 };
434 let Some(module_source) = module_source else {
435 continue;
436 };
437 let candidates = sorted_imported_enum_candidates(&graph, &path);
438 projections.push((
439 path,
440 (
441 *blake3::hash(module_source.as_bytes()).as_bytes(),
442 candidates,
443 ),
444 ));
445 }
446 let mut cache = imported_enum_cache()
447 .lock()
448 .expect("imported enum cache lock poisoned");
449 for (path, projection) in projections {
450 if is_immutable_stdlib_path(&path) {
451 cache.insert(path, projection);
452 }
453 }
454 cache
455 .get(&cache_key)
456 .filter(|(cached_hash, _)| *cached_hash == source_hash)
457 .map(|(_, candidates)| candidates.clone())
458 .unwrap_or_default()
459}
460
461fn sorted_imported_enum_candidates(
462 graph: &harn_modules::ModuleGraph,
463 source_path: &Path,
464) -> Vec<String> {
465 let mut candidates = graph
466 .imported_names_by_kind_for_file(source_path, DefKind::Enum)
467 .unwrap_or_default()
468 .into_iter()
469 .collect::<Vec<_>>();
470 candidates.sort_unstable();
471 candidates
472}
473
474fn is_immutable_stdlib_path(path: &Path) -> bool {
475 path.to_str()
476 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
477}
478
479fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
480 harn_parser::visit::contains_identifier_enum_pattern(program)
481}
482
483fn parse_module_source(
484 source_path: &Path,
485 source: &str,
486) -> Result<Vec<harn_parser::SNode>, VmError> {
487 let mut lexer = harn_lexer::Lexer::new(source);
488 let tokens = lexer.tokenize().map_err(|e| {
489 VmError::Runtime(format!(
490 "Import lex error in {}: {e}",
491 source_path.display()
492 ))
493 })?;
494 let mut parser = harn_parser::Parser::new(tokens);
495 parser.parse().map_err(|e| {
496 VmError::Runtime(format!(
497 "Import parse error in {}: {e}",
498 source_path.display()
499 ))
500 })
501}
502
503pub fn compile_module_artifact_from_source_with_imported_enums(
508 source_path: &Path,
509 source: &str,
510 imported_enum_candidates: impl IntoIterator<Item = String>,
511) -> Result<ModuleArtifact, VmError> {
512 let program = parse_module_source(source_path, source)?;
513 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
514 compile_module_artifact_with_imported_enums(
515 &program,
516 Some(source_path.display().to_string()),
517 &imported_enum_candidates,
518 )
519}
520
521#[cfg(test)]
522mod tests {
523 use std::path::Path;
524
525 use harn_lexer::Lexer;
526 use harn_parser::Parser;
527
528 use super::{
529 compile_module_artifact, compile_module_artifact_from_source,
530 compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
531 parse_module_source, ModuleProvenance,
532 };
533 use crate::chunk::Constant;
534
535 #[test]
536 fn module_init_schema_of_uses_full_program_aliases() {
537 let source = r"
538pub type Item = {id: string}
539const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
540";
541 let mut lexer = Lexer::new(source);
542 let tokens = lexer.tokenize().unwrap();
543 let mut parser = Parser::new(tokens);
544 let program = parser.parse().unwrap();
545 let artifact = compile_module_artifact(&program, None).unwrap();
546 let constants = &artifact.init_chunk.expect("init chunk").constants;
547 let strings = constants
548 .iter()
549 .filter_map(|constant| match constant {
550 Constant::String(value) => Some(value.as_str()),
551 _ => None,
552 })
553 .collect::<Vec<_>>();
554 assert!(strings.contains(&"id"), "{strings:?}");
555 assert!(!strings.contains(&"Item"), "{strings:?}");
556 }
557
558 #[test]
559 fn type_only_modules_use_a_separate_schema_initializer() {
560 let source = r"
561pub type UserShape = {name: string, active?: bool}
562pub type UserList = list<UserShape>
563";
564
565 let artifact =
566 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
567 .expect("module compiles");
568
569 assert!(
570 artifact.init_chunk.is_none(),
571 "erased type aliases must not inflate module init bytecode"
572 );
573 assert!(artifact.public_type_names.contains("UserShape"));
574 assert!(artifact.public_type_names.contains("UserList"));
575 assert!(artifact.type_schema_init_chunk.is_some());
576 }
577
578 #[test]
579 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
580 let error = compile_module_artifact_from_source(
581 Path::new("<test>/user.harn"),
582 r#"fn probe() { host_call("project.scan", {}) }"#,
583 )
584 .expect_err("ordinary source must not acquire wire authority");
585 assert!(
586 error.to_string().contains("not callable source API"),
587 "{error}"
588 );
589 }
590
591 #[test]
592 fn explicit_privileged_compilation_stamps_private_wire_code() {
593 let artifact = compile_privileged_wire_module_artifact_from_source(
594 Path::new("<trusted>/wire.harn"),
595 r#"fn probe() { host_call("project.scan", {}) }"#,
596 )
597 .expect("trusted private wire function compiles");
598 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
599 assert!(artifact.functions.contains_key("probe"));
600 assert!(artifact.public_exports.is_empty());
601 }
602
603 #[test]
604 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
605 let error = compile_privileged_wire_module_artifact_from_source(
606 Path::new("<trusted>/wire.harn"),
607 r#"pub fn probe() { host_call("project.scan", {}) }"#,
608 )
609 .expect_err("wire closures must not be exportable");
610 assert!(
611 error
612 .to_string()
613 .contains("only explicit capability-value bindings"),
614 "{error}"
615 );
616 }
617
618 #[test]
619 fn privileged_wire_modules_cannot_reexport_imports() {
620 let error = compile_privileged_wire_module_artifact_from_source(
621 Path::new("<trusted>/wire.harn"),
622 r#"pub import { probe } from "./other""#,
623 )
624 .expect_err("wire authority must be non-reexportable");
625 assert!(
626 error.to_string().contains("cannot re-export imports"),
627 "{error}"
628 );
629 }
630
631 #[test]
632 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
633 let source = r#"
634import { External } from "./external"
635pub type Wrapped = {value: External}
636"#;
637 let source_path = Path::new("<test>/wrapped.harn");
638 let artifact =
639 compile_module_artifact_from_source(source_path, source).expect("module compiles");
640 let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
641 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
642 assert!(chunk
643 .constants
644 .iter()
645 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
646 }
647
648 #[test]
649 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
650 let plain = parse_module_source(
651 Path::new("<test>/plain.harn"),
652 r#"
653import { helper } from "./support"
654pub fn run() -> int { return helper(1) }
655"#,
656 )
657 .expect("plain module parses");
658 assert!(!needs_imported_enum_candidates(&plain));
659
660 let qualified = parse_module_source(
661 Path::new("<test>/qualified.harn"),
662 r#"
663import { Status } from "./status"
664pub fn run(value: Status) {
665 match value {
666 Status.Ready -> { return 1 }
667 _ -> { return 0 }
668 }
669}
670"#,
671 )
672 .expect("qualified module parses");
673 assert!(needs_imported_enum_candidates(&qualified));
674 }
675
676 #[test]
677 fn private_declarations_do_not_expand_module_init() {
678 let artifact = compile_module_artifact_from_source(
679 Path::new("<test>/private-declarations.harn"),
680 r"
681enum PrivateStatus { Ready }
682struct PrivateConfig { value: int }
683pub fn run() { return PrivateStatus.Ready }
684",
685 )
686 .expect("private declarations compile");
687
688 assert!(artifact.init_chunk.is_none());
689 assert!(artifact.functions.contains_key("PrivateConfig"));
690 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
691 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
692 }
693}