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