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};
18use sha2::{Digest, Sha256};
19
20use crate::chunk::{CachedChunk, CachedCompiledFunction};
21use crate::value::VmError;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct ModuleCompilationContext {
30 enum_candidates: Vec<String>,
31 source_callable_names: Vec<String>,
32}
33
34impl ModuleCompilationContext {
35 pub fn new(
36 enum_candidates: impl IntoIterator<Item = String>,
37 source_callable_names: impl IntoIterator<Item = String>,
38 ) -> Self {
39 let mut enum_candidates = enum_candidates.into_iter().collect::<Vec<_>>();
40 enum_candidates.sort_unstable();
41 enum_candidates.dedup();
42 let mut source_callable_names = source_callable_names.into_iter().collect::<Vec<_>>();
43 source_callable_names.sort_unstable();
44 source_callable_names.dedup();
45 Self {
46 enum_candidates,
47 source_callable_names,
48 }
49 }
50
51 pub fn from_graph(graph: &harn_modules::ModuleGraph, source_path: &Path) -> Self {
52 Self::new(
53 graph
54 .imported_names_by_kind_for_file(source_path, DefKind::Enum)
55 .unwrap_or_default(),
56 graph
57 .imported_callable_names_for_file(source_path)
58 .unwrap_or_default(),
59 )
60 }
61
62 pub fn for_source_in_graph(
67 graph: &harn_modules::ModuleGraph,
68 source_path: &Path,
69 source: &str,
70 ) -> Result<Self, VmError> {
71 let program = parse_module_source(source_path, source)?;
72 Ok(if needs_imported_symbol_projection(&program) {
73 Self::from_graph(graph, source_path)
74 } else {
75 Self::default()
76 })
77 }
78
79 pub fn enum_candidates(&self) -> &[String] {
80 &self.enum_candidates
81 }
82
83 pub fn source_callable_names(&self) -> &[String] {
84 &self.source_callable_names
85 }
86
87 pub fn digest(&self) -> [u8; 32] {
90 let mut hasher = Sha256::new();
91 hasher.update(b"harn-module-compilation-context-v1\0");
92 hash_names(&mut hasher, b"enum\0", &self.enum_candidates);
93 hash_names(
94 &mut hasher,
95 b"source-callable\0",
96 &self.source_callable_names,
97 );
98 hasher.finalize().into()
99 }
100}
101
102fn hash_names(hasher: &mut Sha256, label: &[u8], names: &[String]) {
103 hasher.update(label);
104 hasher.update((names.len() as u64).to_le_bytes());
105 for name in names {
106 hasher.update((name.len() as u64).to_le_bytes());
107 hasher.update(name.as_bytes());
108 }
109}
110
111type ImportedSymbolCache = BTreeMap<PathBuf, ([u8; 32], ModuleCompilationContext)>;
112
113#[derive(
120 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
121)]
122pub enum ModuleProvenance {
123 #[default]
124 User,
125 PrivilegedWire,
126 TrustedHostDispatch,
131}
132
133fn imported_symbol_cache() -> &'static Mutex<ImportedSymbolCache> {
134 static CACHE: OnceLock<Mutex<ImportedSymbolCache>> = OnceLock::new();
135 CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
142pub struct ModuleImportSpec {
143 pub path: String,
144 pub binding: ModuleImportBinding,
145 pub is_pub: bool,
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize)]
150pub enum ModuleImportBinding {
151 Wildcard,
152 Selected(Vec<String>),
153 Namespace {
154 alias: String,
155 demand: harn_parser::NamespaceDemand,
156 },
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize)]
165pub struct ModuleArtifact {
166 #[serde(default)]
167 pub provenance: ModuleProvenance,
168 pub imports: Vec<ModuleImportSpec>,
169 pub type_schema_init_chunks: Vec<CachedChunk>,
172 pub init_chunk: Option<CachedChunk>,
173 pub functions: BTreeMap<String, CachedCompiledFunction>,
174 pub public_exports: BTreeMap<String, DefKind>,
179 pub public_value_names: HashSet<String>,
183 pub public_type_names: HashSet<String>,
188}
189
190pub fn specialize_module_artifact(
197 program: &[harn_parser::SNode],
198 source_file: Option<String>,
199 mut artifact: ModuleArtifact,
200 demand: &harn_modules::ExportDemand,
201) -> Result<ModuleArtifact, VmError> {
202 use harn_parser::Node;
203 use std::collections::{BTreeSet, HashMap};
204
205 if matches!(demand, harn_modules::ExportDemand::WholeNamespace) {
206 return Ok(artifact);
207 }
208
209 if artifact.imports.iter().any(|import| import.is_pub) {
214 return Ok(artifact);
215 }
216
217 let callable_names = artifact.functions.keys().cloned().collect::<HashSet<_>>();
218 let mut declarations = HashMap::<String, &harn_parser::SNode>::new();
219 for node in program {
220 let inner = match &node.node {
221 Node::AttributedDecl { inner, .. } => inner.as_ref(),
222 _ => node,
223 };
224 let name = match &inner.node {
225 Node::FnDecl { name, .. }
226 | Node::Pipeline { name, .. }
227 | Node::StructDecl { name, .. } => Some(name),
228 _ => None,
229 };
230 if let Some(name) = name {
231 declarations.insert(name.clone(), inner);
232 }
233 }
234
235 let mut pending = Vec::new();
236 if let harn_modules::ExportDemand::Members(members) = demand {
237 pending.extend(
238 members
239 .iter()
240 .filter(|name| callable_names.contains(*name))
241 .cloned(),
242 );
243 }
244 for node in program {
246 let inner = match &node.node {
247 Node::AttributedDecl { inner, .. } => inner.as_ref(),
248 _ => node,
249 };
250 if matches!(
251 &inner.node,
252 Node::LetBinding { .. }
253 | Node::ConstBinding { .. }
254 | Node::EnumDecl { is_pub: true, .. }
255 | Node::ToolDecl { .. }
256 | Node::SkillDecl { .. }
257 | Node::EvalPackDecl { .. }
258 ) {
259 collect_callable_references(inner, &callable_names, &mut pending);
260 }
261 }
262
263 let mut retained = HashSet::new();
264 while let Some(name) = pending.pop() {
265 if !retained.insert(name.clone()) {
266 continue;
267 }
268 if let Some(declaration) = declarations.get(&name) {
269 collect_callable_references(declaration, &callable_names, &mut pending);
270 }
271 }
272 artifact.functions.retain(|name, _| retained.contains(name));
273 artifact
274 .public_exports
275 .retain(|name, _| demand.contains(name));
276 artifact
277 .public_value_names
278 .retain(|name| demand.contains(name));
279 artifact
280 .public_type_names
281 .retain(|name| demand.contains(name));
282
283 let selected_type_names = artifact
284 .public_type_names
285 .iter()
286 .cloned()
287 .collect::<BTreeSet<_>>();
288 artifact.type_schema_init_chunks =
289 crate::Compiler::compile_selected_public_type_schema_initializers(
290 program,
291 source_file,
292 Some(&selected_type_names),
293 )
294 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
295 .into_iter()
296 .map(|chunk| chunk.freeze_for_cache())
297 .collect();
298 Ok(artifact)
299}
300
301fn collect_callable_references(
302 node: &harn_parser::SNode,
303 callable_names: &HashSet<String>,
304 out: &mut Vec<String>,
305) {
306 use harn_parser::Node;
307 let referenced = match &node.node {
308 Node::Identifier(name)
309 | Node::FunctionCall { name, .. }
310 | Node::StructConstruct {
311 struct_name: name, ..
312 }
313 | Node::EnumConstruct {
314 enum_name: name, ..
315 } => Some(name),
316 _ => None,
317 };
318 if let Some(name) = referenced.filter(|name| callable_names.contains(*name)) {
319 out.push(name.clone());
320 }
321 for child in harn_parser::visit::immediate_children(node) {
322 collect_callable_references(child, callable_names, out);
323 }
324}
325
326impl ModuleArtifact {
327 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
334 let source_file = source_path.display().to_string();
335 for chunk in &mut self.type_schema_init_chunks {
336 bind_chunk_source_file(chunk, &source_file);
337 }
338 if let Some(chunk) = &mut self.init_chunk {
339 bind_chunk_source_file(chunk, &source_file);
340 }
341 for function in self.functions.values_mut() {
342 bind_chunk_source_file(&mut function.chunk, &source_file);
343 }
344 }
345}
346
347fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
348 chunk.source_file = Some(source_file.to_string());
349 for function in &mut chunk.functions {
350 bind_chunk_source_file(&mut function.chunk, source_file);
351 }
352}
353
354pub fn compile_module_artifact(
359 program: &[harn_parser::SNode],
360 module_source_file: Option<String>,
361) -> Result<ModuleArtifact, VmError> {
362 let imported_symbols = module_source_file
363 .as_deref()
364 .filter(|_| needs_imported_symbol_projection(program))
365 .map(|path| {
366 let graph = harn_modules::build(&[Path::new(path).to_path_buf()]);
367 sorted_imported_symbol_projection(&graph, Path::new(path))
368 })
369 .unwrap_or_default();
370 compile_module_artifact_with_imported_symbols(
371 program,
372 module_source_file,
373 &imported_symbols.enum_candidates,
374 &imported_symbols.source_callable_names,
375 )
376}
377
378fn compile_module_artifact_with_imported_symbols(
379 program: &[harn_parser::SNode],
380 module_source_file: Option<String>,
381 imported_enum_candidates: &[String],
382 imported_source_callable_names: &[String],
383) -> Result<ModuleArtifact, VmError> {
384 compile_module_artifact_with_provenance(
385 program,
386 module_source_file,
387 imported_enum_candidates,
388 imported_source_callable_names,
389 ModuleProvenance::User,
390 )
391}
392
393fn compile_module_artifact_with_provenance(
394 program: &[harn_parser::SNode],
395 module_source_file: Option<String>,
396 imported_enum_candidates: &[String],
397 imported_source_callable_names: &[String],
398 provenance: ModuleProvenance,
399) -> Result<ModuleArtifact, VmError> {
400 let namespace_demands = harn_parser::namespace_import_demands(program);
401 let imports: Vec<ModuleImportSpec> = program
402 .iter()
403 .filter_map(|node| match &node.node {
404 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
405 path: path.clone(),
406 binding: ModuleImportBinding::Wildcard,
407 is_pub: *is_pub,
408 }),
409 harn_parser::Node::SelectiveImport {
410 names,
411 path,
412 is_pub,
413 } => Some(ModuleImportSpec {
414 path: path.clone(),
415 binding: ModuleImportBinding::Selected(names.clone()),
416 is_pub: *is_pub,
417 }),
418 harn_parser::Node::NamespaceImport {
419 alias,
420 path,
421 is_pub,
422 } => Some(ModuleImportSpec {
423 path: path.clone(),
424 binding: ModuleImportBinding::Namespace {
425 alias: alias.clone(),
426 demand: namespace_demands
427 .get(alias)
428 .cloned()
429 .unwrap_or(harn_parser::NamespaceDemand::Whole),
430 },
431 is_pub: *is_pub,
432 }),
433 _ => None,
434 })
435 .collect();
436
437 if provenance == ModuleProvenance::PrivilegedWire {
438 validate_privileged_wire_surface(program, &imports)?;
439 }
440
441 let compiler = || match provenance {
442 ModuleProvenance::User => crate::Compiler::new(),
443 ModuleProvenance::PrivilegedWire => {
444 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
445 }
446 ModuleProvenance::TrustedHostDispatch => {
447 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
448 }
449 };
450
451 let init_nodes: Vec<harn_parser::SNode> = program
452 .iter()
453 .filter(|sn| {
454 let inner = match &sn.node {
455 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
456 _ => sn,
457 };
458 matches!(
459 &inner.node,
460 harn_parser::Node::LetBinding { .. }
461 | harn_parser::Node::ConstBinding { .. }
462 | harn_parser::Node::EnumDecl { is_pub: true, .. }
469 | harn_parser::Node::ToolDecl { .. }
470 | harn_parser::Node::SkillDecl { .. }
471 | harn_parser::Node::EvalPackDecl { .. }
472 )
473 })
474 .cloned()
475 .collect();
476 let init_chunk = if init_nodes.is_empty() {
477 None
478 } else {
479 let compiler = compiler();
480 Some(
481 compiler
482 .compile_module_init(
483 program,
484 &init_nodes,
485 imported_enum_candidates,
486 imported_source_callable_names,
487 )
488 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
489 .freeze_for_cache(),
490 )
491 };
492
493 let public_exports: BTreeMap<String, DefKind> = program
494 .iter()
495 .flat_map(public_declarations)
496 .map(|export| (export.name, export.kind))
497 .collect();
498 let public_value_names = public_exports
499 .iter()
500 .filter(|(_, kind)| {
501 matches!(
502 kind,
503 DefKind::Variable
504 | DefKind::Enum
505 | DefKind::Tool
506 | DefKind::Skill
507 | DefKind::EvalPack
508 )
509 })
510 .map(|(name, _)| name.clone())
511 .collect();
512 let public_type_names = public_exports
513 .iter()
514 .filter(|(_, kind)| !kind.has_runtime_value())
515 .map(|(name, _)| name.clone())
516 .collect();
517
518 let mut functions = BTreeMap::new();
519 for node in program {
520 let inner = match &node.node {
521 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
522 _ => node,
523 };
524 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
525 let constructor = compiler()
530 .compile_struct_constructor(name, fields)
531 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
532 functions.insert(name.clone(), constructor.freeze_for_cache());
533 continue;
534 }
535 if let harn_parser::Node::Pipeline {
536 name,
537 params,
538 body,
539 extends,
540 ..
541 } = &inner.node
542 {
543 let mut compiler = compiler();
544 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
545 compiler
546 .add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
547 let pipeline = compiler
548 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
549 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
550 functions.insert(name.clone(), pipeline.freeze_for_cache());
551 continue;
552 }
553 let harn_parser::Node::FnDecl {
554 name,
555 type_params,
556 params,
557 body,
558 ..
559 } = &inner.node
560 else {
561 continue;
562 };
563
564 let mut compiler = compiler();
565 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
566 compiler.add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
567 compiler.prepare_module_context(program);
568 let func_chunk = compiler
569 .compile_fn_body(type_params, params, body, module_source_file.clone())
570 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
571 functions.insert(name.clone(), func_chunk.freeze_for_cache());
572 }
573
574 let type_schema_init_chunks =
575 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
576 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
577 .into_iter()
578 .map(|chunk| chunk.freeze_for_cache())
579 .collect();
580
581 Ok(ModuleArtifact {
582 provenance,
583 imports,
584 type_schema_init_chunks,
585 init_chunk,
586 functions,
587 public_exports,
588 public_value_names,
589 public_type_names,
590 })
591}
592
593fn validate_privileged_wire_surface(
594 program: &[harn_parser::SNode],
595 imports: &[ModuleImportSpec],
596) -> Result<(), VmError> {
597 if imports.iter().any(|import| import.is_pub) {
598 return Err(VmError::Runtime(
599 "Privileged wire modules cannot re-export imports".to_string(),
600 ));
601 }
602 for export in program.iter().flat_map(public_declarations) {
603 if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
604 return Err(VmError::Runtime(format!(
605 "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
606 export.name, export.kind
607 )));
608 }
609 }
610 Ok(())
611}
612
613pub fn compile_module_artifact_from_source(
617 source_path: &Path,
618 source: &str,
619) -> Result<ModuleArtifact, VmError> {
620 let program = parse_module_source(source_path, source)?;
621 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
622 compile_module_artifact_with_imported_symbols(
623 &program,
624 Some(source_path.display().to_string()),
625 &imported_symbols.enum_candidates,
626 &imported_symbols.source_callable_names,
627 )
628}
629
630#[cfg(test)]
638thread_local! {
639 pub(crate) static INTERFACE_RESOLUTIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
640}
641
642pub fn module_compilation_context_for_source(
646 source_path: &Path,
647 source: &str,
648) -> Result<ModuleCompilationContext, VmError> {
649 #[cfg(test)]
650 INTERFACE_RESOLUTIONS.with(|count| count.set(count.get() + 1));
651 let program = parse_module_source(source_path, source)?;
652 Ok(imported_symbol_projection_for_program(
653 source_path,
654 source,
655 &program,
656 ))
657}
658
659pub fn compile_privileged_wire_module_artifact_from_source(
668 source_path: &Path,
669 source: &str,
670) -> Result<ModuleArtifact, VmError> {
671 let program = parse_module_source(source_path, source)?;
672 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
673 compile_module_artifact_with_provenance(
674 &program,
675 Some(source_path.display().to_string()),
676 &imported_symbols.enum_candidates,
677 &imported_symbols.source_callable_names,
678 ModuleProvenance::PrivilegedWire,
679 )
680}
681
682pub fn compile_trusted_host_dispatch_module_artifact_from_source(
687 source_path: &Path,
688 source: &str,
689) -> Result<ModuleArtifact, VmError> {
690 let program = parse_module_source(source_path, source)?;
691 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
692 compile_module_artifact_with_provenance(
693 &program,
694 Some(source_path.display().to_string()),
695 &imported_symbols.enum_candidates,
696 &imported_symbols.source_callable_names,
697 ModuleProvenance::TrustedHostDispatch,
698 )
699}
700
701pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_enums(
706 source_path: &Path,
707 source: &str,
708 imported_enum_candidates: impl IntoIterator<Item = String>,
709) -> Result<ModuleArtifact, VmError> {
710 let program = parse_module_source(source_path, source)?;
711 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
712 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
713 compile_module_artifact_with_provenance(
714 &program,
715 Some(source_path.display().to_string()),
716 &imported_enum_candidates,
717 &imported_symbols.source_callable_names,
718 ModuleProvenance::TrustedHostDispatch,
719 )
720}
721
722pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_symbols(
723 source_path: &Path,
724 source: &str,
725 imported_enum_candidates: impl IntoIterator<Item = String>,
726 imported_source_callable_names: impl IntoIterator<Item = String>,
727) -> Result<ModuleArtifact, VmError> {
728 let context =
729 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
730 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
731 source_path,
732 source,
733 &context,
734 )
735}
736
737pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_context(
738 source_path: &Path,
739 source: &str,
740 context: &ModuleCompilationContext,
741) -> Result<ModuleArtifact, VmError> {
742 let program = parse_module_source(source_path, source)?;
743 compile_module_artifact_with_provenance(
744 &program,
745 Some(source_path.display().to_string()),
746 context.enum_candidates(),
747 context.source_callable_names(),
748 ModuleProvenance::TrustedHostDispatch,
749 )
750}
751
752fn imported_symbol_projection_for_program(
758 source_path: &Path,
759 source: &str,
760 program: &[harn_parser::SNode],
761) -> ModuleCompilationContext {
762 if !needs_imported_symbol_projection(program) {
763 return ModuleCompilationContext::default();
764 }
765 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
766 let cache_key = harn_modules::canonical_path(source_path);
767 let cacheable = is_immutable_stdlib_path(source_path);
768 if cacheable {
769 if let Some((_cached_hash, projection)) = imported_symbol_cache()
770 .lock()
771 .expect("imported symbol cache lock poisoned")
772 .get(&cache_key)
773 .filter(|(cached_hash, _)| *cached_hash == source_hash)
774 {
775 return projection.clone();
776 }
777 }
778
779 let graph = harn_modules::build_with_source(source_path, source);
783 if !cacheable {
784 return sorted_imported_symbol_projection(&graph, source_path);
785 }
786 let mut projections = Vec::new();
787 for path in graph.module_paths() {
788 let module_source = if path == cache_key {
789 Some(source.to_string())
790 } else {
791 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
792 };
793 let Some(module_source) = module_source else {
794 continue;
795 };
796 let projection = sorted_imported_symbol_projection(&graph, &path);
797 projections.push((
798 path,
799 (
800 *blake3::hash(module_source.as_bytes()).as_bytes(),
801 projection,
802 ),
803 ));
804 }
805 let mut cache = imported_symbol_cache()
806 .lock()
807 .expect("imported symbol cache lock poisoned");
808 for (path, projection) in projections {
809 if is_immutable_stdlib_path(&path) {
810 cache.insert(path, projection);
811 }
812 }
813 cache
814 .get(&cache_key)
815 .filter(|(cached_hash, _)| *cached_hash == source_hash)
816 .map(|(_, projection)| projection.clone())
817 .unwrap_or_default()
818}
819
820fn sorted_imported_symbol_projection(
821 graph: &harn_modules::ModuleGraph,
822 source_path: &Path,
823) -> ModuleCompilationContext {
824 ModuleCompilationContext::from_graph(graph, source_path)
825}
826
827fn is_immutable_stdlib_path(path: &Path) -> bool {
828 path.to_str()
829 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
830}
831
832fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
833 harn_parser::visit::contains_identifier_enum_pattern(program)
834}
835
836fn needs_imported_symbol_projection(program: &[harn_parser::SNode]) -> bool {
837 needs_imported_enum_candidates(program) || needs_imported_source_callable_names(program)
838}
839
840fn needs_imported_source_callable_names(program: &[harn_parser::SNode]) -> bool {
841 let has_wildcard_import = program.iter().any(|node| match &node.node {
842 harn_parser::Node::ImportDecl { .. } => true,
843 harn_parser::Node::AttributedDecl { inner, .. } => {
844 matches!(&inner.node, harn_parser::Node::ImportDecl { .. })
845 }
846 _ => false,
847 });
848 if !has_wildcard_import {
849 return false;
850 }
851 let mut needs_projection = false;
852 harn_parser::visit::walk_program(program, &mut |node| {
853 if let harn_parser::Node::FunctionCall { name, .. } = &node.node {
854 needs_projection |= harn_parser::builtin_signatures::is_builtin(name);
855 }
856 });
857 needs_projection
858}
859
860fn parse_module_source(
861 source_path: &Path,
862 source: &str,
863) -> Result<Vec<harn_parser::SNode>, VmError> {
864 let mut lexer = harn_lexer::Lexer::new(source);
865 let tokens = lexer.tokenize().map_err(|e| {
866 VmError::Runtime(format!(
867 "Import lex error in {}: {e}",
868 source_path.display()
869 ))
870 })?;
871 let mut parser = harn_parser::Parser::new(tokens);
872 parser.parse().map_err(|e| {
873 VmError::Runtime(format!(
874 "Import parse error in {}: {e}",
875 source_path.display()
876 ))
877 })
878}
879
880pub fn compile_module_artifact_from_source_with_imported_enums(
885 source_path: &Path,
886 source: &str,
887 imported_enum_candidates: impl IntoIterator<Item = String>,
888) -> Result<ModuleArtifact, VmError> {
889 let program = parse_module_source(source_path, source)?;
890 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
891 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
892 compile_module_artifact_with_imported_symbols(
893 &program,
894 Some(source_path.display().to_string()),
895 &imported_enum_candidates,
896 &imported_symbols.source_callable_names,
897 )
898}
899
900pub fn compile_module_artifact_from_source_with_imported_symbols(
904 source_path: &Path,
905 source: &str,
906 imported_enum_candidates: impl IntoIterator<Item = String>,
907 imported_source_callable_names: impl IntoIterator<Item = String>,
908) -> Result<ModuleArtifact, VmError> {
909 let context =
910 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
911 compile_module_artifact_from_source_with_context(source_path, source, &context)
912}
913
914pub fn compile_module_artifact_from_source_with_context(
915 source_path: &Path,
916 source: &str,
917 context: &ModuleCompilationContext,
918) -> Result<ModuleArtifact, VmError> {
919 let program = parse_module_source(source_path, source)?;
920 compile_module_artifact_with_imported_symbols(
921 &program,
922 Some(source_path.display().to_string()),
923 context.enum_candidates(),
924 context.source_callable_names(),
925 )
926}
927
928#[cfg(test)]
929mod tests {
930 use std::path::Path;
931
932 use harn_lexer::Lexer;
933 use harn_parser::Parser;
934
935 use super::{
936 compile_module_artifact, compile_module_artifact_from_source,
937 compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
938 parse_module_source, ModuleImportBinding, ModuleProvenance,
939 };
940 use crate::chunk::Constant;
941
942 #[test]
943 fn module_init_schema_of_uses_full_program_aliases() {
944 let source = r"
945pub type Item = {id: string}
946const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
947";
948 let mut lexer = Lexer::new(source);
949 let tokens = lexer.tokenize().unwrap();
950 let mut parser = Parser::new(tokens);
951 let program = parser.parse().unwrap();
952 let artifact = compile_module_artifact(&program, None).unwrap();
953 let constants = &artifact.init_chunk.expect("init chunk").constants;
954 let strings = constants
955 .iter()
956 .filter_map(|constant| match constant {
957 Constant::String(value) => Some(value.as_str()),
958 _ => None,
959 })
960 .collect::<Vec<_>>();
961 assert!(strings.contains(&"id"), "{strings:?}");
962 assert!(!strings.contains(&"Item"), "{strings:?}");
963 }
964
965 #[test]
966 fn type_only_modules_use_a_separate_schema_initializer() {
967 let source = r"
968pub type UserShape = {name: string, active?: bool}
969pub type UserList = list<UserShape>
970";
971
972 let artifact =
973 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
974 .expect("module compiles");
975
976 assert!(
977 artifact.init_chunk.is_none(),
978 "erased type aliases must not inflate module init bytecode"
979 );
980 assert!(artifact.public_type_names.contains("UserShape"));
981 assert!(artifact.public_type_names.contains("UserList"));
982 assert_eq!(artifact.type_schema_init_chunks.len(), 2);
983 }
984
985 #[test]
986 fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
987 let source = r#"
988pub enum KeptStatus { Ready }
989pub enum DeadStatus { Gone }
990pub struct KeptConfig { value: int }
991pub struct DeadConfig { value: string }
992pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
993pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
994"#;
995 let source_path = Path::new("<test>/declarations.harn");
996 let parsed = parse_module_source(source_path, source).expect("module parses");
997 let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
998 .expect("module compiles");
999 let selected = super::specialize_module_artifact(
1000 &parsed,
1001 Some(source_path.display().to_string()),
1002 full,
1003 &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
1004 "KeptStatus".to_string(),
1005 "KeptConfig".to_string(),
1006 "kept_pipeline".to_string(),
1007 ])),
1008 )
1009 .expect("specialization succeeds");
1010
1011 assert!(selected.public_exports.contains_key("KeptStatus"));
1012 assert!(selected.public_exports.contains_key("KeptConfig"));
1013 assert!(selected.public_exports.contains_key("kept_pipeline"));
1014 assert!(!selected.public_exports.contains_key("DeadStatus"));
1015 assert!(!selected.public_exports.contains_key("DeadConfig"));
1016 assert!(!selected.public_exports.contains_key("dead_pipeline"));
1017 assert!(selected.functions.contains_key("KeptConfig"));
1018 assert!(selected.functions.contains_key("kept_pipeline"));
1019 assert!(!selected.functions.contains_key("DeadConfig"));
1020 assert!(!selected.functions.contains_key("dead_pipeline"));
1021 }
1022
1023 #[test]
1024 fn nested_namespace_import_retains_static_member_demand() {
1025 let artifact = compile_module_artifact_from_source(
1026 Path::new("<test>/wrapper.harn"),
1027 r#"
1028import * as lib from "./lib"
1029pub fn call() { return lib.greet() }
1030"#,
1031 )
1032 .expect("module compiles");
1033
1034 let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
1035 panic!("expected namespace import metadata");
1036 };
1037 assert_eq!(alias, "lib");
1038 assert_eq!(
1039 demand,
1040 &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
1041 "greet".to_string(),
1042 ]))
1043 );
1044 }
1045
1046 #[test]
1047 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
1048 let error = compile_module_artifact_from_source(
1049 Path::new("<test>/user.harn"),
1050 r#"fn probe() { host_call("project.scan", {}) }"#,
1051 )
1052 .expect_err("ordinary source must not acquire wire authority");
1053 assert!(
1054 error.to_string().contains("not callable source API"),
1055 "{error}"
1056 );
1057 }
1058
1059 #[test]
1060 fn explicit_privileged_compilation_stamps_private_wire_code() {
1061 let artifact = compile_privileged_wire_module_artifact_from_source(
1062 Path::new("<trusted>/wire.harn"),
1063 r#"fn probe() { host_call("project.scan", {}) }"#,
1064 )
1065 .expect("trusted private wire function compiles");
1066 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
1067 assert!(artifact.functions.contains_key("probe"));
1068 assert!(artifact.public_exports.is_empty());
1069 }
1070
1071 #[test]
1072 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
1073 let error = compile_privileged_wire_module_artifact_from_source(
1074 Path::new("<trusted>/wire.harn"),
1075 r#"pub fn probe() { host_call("project.scan", {}) }"#,
1076 )
1077 .expect_err("wire closures must not be exportable");
1078 assert!(
1079 error
1080 .to_string()
1081 .contains("only explicit capability-value bindings"),
1082 "{error}"
1083 );
1084 }
1085
1086 #[test]
1087 fn privileged_wire_modules_cannot_reexport_imports() {
1088 let error = compile_privileged_wire_module_artifact_from_source(
1089 Path::new("<trusted>/wire.harn"),
1090 r#"pub import { probe } from "./other""#,
1091 )
1092 .expect_err("wire authority must be non-reexportable");
1093 assert!(
1094 error.to_string().contains("cannot re-export imports"),
1095 "{error}"
1096 );
1097 }
1098
1099 #[test]
1100 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
1101 let source = r#"
1102import { External } from "./external"
1103pub type Wrapped = {value: External}
1104"#;
1105 let source_path = Path::new("<test>/wrapped.harn");
1106 let artifact =
1107 compile_module_artifact_from_source(source_path, source).expect("module compiles");
1108 let chunk = artifact
1109 .type_schema_init_chunks
1110 .into_iter()
1111 .next()
1112 .expect("schema initializer");
1113 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
1114 assert!(chunk
1115 .constants
1116 .iter()
1117 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
1118 }
1119
1120 #[test]
1121 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
1122 let plain = parse_module_source(
1123 Path::new("<test>/plain.harn"),
1124 r#"
1125import { helper } from "./support"
1126pub fn run() -> int { return helper(1) }
1127"#,
1128 )
1129 .expect("plain module parses");
1130 assert!(!needs_imported_enum_candidates(&plain));
1131
1132 let qualified = parse_module_source(
1133 Path::new("<test>/qualified.harn"),
1134 r#"
1135import { Status } from "./status"
1136pub fn run(value: Status) {
1137 match value {
1138 Status.Ready -> { return 1 }
1139 _ -> { return 0 }
1140 }
1141}
1142"#,
1143 )
1144 .expect("qualified module parses");
1145 assert!(needs_imported_enum_candidates(&qualified));
1146 }
1147
1148 #[test]
1149 fn private_declarations_do_not_expand_module_init() {
1150 let artifact = compile_module_artifact_from_source(
1151 Path::new("<test>/private-declarations.harn"),
1152 r"
1153enum PrivateStatus { Ready }
1154struct PrivateConfig { value: int }
1155pub fn run() { return PrivateStatus.Ready }
1156",
1157 )
1158 .expect("private declarations compile");
1159
1160 assert!(artifact.init_chunk.is_none());
1161 assert!(artifact.functions.contains_key("PrivateConfig"));
1162 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
1163 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
1164 }
1165}