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
630pub fn module_compilation_context_for_source(
634 source_path: &Path,
635 source: &str,
636) -> Result<ModuleCompilationContext, VmError> {
637 let program = parse_module_source(source_path, source)?;
638 Ok(imported_symbol_projection_for_program(
639 source_path,
640 source,
641 &program,
642 ))
643}
644
645pub fn compile_privileged_wire_module_artifact_from_source(
654 source_path: &Path,
655 source: &str,
656) -> Result<ModuleArtifact, VmError> {
657 let program = parse_module_source(source_path, source)?;
658 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
659 compile_module_artifact_with_provenance(
660 &program,
661 Some(source_path.display().to_string()),
662 &imported_symbols.enum_candidates,
663 &imported_symbols.source_callable_names,
664 ModuleProvenance::PrivilegedWire,
665 )
666}
667
668pub fn compile_trusted_host_dispatch_module_artifact_from_source(
673 source_path: &Path,
674 source: &str,
675) -> Result<ModuleArtifact, VmError> {
676 let program = parse_module_source(source_path, source)?;
677 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
678 compile_module_artifact_with_provenance(
679 &program,
680 Some(source_path.display().to_string()),
681 &imported_symbols.enum_candidates,
682 &imported_symbols.source_callable_names,
683 ModuleProvenance::TrustedHostDispatch,
684 )
685}
686
687pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_enums(
692 source_path: &Path,
693 source: &str,
694 imported_enum_candidates: impl IntoIterator<Item = String>,
695) -> Result<ModuleArtifact, VmError> {
696 let program = parse_module_source(source_path, source)?;
697 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
698 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
699 compile_module_artifact_with_provenance(
700 &program,
701 Some(source_path.display().to_string()),
702 &imported_enum_candidates,
703 &imported_symbols.source_callable_names,
704 ModuleProvenance::TrustedHostDispatch,
705 )
706}
707
708pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_symbols(
709 source_path: &Path,
710 source: &str,
711 imported_enum_candidates: impl IntoIterator<Item = String>,
712 imported_source_callable_names: impl IntoIterator<Item = String>,
713) -> Result<ModuleArtifact, VmError> {
714 let context =
715 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
716 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
717 source_path,
718 source,
719 &context,
720 )
721}
722
723pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_context(
724 source_path: &Path,
725 source: &str,
726 context: &ModuleCompilationContext,
727) -> Result<ModuleArtifact, VmError> {
728 let program = parse_module_source(source_path, source)?;
729 compile_module_artifact_with_provenance(
730 &program,
731 Some(source_path.display().to_string()),
732 context.enum_candidates(),
733 context.source_callable_names(),
734 ModuleProvenance::TrustedHostDispatch,
735 )
736}
737
738fn imported_symbol_projection_for_program(
744 source_path: &Path,
745 source: &str,
746 program: &[harn_parser::SNode],
747) -> ModuleCompilationContext {
748 if !needs_imported_symbol_projection(program) {
749 return ModuleCompilationContext::default();
750 }
751 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
752 let cache_key = harn_modules::canonical_path(source_path);
753 let cacheable = is_immutable_stdlib_path(source_path);
754 if cacheable {
755 if let Some((_cached_hash, projection)) = imported_symbol_cache()
756 .lock()
757 .expect("imported symbol cache lock poisoned")
758 .get(&cache_key)
759 .filter(|(cached_hash, _)| *cached_hash == source_hash)
760 {
761 return projection.clone();
762 }
763 }
764
765 let graph = harn_modules::build_with_source(source_path, source);
769 if !cacheable {
770 return sorted_imported_symbol_projection(&graph, source_path);
771 }
772 let mut projections = Vec::new();
773 for path in graph.module_paths() {
774 let module_source = if path == cache_key {
775 Some(source.to_string())
776 } else {
777 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
778 };
779 let Some(module_source) = module_source else {
780 continue;
781 };
782 let projection = sorted_imported_symbol_projection(&graph, &path);
783 projections.push((
784 path,
785 (
786 *blake3::hash(module_source.as_bytes()).as_bytes(),
787 projection,
788 ),
789 ));
790 }
791 let mut cache = imported_symbol_cache()
792 .lock()
793 .expect("imported symbol cache lock poisoned");
794 for (path, projection) in projections {
795 if is_immutable_stdlib_path(&path) {
796 cache.insert(path, projection);
797 }
798 }
799 cache
800 .get(&cache_key)
801 .filter(|(cached_hash, _)| *cached_hash == source_hash)
802 .map(|(_, projection)| projection.clone())
803 .unwrap_or_default()
804}
805
806fn sorted_imported_symbol_projection(
807 graph: &harn_modules::ModuleGraph,
808 source_path: &Path,
809) -> ModuleCompilationContext {
810 ModuleCompilationContext::from_graph(graph, source_path)
811}
812
813fn is_immutable_stdlib_path(path: &Path) -> bool {
814 path.to_str()
815 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
816}
817
818fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
819 harn_parser::visit::contains_identifier_enum_pattern(program)
820}
821
822fn needs_imported_symbol_projection(program: &[harn_parser::SNode]) -> bool {
823 needs_imported_enum_candidates(program) || needs_imported_source_callable_names(program)
824}
825
826fn needs_imported_source_callable_names(program: &[harn_parser::SNode]) -> bool {
827 let has_wildcard_import = program.iter().any(|node| match &node.node {
828 harn_parser::Node::ImportDecl { .. } => true,
829 harn_parser::Node::AttributedDecl { inner, .. } => {
830 matches!(&inner.node, harn_parser::Node::ImportDecl { .. })
831 }
832 _ => false,
833 });
834 if !has_wildcard_import {
835 return false;
836 }
837 let mut needs_projection = false;
838 harn_parser::visit::walk_program(program, &mut |node| {
839 if let harn_parser::Node::FunctionCall { name, .. } = &node.node {
840 needs_projection |= harn_parser::builtin_signatures::is_builtin(name);
841 }
842 });
843 needs_projection
844}
845
846fn parse_module_source(
847 source_path: &Path,
848 source: &str,
849) -> Result<Vec<harn_parser::SNode>, VmError> {
850 let mut lexer = harn_lexer::Lexer::new(source);
851 let tokens = lexer.tokenize().map_err(|e| {
852 VmError::Runtime(format!(
853 "Import lex error in {}: {e}",
854 source_path.display()
855 ))
856 })?;
857 let mut parser = harn_parser::Parser::new(tokens);
858 parser.parse().map_err(|e| {
859 VmError::Runtime(format!(
860 "Import parse error in {}: {e}",
861 source_path.display()
862 ))
863 })
864}
865
866pub fn compile_module_artifact_from_source_with_imported_enums(
871 source_path: &Path,
872 source: &str,
873 imported_enum_candidates: impl IntoIterator<Item = String>,
874) -> Result<ModuleArtifact, VmError> {
875 let program = parse_module_source(source_path, source)?;
876 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
877 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
878 compile_module_artifact_with_imported_symbols(
879 &program,
880 Some(source_path.display().to_string()),
881 &imported_enum_candidates,
882 &imported_symbols.source_callable_names,
883 )
884}
885
886pub fn compile_module_artifact_from_source_with_imported_symbols(
890 source_path: &Path,
891 source: &str,
892 imported_enum_candidates: impl IntoIterator<Item = String>,
893 imported_source_callable_names: impl IntoIterator<Item = String>,
894) -> Result<ModuleArtifact, VmError> {
895 let context =
896 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
897 compile_module_artifact_from_source_with_context(source_path, source, &context)
898}
899
900pub fn compile_module_artifact_from_source_with_context(
901 source_path: &Path,
902 source: &str,
903 context: &ModuleCompilationContext,
904) -> Result<ModuleArtifact, VmError> {
905 let program = parse_module_source(source_path, source)?;
906 compile_module_artifact_with_imported_symbols(
907 &program,
908 Some(source_path.display().to_string()),
909 context.enum_candidates(),
910 context.source_callable_names(),
911 )
912}
913
914#[cfg(test)]
915mod tests {
916 use std::path::Path;
917
918 use harn_lexer::Lexer;
919 use harn_parser::Parser;
920
921 use super::{
922 compile_module_artifact, compile_module_artifact_from_source,
923 compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
924 parse_module_source, ModuleImportBinding, ModuleProvenance,
925 };
926 use crate::chunk::Constant;
927
928 #[test]
929 fn module_init_schema_of_uses_full_program_aliases() {
930 let source = r"
931pub type Item = {id: string}
932const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
933";
934 let mut lexer = Lexer::new(source);
935 let tokens = lexer.tokenize().unwrap();
936 let mut parser = Parser::new(tokens);
937 let program = parser.parse().unwrap();
938 let artifact = compile_module_artifact(&program, None).unwrap();
939 let constants = &artifact.init_chunk.expect("init chunk").constants;
940 let strings = constants
941 .iter()
942 .filter_map(|constant| match constant {
943 Constant::String(value) => Some(value.as_str()),
944 _ => None,
945 })
946 .collect::<Vec<_>>();
947 assert!(strings.contains(&"id"), "{strings:?}");
948 assert!(!strings.contains(&"Item"), "{strings:?}");
949 }
950
951 #[test]
952 fn type_only_modules_use_a_separate_schema_initializer() {
953 let source = r"
954pub type UserShape = {name: string, active?: bool}
955pub type UserList = list<UserShape>
956";
957
958 let artifact =
959 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
960 .expect("module compiles");
961
962 assert!(
963 artifact.init_chunk.is_none(),
964 "erased type aliases must not inflate module init bytecode"
965 );
966 assert!(artifact.public_type_names.contains("UserShape"));
967 assert!(artifact.public_type_names.contains("UserList"));
968 assert_eq!(artifact.type_schema_init_chunks.len(), 2);
969 }
970
971 #[test]
972 fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
973 let source = r#"
974pub enum KeptStatus { Ready }
975pub enum DeadStatus { Gone }
976pub struct KeptConfig { value: int }
977pub struct DeadConfig { value: string }
978pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
979pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
980"#;
981 let source_path = Path::new("<test>/declarations.harn");
982 let parsed = parse_module_source(source_path, source).expect("module parses");
983 let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
984 .expect("module compiles");
985 let selected = super::specialize_module_artifact(
986 &parsed,
987 Some(source_path.display().to_string()),
988 full,
989 &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
990 "KeptStatus".to_string(),
991 "KeptConfig".to_string(),
992 "kept_pipeline".to_string(),
993 ])),
994 )
995 .expect("specialization succeeds");
996
997 assert!(selected.public_exports.contains_key("KeptStatus"));
998 assert!(selected.public_exports.contains_key("KeptConfig"));
999 assert!(selected.public_exports.contains_key("kept_pipeline"));
1000 assert!(!selected.public_exports.contains_key("DeadStatus"));
1001 assert!(!selected.public_exports.contains_key("DeadConfig"));
1002 assert!(!selected.public_exports.contains_key("dead_pipeline"));
1003 assert!(selected.functions.contains_key("KeptConfig"));
1004 assert!(selected.functions.contains_key("kept_pipeline"));
1005 assert!(!selected.functions.contains_key("DeadConfig"));
1006 assert!(!selected.functions.contains_key("dead_pipeline"));
1007 }
1008
1009 #[test]
1010 fn nested_namespace_import_retains_static_member_demand() {
1011 let artifact = compile_module_artifact_from_source(
1012 Path::new("<test>/wrapper.harn"),
1013 r#"
1014import * as lib from "./lib"
1015pub fn call() { return lib.greet() }
1016"#,
1017 )
1018 .expect("module compiles");
1019
1020 let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
1021 panic!("expected namespace import metadata");
1022 };
1023 assert_eq!(alias, "lib");
1024 assert_eq!(
1025 demand,
1026 &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
1027 "greet".to_string(),
1028 ]))
1029 );
1030 }
1031
1032 #[test]
1033 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
1034 let error = compile_module_artifact_from_source(
1035 Path::new("<test>/user.harn"),
1036 r#"fn probe() { host_call("project.scan", {}) }"#,
1037 )
1038 .expect_err("ordinary source must not acquire wire authority");
1039 assert!(
1040 error.to_string().contains("not callable source API"),
1041 "{error}"
1042 );
1043 }
1044
1045 #[test]
1046 fn explicit_privileged_compilation_stamps_private_wire_code() {
1047 let artifact = compile_privileged_wire_module_artifact_from_source(
1048 Path::new("<trusted>/wire.harn"),
1049 r#"fn probe() { host_call("project.scan", {}) }"#,
1050 )
1051 .expect("trusted private wire function compiles");
1052 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
1053 assert!(artifact.functions.contains_key("probe"));
1054 assert!(artifact.public_exports.is_empty());
1055 }
1056
1057 #[test]
1058 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
1059 let error = compile_privileged_wire_module_artifact_from_source(
1060 Path::new("<trusted>/wire.harn"),
1061 r#"pub fn probe() { host_call("project.scan", {}) }"#,
1062 )
1063 .expect_err("wire closures must not be exportable");
1064 assert!(
1065 error
1066 .to_string()
1067 .contains("only explicit capability-value bindings"),
1068 "{error}"
1069 );
1070 }
1071
1072 #[test]
1073 fn privileged_wire_modules_cannot_reexport_imports() {
1074 let error = compile_privileged_wire_module_artifact_from_source(
1075 Path::new("<trusted>/wire.harn"),
1076 r#"pub import { probe } from "./other""#,
1077 )
1078 .expect_err("wire authority must be non-reexportable");
1079 assert!(
1080 error.to_string().contains("cannot re-export imports"),
1081 "{error}"
1082 );
1083 }
1084
1085 #[test]
1086 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
1087 let source = r#"
1088import { External } from "./external"
1089pub type Wrapped = {value: External}
1090"#;
1091 let source_path = Path::new("<test>/wrapped.harn");
1092 let artifact =
1093 compile_module_artifact_from_source(source_path, source).expect("module compiles");
1094 let chunk = artifact
1095 .type_schema_init_chunks
1096 .into_iter()
1097 .next()
1098 .expect("schema initializer");
1099 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
1100 assert!(chunk
1101 .constants
1102 .iter()
1103 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
1104 }
1105
1106 #[test]
1107 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
1108 let plain = parse_module_source(
1109 Path::new("<test>/plain.harn"),
1110 r#"
1111import { helper } from "./support"
1112pub fn run() -> int { return helper(1) }
1113"#,
1114 )
1115 .expect("plain module parses");
1116 assert!(!needs_imported_enum_candidates(&plain));
1117
1118 let qualified = parse_module_source(
1119 Path::new("<test>/qualified.harn"),
1120 r#"
1121import { Status } from "./status"
1122pub fn run(value: Status) {
1123 match value {
1124 Status.Ready -> { return 1 }
1125 _ -> { return 0 }
1126 }
1127}
1128"#,
1129 )
1130 .expect("qualified module parses");
1131 assert!(needs_imported_enum_candidates(&qualified));
1132 }
1133
1134 #[test]
1135 fn private_declarations_do_not_expand_module_init() {
1136 let artifact = compile_module_artifact_from_source(
1137 Path::new("<test>/private-declarations.harn"),
1138 r"
1139enum PrivateStatus { Ready }
1140struct PrivateConfig { value: int }
1141pub fn run() { return PrivateStatus.Ready }
1142",
1143 )
1144 .expect("private declarations compile");
1145
1146 assert!(artifact.init_chunk.is_none());
1147 assert!(artifact.functions.contains_key("PrivateConfig"));
1148 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
1149 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
1150 }
1151}