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(Clone, Debug, Serialize, Deserialize)]
51pub struct ModuleImportSpec {
52 pub path: String,
53 pub binding: ModuleImportBinding,
54 pub is_pub: bool,
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
59pub enum ModuleImportBinding {
60 Wildcard,
61 Selected(Vec<String>),
62 Namespace {
63 alias: String,
64 demand: harn_parser::NamespaceDemand,
65 },
66}
67
68#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct ModuleArtifact {
75 #[serde(default)]
76 pub provenance: ModuleProvenance,
77 pub imports: Vec<ModuleImportSpec>,
78 pub type_schema_init_chunks: Vec<CachedChunk>,
81 pub init_chunk: Option<CachedChunk>,
82 pub functions: BTreeMap<String, CachedCompiledFunction>,
83 pub public_exports: BTreeMap<String, DefKind>,
88 pub public_value_names: HashSet<String>,
92 pub public_type_names: HashSet<String>,
97}
98
99pub fn specialize_module_artifact(
106 program: &[harn_parser::SNode],
107 source_file: Option<String>,
108 mut artifact: ModuleArtifact,
109 demand: &harn_modules::ExportDemand,
110) -> Result<ModuleArtifact, VmError> {
111 use harn_parser::Node;
112 use std::collections::{BTreeSet, HashMap};
113
114 if matches!(demand, harn_modules::ExportDemand::WholeNamespace) {
115 return Ok(artifact);
116 }
117
118 if artifact.imports.iter().any(|import| import.is_pub) {
123 return Ok(artifact);
124 }
125
126 let callable_names = artifact.functions.keys().cloned().collect::<HashSet<_>>();
127 let mut declarations = HashMap::<String, &harn_parser::SNode>::new();
128 for node in program {
129 let inner = match &node.node {
130 Node::AttributedDecl { inner, .. } => inner.as_ref(),
131 _ => node,
132 };
133 let name = match &inner.node {
134 Node::FnDecl { name, .. }
135 | Node::Pipeline { name, .. }
136 | Node::StructDecl { name, .. } => Some(name),
137 _ => None,
138 };
139 if let Some(name) = name {
140 declarations.insert(name.clone(), inner);
141 }
142 }
143
144 let mut pending = Vec::new();
145 if let harn_modules::ExportDemand::Members(members) = demand {
146 pending.extend(
147 members
148 .iter()
149 .filter(|name| callable_names.contains(*name))
150 .cloned(),
151 );
152 }
153 for node in program {
155 let inner = match &node.node {
156 Node::AttributedDecl { inner, .. } => inner.as_ref(),
157 _ => node,
158 };
159 if matches!(
160 &inner.node,
161 Node::LetBinding { .. }
162 | Node::ConstBinding { .. }
163 | Node::EnumDecl { is_pub: true, .. }
164 | Node::ToolDecl { .. }
165 | Node::SkillDecl { .. }
166 | Node::EvalPackDecl { .. }
167 ) {
168 collect_callable_references(inner, &callable_names, &mut pending);
169 }
170 }
171
172 let mut retained = HashSet::new();
173 while let Some(name) = pending.pop() {
174 if !retained.insert(name.clone()) {
175 continue;
176 }
177 if let Some(declaration) = declarations.get(&name) {
178 collect_callable_references(declaration, &callable_names, &mut pending);
179 }
180 }
181 artifact.functions.retain(|name, _| retained.contains(name));
182 artifact
183 .public_exports
184 .retain(|name, _| demand.contains(name));
185 artifact
186 .public_value_names
187 .retain(|name| demand.contains(name));
188 artifact
189 .public_type_names
190 .retain(|name| demand.contains(name));
191
192 let selected_type_names = artifact
193 .public_type_names
194 .iter()
195 .cloned()
196 .collect::<BTreeSet<_>>();
197 artifact.type_schema_init_chunks =
198 crate::Compiler::compile_selected_public_type_schema_initializers(
199 program,
200 source_file,
201 Some(&selected_type_names),
202 )
203 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
204 .into_iter()
205 .map(|chunk| chunk.freeze_for_cache())
206 .collect();
207 Ok(artifact)
208}
209
210fn collect_callable_references(
211 node: &harn_parser::SNode,
212 callable_names: &HashSet<String>,
213 out: &mut Vec<String>,
214) {
215 use harn_parser::Node;
216 let referenced = match &node.node {
217 Node::Identifier(name)
218 | Node::FunctionCall { name, .. }
219 | Node::StructConstruct {
220 struct_name: name, ..
221 }
222 | Node::EnumConstruct {
223 enum_name: name, ..
224 } => Some(name),
225 _ => None,
226 };
227 if let Some(name) = referenced.filter(|name| callable_names.contains(*name)) {
228 out.push(name.clone());
229 }
230 for child in harn_parser::visit::immediate_children(node) {
231 collect_callable_references(child, callable_names, out);
232 }
233}
234
235impl ModuleArtifact {
236 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
243 let source_file = source_path.display().to_string();
244 for chunk in &mut self.type_schema_init_chunks {
245 bind_chunk_source_file(chunk, &source_file);
246 }
247 if let Some(chunk) = &mut self.init_chunk {
248 bind_chunk_source_file(chunk, &source_file);
249 }
250 for function in self.functions.values_mut() {
251 bind_chunk_source_file(&mut function.chunk, &source_file);
252 }
253 }
254}
255
256fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
257 chunk.source_file = Some(source_file.to_string());
258 for function in &mut chunk.functions {
259 bind_chunk_source_file(&mut function.chunk, source_file);
260 }
261}
262
263pub fn compile_module_artifact(
268 program: &[harn_parser::SNode],
269 module_source_file: Option<String>,
270) -> Result<ModuleArtifact, VmError> {
271 let imported_enum_candidates = module_source_file
272 .as_deref()
273 .filter(|_| needs_imported_enum_candidates(program))
274 .and_then(|path| {
275 harn_modules::build(&[Path::new(path).to_path_buf()])
276 .imported_names_by_kind_for_file(Path::new(path), DefKind::Enum)
277 })
278 .unwrap_or_default();
279 compile_module_artifact_with_imported_enums(
280 program,
281 module_source_file,
282 &imported_enum_candidates.into_iter().collect::<Vec<_>>(),
283 )
284}
285
286fn compile_module_artifact_with_imported_enums(
287 program: &[harn_parser::SNode],
288 module_source_file: Option<String>,
289 imported_enum_candidates: &[String],
290) -> Result<ModuleArtifact, VmError> {
291 compile_module_artifact_with_provenance(
292 program,
293 module_source_file,
294 imported_enum_candidates,
295 ModuleProvenance::User,
296 )
297}
298
299fn compile_module_artifact_with_provenance(
300 program: &[harn_parser::SNode],
301 module_source_file: Option<String>,
302 imported_enum_candidates: &[String],
303 provenance: ModuleProvenance,
304) -> Result<ModuleArtifact, VmError> {
305 let namespace_demands = harn_parser::namespace_import_demands(program);
306 let imports: Vec<ModuleImportSpec> = program
307 .iter()
308 .filter_map(|node| match &node.node {
309 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
310 path: path.clone(),
311 binding: ModuleImportBinding::Wildcard,
312 is_pub: *is_pub,
313 }),
314 harn_parser::Node::SelectiveImport {
315 names,
316 path,
317 is_pub,
318 } => Some(ModuleImportSpec {
319 path: path.clone(),
320 binding: ModuleImportBinding::Selected(names.clone()),
321 is_pub: *is_pub,
322 }),
323 harn_parser::Node::NamespaceImport {
324 alias,
325 path,
326 is_pub,
327 } => Some(ModuleImportSpec {
328 path: path.clone(),
329 binding: ModuleImportBinding::Namespace {
330 alias: alias.clone(),
331 demand: namespace_demands
332 .get(alias)
333 .cloned()
334 .unwrap_or(harn_parser::NamespaceDemand::Whole),
335 },
336 is_pub: *is_pub,
337 }),
338 _ => None,
339 })
340 .collect();
341
342 if provenance == ModuleProvenance::PrivilegedWire {
343 validate_privileged_wire_surface(program, &imports)?;
344 }
345
346 let compiler = || match provenance {
347 ModuleProvenance::User => crate::Compiler::new(),
348 ModuleProvenance::PrivilegedWire => {
349 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
350 }
351 ModuleProvenance::TrustedHostDispatch => {
352 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
353 }
354 };
355
356 let init_nodes: Vec<harn_parser::SNode> = program
357 .iter()
358 .filter(|sn| {
359 let inner = match &sn.node {
360 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
361 _ => sn,
362 };
363 matches!(
364 &inner.node,
365 harn_parser::Node::LetBinding { .. }
366 | harn_parser::Node::ConstBinding { .. }
367 | harn_parser::Node::EnumDecl { is_pub: true, .. }
374 | harn_parser::Node::ToolDecl { .. }
375 | harn_parser::Node::SkillDecl { .. }
376 | harn_parser::Node::EvalPackDecl { .. }
377 )
378 })
379 .cloned()
380 .collect();
381 let init_chunk = if init_nodes.is_empty() {
382 None
383 } else {
384 let compiler = compiler();
385 Some(
386 compiler
387 .compile_module_init(program, &init_nodes, imported_enum_candidates)
388 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
389 .freeze_for_cache(),
390 )
391 };
392
393 let public_exports: BTreeMap<String, DefKind> = program
394 .iter()
395 .flat_map(public_declarations)
396 .map(|export| (export.name, export.kind))
397 .collect();
398 let public_value_names = public_exports
399 .iter()
400 .filter(|(_, kind)| {
401 matches!(
402 kind,
403 DefKind::Variable
404 | DefKind::Enum
405 | DefKind::Tool
406 | DefKind::Skill
407 | DefKind::EvalPack
408 )
409 })
410 .map(|(name, _)| name.clone())
411 .collect();
412 let public_type_names = public_exports
413 .iter()
414 .filter(|(_, kind)| !kind.has_runtime_value())
415 .map(|(name, _)| name.clone())
416 .collect();
417
418 let mut functions = BTreeMap::new();
419 for node in program {
420 let inner = match &node.node {
421 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
422 _ => node,
423 };
424 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
425 let constructor = compiler()
430 .compile_struct_constructor(name, fields)
431 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
432 functions.insert(name.clone(), constructor.freeze_for_cache());
433 continue;
434 }
435 if let harn_parser::Node::Pipeline {
436 name,
437 params,
438 body,
439 extends,
440 ..
441 } = &inner.node
442 {
443 let mut compiler = compiler();
444 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
445 let pipeline = compiler
446 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
447 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
448 functions.insert(name.clone(), pipeline.freeze_for_cache());
449 continue;
450 }
451 let harn_parser::Node::FnDecl {
452 name,
453 type_params,
454 params,
455 body,
456 ..
457 } = &inner.node
458 else {
459 continue;
460 };
461
462 let mut compiler = compiler();
463 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
464 compiler.prepare_module_context(program);
465 let func_chunk = compiler
466 .compile_fn_body(type_params, params, body, module_source_file.clone())
467 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
468 functions.insert(name.clone(), func_chunk.freeze_for_cache());
469 }
470
471 let type_schema_init_chunks =
472 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
473 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
474 .into_iter()
475 .map(|chunk| chunk.freeze_for_cache())
476 .collect();
477
478 Ok(ModuleArtifact {
479 provenance,
480 imports,
481 type_schema_init_chunks,
482 init_chunk,
483 functions,
484 public_exports,
485 public_value_names,
486 public_type_names,
487 })
488}
489
490fn validate_privileged_wire_surface(
491 program: &[harn_parser::SNode],
492 imports: &[ModuleImportSpec],
493) -> Result<(), VmError> {
494 if imports.iter().any(|import| import.is_pub) {
495 return Err(VmError::Runtime(
496 "Privileged wire modules cannot re-export imports".to_string(),
497 ));
498 }
499 for export in program.iter().flat_map(public_declarations) {
500 if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
501 return Err(VmError::Runtime(format!(
502 "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
503 export.name, export.kind
504 )));
505 }
506 }
507 Ok(())
508}
509
510pub fn compile_module_artifact_from_source(
514 source_path: &Path,
515 source: &str,
516) -> Result<ModuleArtifact, VmError> {
517 let program = parse_module_source(source_path, source)?;
518 let imported_enum_candidates =
519 imported_enum_candidates_for_program(source_path, source, &program);
520 compile_module_artifact_with_imported_enums(
521 &program,
522 Some(source_path.display().to_string()),
523 &imported_enum_candidates,
524 )
525}
526
527pub fn compile_privileged_wire_module_artifact_from_source(
536 source_path: &Path,
537 source: &str,
538) -> Result<ModuleArtifact, VmError> {
539 let program = parse_module_source(source_path, source)?;
540 let imported_enum_candidates =
541 imported_enum_candidates_for_program(source_path, source, &program);
542 compile_module_artifact_with_provenance(
543 &program,
544 Some(source_path.display().to_string()),
545 &imported_enum_candidates,
546 ModuleProvenance::PrivilegedWire,
547 )
548}
549
550pub fn compile_trusted_host_dispatch_module_artifact_from_source(
555 source_path: &Path,
556 source: &str,
557) -> Result<ModuleArtifact, VmError> {
558 let program = parse_module_source(source_path, source)?;
559 let imported_enum_candidates =
560 imported_enum_candidates_for_program(source_path, source, &program);
561 compile_module_artifact_with_provenance(
562 &program,
563 Some(source_path.display().to_string()),
564 &imported_enum_candidates,
565 ModuleProvenance::TrustedHostDispatch,
566 )
567}
568
569fn imported_enum_candidates_for_program(
574 source_path: &Path,
575 source: &str,
576 program: &[harn_parser::SNode],
577) -> Vec<String> {
578 if !needs_imported_enum_candidates(program) {
579 return Vec::new();
580 }
581 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
582 let cache_key = harn_modules::canonical_path(source_path);
583 let cacheable = is_immutable_stdlib_path(source_path);
584 if cacheable {
585 if let Some((_cached_hash, candidates)) = imported_enum_cache()
586 .lock()
587 .expect("imported enum cache lock poisoned")
588 .get(&cache_key)
589 .filter(|(cached_hash, _)| *cached_hash == source_hash)
590 {
591 return candidates.clone();
592 }
593 }
594
595 let graph = harn_modules::build_with_source(source_path, source);
600 if !cacheable {
601 return sorted_imported_enum_candidates(&graph, source_path);
602 }
603 let mut projections = Vec::new();
604 for path in graph.module_paths() {
605 let module_source = if path == cache_key {
606 Some(source.to_string())
607 } else {
608 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
609 };
610 let Some(module_source) = module_source else {
611 continue;
612 };
613 let candidates = sorted_imported_enum_candidates(&graph, &path);
614 projections.push((
615 path,
616 (
617 *blake3::hash(module_source.as_bytes()).as_bytes(),
618 candidates,
619 ),
620 ));
621 }
622 let mut cache = imported_enum_cache()
623 .lock()
624 .expect("imported enum cache lock poisoned");
625 for (path, projection) in projections {
626 if is_immutable_stdlib_path(&path) {
627 cache.insert(path, projection);
628 }
629 }
630 cache
631 .get(&cache_key)
632 .filter(|(cached_hash, _)| *cached_hash == source_hash)
633 .map(|(_, candidates)| candidates.clone())
634 .unwrap_or_default()
635}
636
637fn sorted_imported_enum_candidates(
638 graph: &harn_modules::ModuleGraph,
639 source_path: &Path,
640) -> Vec<String> {
641 let mut candidates = graph
642 .imported_names_by_kind_for_file(source_path, DefKind::Enum)
643 .unwrap_or_default()
644 .into_iter()
645 .collect::<Vec<_>>();
646 candidates.sort_unstable();
647 candidates
648}
649
650fn is_immutable_stdlib_path(path: &Path) -> bool {
651 path.to_str()
652 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
653}
654
655fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
656 harn_parser::visit::contains_identifier_enum_pattern(program)
657}
658
659fn parse_module_source(
660 source_path: &Path,
661 source: &str,
662) -> Result<Vec<harn_parser::SNode>, VmError> {
663 let mut lexer = harn_lexer::Lexer::new(source);
664 let tokens = lexer.tokenize().map_err(|e| {
665 VmError::Runtime(format!(
666 "Import lex error in {}: {e}",
667 source_path.display()
668 ))
669 })?;
670 let mut parser = harn_parser::Parser::new(tokens);
671 parser.parse().map_err(|e| {
672 VmError::Runtime(format!(
673 "Import parse error in {}: {e}",
674 source_path.display()
675 ))
676 })
677}
678
679pub fn compile_module_artifact_from_source_with_imported_enums(
684 source_path: &Path,
685 source: &str,
686 imported_enum_candidates: impl IntoIterator<Item = String>,
687) -> Result<ModuleArtifact, VmError> {
688 let program = parse_module_source(source_path, source)?;
689 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
690 compile_module_artifact_with_imported_enums(
691 &program,
692 Some(source_path.display().to_string()),
693 &imported_enum_candidates,
694 )
695}
696
697#[cfg(test)]
698mod tests {
699 use std::path::Path;
700
701 use harn_lexer::Lexer;
702 use harn_parser::Parser;
703
704 use super::{
705 compile_module_artifact, compile_module_artifact_from_source,
706 compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
707 parse_module_source, ModuleImportBinding, ModuleProvenance,
708 };
709 use crate::chunk::Constant;
710
711 #[test]
712 fn module_init_schema_of_uses_full_program_aliases() {
713 let source = r"
714pub type Item = {id: string}
715const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
716";
717 let mut lexer = Lexer::new(source);
718 let tokens = lexer.tokenize().unwrap();
719 let mut parser = Parser::new(tokens);
720 let program = parser.parse().unwrap();
721 let artifact = compile_module_artifact(&program, None).unwrap();
722 let constants = &artifact.init_chunk.expect("init chunk").constants;
723 let strings = constants
724 .iter()
725 .filter_map(|constant| match constant {
726 Constant::String(value) => Some(value.as_str()),
727 _ => None,
728 })
729 .collect::<Vec<_>>();
730 assert!(strings.contains(&"id"), "{strings:?}");
731 assert!(!strings.contains(&"Item"), "{strings:?}");
732 }
733
734 #[test]
735 fn type_only_modules_use_a_separate_schema_initializer() {
736 let source = r"
737pub type UserShape = {name: string, active?: bool}
738pub type UserList = list<UserShape>
739";
740
741 let artifact =
742 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
743 .expect("module compiles");
744
745 assert!(
746 artifact.init_chunk.is_none(),
747 "erased type aliases must not inflate module init bytecode"
748 );
749 assert!(artifact.public_type_names.contains("UserShape"));
750 assert!(artifact.public_type_names.contains("UserList"));
751 assert_eq!(artifact.type_schema_init_chunks.len(), 2);
752 }
753
754 #[test]
755 fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
756 let source = r#"
757pub enum KeptStatus { Ready }
758pub enum DeadStatus { Gone }
759pub struct KeptConfig { value: int }
760pub struct DeadConfig { value: string }
761pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
762pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
763"#;
764 let source_path = Path::new("<test>/declarations.harn");
765 let parsed = parse_module_source(source_path, source).expect("module parses");
766 let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
767 .expect("module compiles");
768 let selected = super::specialize_module_artifact(
769 &parsed,
770 Some(source_path.display().to_string()),
771 full,
772 &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
773 "KeptStatus".to_string(),
774 "KeptConfig".to_string(),
775 "kept_pipeline".to_string(),
776 ])),
777 )
778 .expect("specialization succeeds");
779
780 assert!(selected.public_exports.contains_key("KeptStatus"));
781 assert!(selected.public_exports.contains_key("KeptConfig"));
782 assert!(selected.public_exports.contains_key("kept_pipeline"));
783 assert!(!selected.public_exports.contains_key("DeadStatus"));
784 assert!(!selected.public_exports.contains_key("DeadConfig"));
785 assert!(!selected.public_exports.contains_key("dead_pipeline"));
786 assert!(selected.functions.contains_key("KeptConfig"));
787 assert!(selected.functions.contains_key("kept_pipeline"));
788 assert!(!selected.functions.contains_key("DeadConfig"));
789 assert!(!selected.functions.contains_key("dead_pipeline"));
790 }
791
792 #[test]
793 fn nested_namespace_import_retains_static_member_demand() {
794 let artifact = compile_module_artifact_from_source(
795 Path::new("<test>/wrapper.harn"),
796 r#"
797import * as lib from "./lib"
798pub fn call() { return lib.greet() }
799"#,
800 )
801 .expect("module compiles");
802
803 let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
804 panic!("expected namespace import metadata");
805 };
806 assert_eq!(alias, "lib");
807 assert_eq!(
808 demand,
809 &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
810 "greet".to_string(),
811 ]))
812 );
813 }
814
815 #[test]
816 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
817 let error = compile_module_artifact_from_source(
818 Path::new("<test>/user.harn"),
819 r#"fn probe() { host_call("project.scan", {}) }"#,
820 )
821 .expect_err("ordinary source must not acquire wire authority");
822 assert!(
823 error.to_string().contains("not callable source API"),
824 "{error}"
825 );
826 }
827
828 #[test]
829 fn explicit_privileged_compilation_stamps_private_wire_code() {
830 let artifact = compile_privileged_wire_module_artifact_from_source(
831 Path::new("<trusted>/wire.harn"),
832 r#"fn probe() { host_call("project.scan", {}) }"#,
833 )
834 .expect("trusted private wire function compiles");
835 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
836 assert!(artifact.functions.contains_key("probe"));
837 assert!(artifact.public_exports.is_empty());
838 }
839
840 #[test]
841 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
842 let error = compile_privileged_wire_module_artifact_from_source(
843 Path::new("<trusted>/wire.harn"),
844 r#"pub fn probe() { host_call("project.scan", {}) }"#,
845 )
846 .expect_err("wire closures must not be exportable");
847 assert!(
848 error
849 .to_string()
850 .contains("only explicit capability-value bindings"),
851 "{error}"
852 );
853 }
854
855 #[test]
856 fn privileged_wire_modules_cannot_reexport_imports() {
857 let error = compile_privileged_wire_module_artifact_from_source(
858 Path::new("<trusted>/wire.harn"),
859 r#"pub import { probe } from "./other""#,
860 )
861 .expect_err("wire authority must be non-reexportable");
862 assert!(
863 error.to_string().contains("cannot re-export imports"),
864 "{error}"
865 );
866 }
867
868 #[test]
869 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
870 let source = r#"
871import { External } from "./external"
872pub type Wrapped = {value: External}
873"#;
874 let source_path = Path::new("<test>/wrapped.harn");
875 let artifact =
876 compile_module_artifact_from_source(source_path, source).expect("module compiles");
877 let chunk = artifact
878 .type_schema_init_chunks
879 .into_iter()
880 .next()
881 .expect("schema initializer");
882 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
883 assert!(chunk
884 .constants
885 .iter()
886 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
887 }
888
889 #[test]
890 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
891 let plain = parse_module_source(
892 Path::new("<test>/plain.harn"),
893 r#"
894import { helper } from "./support"
895pub fn run() -> int { return helper(1) }
896"#,
897 )
898 .expect("plain module parses");
899 assert!(!needs_imported_enum_candidates(&plain));
900
901 let qualified = parse_module_source(
902 Path::new("<test>/qualified.harn"),
903 r#"
904import { Status } from "./status"
905pub fn run(value: Status) {
906 match value {
907 Status.Ready -> { return 1 }
908 _ -> { return 0 }
909 }
910}
911"#,
912 )
913 .expect("qualified module parses");
914 assert!(needs_imported_enum_candidates(&qualified));
915 }
916
917 #[test]
918 fn private_declarations_do_not_expand_module_init() {
919 let artifact = compile_module_artifact_from_source(
920 Path::new("<test>/private-declarations.harn"),
921 r"
922enum PrivateStatus { Ready }
923struct PrivateConfig { value: int }
924pub fn run() { return PrivateStatus.Ready }
925",
926 )
927 .expect("private declarations compile");
928
929 assert!(artifact.init_chunk.is_none());
930 assert!(artifact.functions.contains_key("PrivateConfig"));
931 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
932 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
933 }
934}