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 EmbeddedStdlib,
129 PrivilegedWire,
130 TrustedHostDispatch,
135}
136
137fn imported_symbol_cache() -> &'static Mutex<ImportedSymbolCache> {
138 static CACHE: OnceLock<Mutex<ImportedSymbolCache>> = OnceLock::new();
139 CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
146pub struct ModuleImportSpec {
147 pub path: String,
148 pub binding: ModuleImportBinding,
149 pub is_pub: bool,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize)]
154pub enum ModuleImportBinding {
155 Wildcard,
156 Selected(Vec<String>),
157 Namespace {
158 alias: String,
159 demand: harn_parser::NamespaceDemand,
160 },
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct ModuleArtifact {
170 #[serde(default)]
171 pub provenance: ModuleProvenance,
172 pub imports: Vec<ModuleImportSpec>,
173 pub type_schema_init_chunks: Vec<CachedChunk>,
176 pub init_chunk: Option<CachedChunk>,
177 pub functions: BTreeMap<String, CachedCompiledFunction>,
178 pub public_exports: BTreeMap<String, DefKind>,
183 pub public_value_names: HashSet<String>,
187 pub public_type_names: HashSet<String>,
192}
193
194pub fn specialize_module_artifact(
201 program: &[harn_parser::SNode],
202 source_file: Option<String>,
203 mut artifact: ModuleArtifact,
204 demand: &harn_modules::ExportDemand,
205) -> Result<ModuleArtifact, VmError> {
206 use harn_parser::Node;
207 use std::collections::{BTreeSet, HashMap};
208
209 if matches!(demand, harn_modules::ExportDemand::WholeNamespace) {
210 return Ok(artifact);
211 }
212
213 if artifact.imports.iter().any(|import| import.is_pub) {
218 return Ok(artifact);
219 }
220
221 let callable_names = artifact.functions.keys().cloned().collect::<HashSet<_>>();
222 let mut declarations = HashMap::<String, &harn_parser::SNode>::new();
223 for node in program {
224 let inner = match &node.node {
225 Node::AttributedDecl { inner, .. } => inner.as_ref(),
226 _ => node,
227 };
228 let name = match &inner.node {
229 Node::FnDecl { name, .. }
230 | Node::Pipeline { name, .. }
231 | Node::StructDecl { name, .. } => Some(name),
232 _ => None,
233 };
234 if let Some(name) = name {
235 declarations.insert(name.clone(), inner);
236 }
237 }
238
239 let mut pending = Vec::new();
240 if let harn_modules::ExportDemand::Members(members) = demand {
241 pending.extend(
242 members
243 .iter()
244 .filter(|name| callable_names.contains(*name))
245 .cloned(),
246 );
247 }
248 for node in program {
250 let inner = match &node.node {
251 Node::AttributedDecl { inner, .. } => inner.as_ref(),
252 _ => node,
253 };
254 if matches!(
255 &inner.node,
256 Node::LetBinding { .. }
257 | Node::ConstBinding { .. }
258 | Node::EnumDecl { is_pub: true, .. }
259 | Node::ToolDecl { .. }
260 | Node::SkillDecl { .. }
261 | Node::EvalPackDecl { .. }
262 ) {
263 collect_callable_references(inner, &callable_names, &mut pending);
264 }
265 }
266
267 let mut retained = HashSet::new();
268 while let Some(name) = pending.pop() {
269 if !retained.insert(name.clone()) {
270 continue;
271 }
272 if let Some(declaration) = declarations.get(&name) {
273 collect_callable_references(declaration, &callable_names, &mut pending);
274 }
275 }
276 artifact.functions.retain(|name, _| retained.contains(name));
277 artifact
278 .public_exports
279 .retain(|name, _| demand.contains(name));
280 artifact
281 .public_value_names
282 .retain(|name| demand.contains(name));
283 artifact
284 .public_type_names
285 .retain(|name| demand.contains(name));
286
287 let selected_type_names = artifact
288 .public_type_names
289 .iter()
290 .cloned()
291 .collect::<BTreeSet<_>>();
292 artifact.type_schema_init_chunks =
293 crate::Compiler::compile_selected_public_type_schema_initializers(
294 program,
295 source_file,
296 Some(&selected_type_names),
297 )
298 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
299 .into_iter()
300 .map(|chunk| chunk.freeze_for_cache())
301 .collect();
302 Ok(artifact)
303}
304
305fn collect_callable_references(
306 node: &harn_parser::SNode,
307 callable_names: &HashSet<String>,
308 out: &mut Vec<String>,
309) {
310 use harn_parser::Node;
311 let referenced = match &node.node {
312 Node::Identifier(name)
313 | Node::FunctionCall { name, .. }
314 | Node::StructConstruct {
315 struct_name: name, ..
316 }
317 | Node::EnumConstruct {
318 enum_name: name, ..
319 } => Some(name),
320 _ => None,
321 };
322 if let Some(name) = referenced.filter(|name| callable_names.contains(*name)) {
323 out.push(name.clone());
324 }
325 for child in harn_parser::visit::immediate_children(node) {
326 collect_callable_references(child, callable_names, out);
327 }
328}
329
330impl ModuleArtifact {
331 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
338 let source_file = source_path.display().to_string();
339 for chunk in &mut self.type_schema_init_chunks {
340 bind_chunk_source_file(chunk, &source_file);
341 }
342 if let Some(chunk) = &mut self.init_chunk {
343 bind_chunk_source_file(chunk, &source_file);
344 }
345 for function in self.functions.values_mut() {
346 bind_chunk_source_file(&mut function.chunk, &source_file);
347 }
348 }
349}
350
351fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
352 chunk.source_file = Some(source_file.to_string());
353 for function in &mut chunk.functions {
354 bind_chunk_source_file(&mut function.chunk, source_file);
355 }
356}
357
358pub fn compile_module_artifact(
363 program: &[harn_parser::SNode],
364 module_source_file: Option<String>,
365) -> Result<ModuleArtifact, VmError> {
366 let imported_symbols = module_source_file
367 .as_deref()
368 .filter(|_| needs_imported_symbol_projection(program))
369 .map(|path| {
370 let graph = harn_modules::build(&[Path::new(path).to_path_buf()]);
371 sorted_imported_symbol_projection(&graph, Path::new(path))
372 })
373 .unwrap_or_default();
374 compile_module_artifact_with_imported_symbols(
375 program,
376 module_source_file,
377 &imported_symbols.enum_candidates,
378 &imported_symbols.source_callable_names,
379 )
380}
381
382fn compile_module_artifact_with_imported_symbols(
383 program: &[harn_parser::SNode],
384 module_source_file: Option<String>,
385 imported_enum_candidates: &[String],
386 imported_source_callable_names: &[String],
387) -> Result<ModuleArtifact, VmError> {
388 compile_module_artifact_with_provenance(
389 program,
390 module_source_file,
391 imported_enum_candidates,
392 imported_source_callable_names,
393 ModuleProvenance::User,
394 )
395}
396
397fn compile_module_artifact_with_provenance(
398 program: &[harn_parser::SNode],
399 module_source_file: Option<String>,
400 imported_enum_candidates: &[String],
401 imported_source_callable_names: &[String],
402 provenance: ModuleProvenance,
403) -> Result<ModuleArtifact, VmError> {
404 let options = match provenance {
405 ModuleProvenance::User => crate::CompilerOptions::from_env(),
406 ModuleProvenance::EmbeddedStdlib => crate::CompilerOptions::embedded_stdlib(),
407 ModuleProvenance::PrivilegedWire | ModuleProvenance::TrustedHostDispatch => {
408 crate::CompilerOptions::privileged_wire()
409 }
410 };
411 compile_module_artifact_with_options(
412 program,
413 module_source_file,
414 imported_enum_candidates,
415 imported_source_callable_names,
416 provenance,
417 options,
418 )
419}
420
421fn compile_module_artifact_with_options(
422 program: &[harn_parser::SNode],
423 module_source_file: Option<String>,
424 imported_enum_candidates: &[String],
425 imported_source_callable_names: &[String],
426 provenance: ModuleProvenance,
427 options: crate::CompilerOptions,
428) -> Result<ModuleArtifact, VmError> {
429 let namespace_demands = harn_parser::namespace_import_demands(program);
430 let imports: Vec<ModuleImportSpec> = program
431 .iter()
432 .filter_map(|node| match &node.node {
433 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
434 path: path.clone(),
435 binding: ModuleImportBinding::Wildcard,
436 is_pub: *is_pub,
437 }),
438 harn_parser::Node::SelectiveImport {
439 names,
440 path,
441 is_pub,
442 } => Some(ModuleImportSpec {
443 path: path.clone(),
444 binding: ModuleImportBinding::Selected(names.clone()),
445 is_pub: *is_pub,
446 }),
447 harn_parser::Node::NamespaceImport {
448 alias,
449 path,
450 is_pub,
451 } => Some(ModuleImportSpec {
452 path: path.clone(),
453 binding: ModuleImportBinding::Namespace {
454 alias: alias.clone(),
455 demand: namespace_demands
456 .get(alias)
457 .cloned()
458 .unwrap_or(harn_parser::NamespaceDemand::Whole),
459 },
460 is_pub: *is_pub,
461 }),
462 _ => None,
463 })
464 .collect();
465
466 if provenance == ModuleProvenance::PrivilegedWire {
467 validate_privileged_wire_surface(program, &imports)?;
468 }
469
470 let compiler = || crate::Compiler::with_options(options);
471
472 let init_nodes: Vec<harn_parser::SNode> = program
473 .iter()
474 .filter(|sn| {
475 let inner = match &sn.node {
476 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
477 _ => sn,
478 };
479 matches!(
480 &inner.node,
481 harn_parser::Node::LetBinding { .. }
482 | harn_parser::Node::ConstBinding { .. }
483 | harn_parser::Node::EnumDecl { is_pub: true, .. }
490 | harn_parser::Node::ToolDecl { .. }
491 | harn_parser::Node::SkillDecl { .. }
492 | harn_parser::Node::EvalPackDecl { .. }
493 )
494 })
495 .cloned()
496 .collect();
497 let init_chunk = if init_nodes.is_empty() {
498 None
499 } else {
500 let compiler = compiler();
501 Some(
502 compiler
503 .compile_module_init(
504 program,
505 &init_nodes,
506 imported_enum_candidates,
507 imported_source_callable_names,
508 )
509 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
510 .freeze_for_cache(),
511 )
512 };
513
514 let public_exports: BTreeMap<String, DefKind> = program
515 .iter()
516 .flat_map(public_declarations)
517 .map(|export| (export.name, export.kind))
518 .collect();
519 let public_value_names = public_exports
520 .iter()
521 .filter(|(_, kind)| {
522 matches!(
523 kind,
524 DefKind::Variable
525 | DefKind::Enum
526 | DefKind::Tool
527 | DefKind::Skill
528 | DefKind::EvalPack
529 )
530 })
531 .map(|(name, _)| name.clone())
532 .collect();
533 let public_type_names = public_exports
534 .iter()
535 .filter(|(_, kind)| !kind.has_runtime_value())
536 .map(|(name, _)| name.clone())
537 .collect();
538
539 let mut functions = BTreeMap::new();
540 for node in program {
541 let inner = match &node.node {
542 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
543 _ => node,
544 };
545 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
546 let constructor = compiler()
551 .compile_struct_constructor(name, fields)
552 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
553 functions.insert(name.clone(), constructor.freeze_for_cache());
554 continue;
555 }
556 if let harn_parser::Node::Pipeline {
557 name,
558 params,
559 body,
560 extends,
561 ..
562 } = &inner.node
563 {
564 let mut compiler = compiler();
565 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
566 compiler
567 .add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
568 let pipeline = compiler
569 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
570 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
571 functions.insert(name.clone(), pipeline.freeze_for_cache());
572 continue;
573 }
574 let harn_parser::Node::FnDecl {
575 name,
576 type_params,
577 params,
578 body,
579 ..
580 } = &inner.node
581 else {
582 continue;
583 };
584
585 let mut compiler = compiler();
586 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
587 compiler.add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
588 compiler.prepare_module_context(program);
589 let func_chunk = compiler
590 .compile_named_fn_body(name, type_params, params, body, module_source_file.clone())
591 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
592 functions.insert(name.clone(), func_chunk.freeze_for_cache());
593 }
594
595 let type_schema_init_chunks =
596 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
597 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
598 .into_iter()
599 .map(|chunk| chunk.freeze_for_cache())
600 .collect();
601
602 Ok(ModuleArtifact {
603 provenance,
604 imports,
605 type_schema_init_chunks,
606 init_chunk,
607 functions,
608 public_exports,
609 public_value_names,
610 public_type_names,
611 })
612}
613
614fn validate_privileged_wire_surface(
615 program: &[harn_parser::SNode],
616 imports: &[ModuleImportSpec],
617) -> Result<(), VmError> {
618 if imports.iter().any(|import| import.is_pub) {
619 return Err(VmError::Runtime(
620 "Privileged wire modules cannot re-export imports".to_string(),
621 ));
622 }
623 for export in program.iter().flat_map(public_declarations) {
624 if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
625 return Err(VmError::Runtime(format!(
626 "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
627 export.name, export.kind
628 )));
629 }
630 }
631 Ok(())
632}
633
634pub fn compile_module_artifact_from_source(
638 source_path: &Path,
639 source: &str,
640) -> Result<ModuleArtifact, VmError> {
641 let program = parse_module_source(source_path, source)?;
642 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
643 compile_module_artifact_with_imported_symbols(
644 &program,
645 Some(source_path.display().to_string()),
646 &imported_symbols.enum_candidates,
647 &imported_symbols.source_callable_names,
648 )
649}
650
651pub(crate) fn compile_embedded_stdlib_module_artifact_from_source(
656 source_path: &Path,
657 source: &str,
658) -> Result<ModuleArtifact, VmError> {
659 let program = parse_module_source(source_path, source)?;
660 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
661 compile_module_artifact_with_provenance(
662 &program,
663 Some(source_path.display().to_string()),
664 &imported_symbols.enum_candidates,
665 &imported_symbols.source_callable_names,
666 ModuleProvenance::EmbeddedStdlib,
667 )
668}
669
670#[cfg(test)]
678thread_local! {
679 pub(crate) static INTERFACE_RESOLUTIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
680}
681
682pub fn module_compilation_context_for_source(
686 source_path: &Path,
687 source: &str,
688) -> Result<ModuleCompilationContext, VmError> {
689 #[cfg(test)]
690 INTERFACE_RESOLUTIONS.with(|count| count.set(count.get() + 1));
691 let program = parse_module_source(source_path, source)?;
692 Ok(imported_symbol_projection_for_program(
693 source_path,
694 source,
695 &program,
696 ))
697}
698
699pub fn compile_privileged_wire_module_artifact_from_source(
708 source_path: &Path,
709 source: &str,
710) -> Result<ModuleArtifact, VmError> {
711 let program = parse_module_source(source_path, source)?;
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_symbols.enum_candidates,
717 &imported_symbols.source_callable_names,
718 ModuleProvenance::PrivilegedWire,
719 )
720}
721
722pub fn compile_trusted_host_dispatch_module_artifact_from_source(
727 source_path: &Path,
728 source: &str,
729) -> Result<ModuleArtifact, VmError> {
730 let program = parse_module_source(source_path, source)?;
731 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
732 compile_module_artifact_with_provenance(
733 &program,
734 Some(source_path.display().to_string()),
735 &imported_symbols.enum_candidates,
736 &imported_symbols.source_callable_names,
737 ModuleProvenance::TrustedHostDispatch,
738 )
739}
740
741pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_enums(
746 source_path: &Path,
747 source: &str,
748 imported_enum_candidates: impl IntoIterator<Item = String>,
749) -> Result<ModuleArtifact, VmError> {
750 let program = parse_module_source(source_path, source)?;
751 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
752 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
753 compile_module_artifact_with_provenance(
754 &program,
755 Some(source_path.display().to_string()),
756 &imported_enum_candidates,
757 &imported_symbols.source_callable_names,
758 ModuleProvenance::TrustedHostDispatch,
759 )
760}
761
762pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_symbols(
763 source_path: &Path,
764 source: &str,
765 imported_enum_candidates: impl IntoIterator<Item = String>,
766 imported_source_callable_names: impl IntoIterator<Item = String>,
767) -> Result<ModuleArtifact, VmError> {
768 let context =
769 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
770 compile_trusted_host_dispatch_module_artifact_from_source_with_context(
771 source_path,
772 source,
773 &context,
774 )
775}
776
777pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_context(
778 source_path: &Path,
779 source: &str,
780 context: &ModuleCompilationContext,
781) -> Result<ModuleArtifact, VmError> {
782 let program = parse_module_source(source_path, source)?;
783 compile_module_artifact_with_provenance(
784 &program,
785 Some(source_path.display().to_string()),
786 context.enum_candidates(),
787 context.source_callable_names(),
788 ModuleProvenance::TrustedHostDispatch,
789 )
790}
791
792fn imported_symbol_projection_for_program(
798 source_path: &Path,
799 source: &str,
800 program: &[harn_parser::SNode],
801) -> ModuleCompilationContext {
802 if !needs_imported_symbol_projection(program) {
803 return ModuleCompilationContext::default();
804 }
805 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
806 let cache_key = harn_modules::canonical_path(source_path);
807 let cacheable = is_immutable_stdlib_path(source_path);
808 if cacheable {
809 if let Some((_cached_hash, projection)) = imported_symbol_cache()
810 .lock()
811 .expect("imported symbol cache lock poisoned")
812 .get(&cache_key)
813 .filter(|(cached_hash, _)| *cached_hash == source_hash)
814 {
815 return projection.clone();
816 }
817 }
818
819 let graph = harn_modules::build_with_source(source_path, source);
823 if !cacheable {
824 return sorted_imported_symbol_projection(&graph, source_path);
825 }
826 let mut projections = Vec::new();
827 for path in graph.module_paths() {
828 let module_source = if path == cache_key {
829 Some(source.to_string())
830 } else {
831 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
832 };
833 let Some(module_source) = module_source else {
834 continue;
835 };
836 let projection = sorted_imported_symbol_projection(&graph, &path);
837 projections.push((
838 path,
839 (
840 *blake3::hash(module_source.as_bytes()).as_bytes(),
841 projection,
842 ),
843 ));
844 }
845 let mut cache = imported_symbol_cache()
846 .lock()
847 .expect("imported symbol cache lock poisoned");
848 for (path, projection) in projections {
849 if is_immutable_stdlib_path(&path) {
850 cache.insert(path, projection);
851 }
852 }
853 cache
854 .get(&cache_key)
855 .filter(|(cached_hash, _)| *cached_hash == source_hash)
856 .map(|(_, projection)| projection.clone())
857 .unwrap_or_default()
858}
859
860fn sorted_imported_symbol_projection(
861 graph: &harn_modules::ModuleGraph,
862 source_path: &Path,
863) -> ModuleCompilationContext {
864 ModuleCompilationContext::from_graph(graph, source_path)
865}
866
867fn is_immutable_stdlib_path(path: &Path) -> bool {
868 path.to_str()
869 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
870}
871
872fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
873 harn_parser::visit::contains_identifier_enum_pattern(program)
874}
875
876fn needs_imported_symbol_projection(program: &[harn_parser::SNode]) -> bool {
877 needs_imported_enum_candidates(program) || needs_imported_source_callable_names(program)
878}
879
880fn needs_imported_source_callable_names(program: &[harn_parser::SNode]) -> bool {
881 let has_wildcard_import = program.iter().any(|node| match &node.node {
882 harn_parser::Node::ImportDecl { .. } => true,
883 harn_parser::Node::AttributedDecl { inner, .. } => {
884 matches!(&inner.node, harn_parser::Node::ImportDecl { .. })
885 }
886 _ => false,
887 });
888 if !has_wildcard_import {
889 return false;
890 }
891 let mut needs_projection = false;
892 harn_parser::visit::walk_program(program, &mut |node| {
893 if let harn_parser::Node::FunctionCall { name, .. } = &node.node {
894 needs_projection |= harn_parser::builtin_signatures::is_builtin(name);
895 }
896 });
897 needs_projection
898}
899
900fn parse_module_source(
901 source_path: &Path,
902 source: &str,
903) -> Result<Vec<harn_parser::SNode>, VmError> {
904 let mut lexer = harn_lexer::Lexer::new(source);
905 let tokens = lexer.tokenize().map_err(|e| {
906 VmError::Runtime(format!(
907 "Import lex error in {}: {e}",
908 source_path.display()
909 ))
910 })?;
911 let mut parser = harn_parser::Parser::new(tokens);
912 parser.parse().map_err(|e| {
913 VmError::Runtime(format!(
914 "Import parse error in {}: {e}",
915 source_path.display()
916 ))
917 })
918}
919
920pub fn compile_module_artifact_from_source_with_imported_enums(
925 source_path: &Path,
926 source: &str,
927 imported_enum_candidates: impl IntoIterator<Item = String>,
928) -> Result<ModuleArtifact, VmError> {
929 let program = parse_module_source(source_path, source)?;
930 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
931 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
932 compile_module_artifact_with_imported_symbols(
933 &program,
934 Some(source_path.display().to_string()),
935 &imported_enum_candidates,
936 &imported_symbols.source_callable_names,
937 )
938}
939
940pub fn compile_module_artifact_from_source_with_imported_symbols(
944 source_path: &Path,
945 source: &str,
946 imported_enum_candidates: impl IntoIterator<Item = String>,
947 imported_source_callable_names: impl IntoIterator<Item = String>,
948) -> Result<ModuleArtifact, VmError> {
949 let context =
950 ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
951 compile_module_artifact_from_source_with_context(source_path, source, &context)
952}
953
954pub fn compile_module_artifact_from_source_with_context(
955 source_path: &Path,
956 source: &str,
957 context: &ModuleCompilationContext,
958) -> Result<ModuleArtifact, VmError> {
959 let program = parse_module_source(source_path, source)?;
960 compile_module_artifact_with_imported_symbols(
961 &program,
962 Some(source_path.display().to_string()),
963 context.enum_candidates(),
964 context.source_callable_names(),
965 )
966}
967
968pub(crate) fn compile_embedded_stdlib_module_artifact_from_source_with_context(
975 source_path: &Path,
976 source: &str,
977 context: &ModuleCompilationContext,
978) -> Result<ModuleArtifact, VmError> {
979 let program = parse_module_source(source_path, source)?;
980 compile_module_artifact_with_options(
981 &program,
982 Some(source_path.display().to_string()),
983 context.enum_candidates(),
984 context.source_callable_names(),
985 ModuleProvenance::EmbeddedStdlib,
986 crate::CompilerOptions::embedded_stdlib(),
987 )
988}
989
990#[cfg(test)]
991mod tests {
992 use std::path::Path;
993
994 use harn_lexer::Lexer;
995 use harn_parser::Parser;
996
997 use super::{
998 compile_embedded_stdlib_module_artifact_from_source_with_context, compile_module_artifact,
999 compile_module_artifact_from_source, compile_privileged_wire_module_artifact_from_source,
1000 needs_imported_enum_candidates, parse_module_source, ModuleCompilationContext,
1001 ModuleImportBinding, ModuleProvenance,
1002 };
1003 use crate::chunk::Constant;
1004
1005 #[test]
1006 fn module_init_schema_of_uses_full_program_aliases() {
1007 let source = r"
1008pub type Item = {id: string}
1009const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
1010";
1011 let mut lexer = Lexer::new(source);
1012 let tokens = lexer.tokenize().unwrap();
1013 let mut parser = Parser::new(tokens);
1014 let program = parser.parse().unwrap();
1015 let artifact = compile_module_artifact(&program, None).unwrap();
1016 let constants = &artifact.init_chunk.expect("init chunk").constants;
1017 let strings = constants
1018 .iter()
1019 .filter_map(|constant| match constant {
1020 Constant::String(value) => Some(value.as_str()),
1021 _ => None,
1022 })
1023 .collect::<Vec<_>>();
1024 assert!(strings.contains(&"id"), "{strings:?}");
1025 assert!(!strings.contains(&"Item"), "{strings:?}");
1026 }
1027
1028 #[test]
1029 fn type_only_modules_use_a_separate_schema_initializer() {
1030 let source = r"
1031pub type UserShape = {name: string, active?: bool}
1032pub type UserList = list<UserShape>
1033";
1034
1035 let artifact =
1036 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
1037 .expect("module compiles");
1038
1039 assert!(
1040 artifact.init_chunk.is_none(),
1041 "erased type aliases must not inflate module init bytecode"
1042 );
1043 assert!(artifact.public_type_names.contains("UserShape"));
1044 assert!(artifact.public_type_names.contains("UserList"));
1045 assert_eq!(artifact.type_schema_init_chunks.len(), 2);
1046 }
1047
1048 #[test]
1049 fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
1050 let source = r#"
1051pub enum KeptStatus { Ready }
1052pub enum DeadStatus { Gone }
1053pub struct KeptConfig { value: int }
1054pub struct DeadConfig { value: string }
1055pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
1056pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
1057"#;
1058 let source_path = Path::new("<test>/declarations.harn");
1059 let parsed = parse_module_source(source_path, source).expect("module parses");
1060 let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
1061 .expect("module compiles");
1062 let selected = super::specialize_module_artifact(
1063 &parsed,
1064 Some(source_path.display().to_string()),
1065 full,
1066 &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
1067 "KeptStatus".to_string(),
1068 "KeptConfig".to_string(),
1069 "kept_pipeline".to_string(),
1070 ])),
1071 )
1072 .expect("specialization succeeds");
1073
1074 assert!(selected.public_exports.contains_key("KeptStatus"));
1075 assert!(selected.public_exports.contains_key("KeptConfig"));
1076 assert!(selected.public_exports.contains_key("kept_pipeline"));
1077 assert!(!selected.public_exports.contains_key("DeadStatus"));
1078 assert!(!selected.public_exports.contains_key("DeadConfig"));
1079 assert!(!selected.public_exports.contains_key("dead_pipeline"));
1080 assert!(selected.functions.contains_key("KeptConfig"));
1081 assert!(selected.functions.contains_key("kept_pipeline"));
1082 assert!(!selected.functions.contains_key("DeadConfig"));
1083 assert!(!selected.functions.contains_key("dead_pipeline"));
1084 }
1085
1086 #[test]
1087 fn nested_namespace_import_retains_static_member_demand() {
1088 let artifact = compile_module_artifact_from_source(
1089 Path::new("<test>/wrapper.harn"),
1090 r#"
1091import * as lib from "./lib"
1092pub fn call() { return lib.greet() }
1093"#,
1094 )
1095 .expect("module compiles");
1096
1097 let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
1098 panic!("expected namespace import metadata");
1099 };
1100 assert_eq!(alias, "lib");
1101 assert_eq!(
1102 demand,
1103 &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
1104 "greet".to_string(),
1105 ]))
1106 );
1107 }
1108
1109 #[test]
1110 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
1111 let source = r#"fn probe() { return host_call("project.scan", {}) }"#;
1112 let error = compile_module_artifact_from_source(Path::new("<test>/user.harn"), source)
1113 .expect_err("a tail call must not acquire wire authority");
1114 assert!(
1115 error.to_string().contains("not callable source API"),
1116 "{error}"
1117 );
1118 }
1119
1120 #[test]
1121 fn embedded_stdlib_can_wrap_runtime_internal_builtins_without_exposing_them() {
1122 let source = r#"pub fn ansi_enabled() { return __ansi_enabled("stdout") }"#;
1123 let context = ModuleCompilationContext::default();
1124 compile_embedded_stdlib_module_artifact_from_source_with_context(
1125 Path::new("<stdlib>/ansi.harn"),
1126 source,
1127 &context,
1128 )
1129 .expect("embedded stdlib wrapper compiles");
1130
1131 let error = compile_module_artifact_from_source(Path::new("<test>/user.harn"), source)
1132 .expect_err("ordinary source must not acquire stdlib authority");
1133 assert!(
1134 error.to_string().contains("not callable source API"),
1135 "{error}"
1136 );
1137 }
1138
1139 #[test]
1140 fn explicit_privileged_compilation_stamps_private_wire_code() {
1141 let artifact = compile_privileged_wire_module_artifact_from_source(
1142 Path::new("<trusted>/wire.harn"),
1143 r#"fn probe() { host_call("project.scan", {}) }"#,
1144 )
1145 .expect("trusted private wire function compiles");
1146 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
1147 assert!(artifact.functions.contains_key("probe"));
1148 assert!(artifact.public_exports.is_empty());
1149 }
1150
1151 #[test]
1152 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
1153 let error = compile_privileged_wire_module_artifact_from_source(
1154 Path::new("<trusted>/wire.harn"),
1155 r#"pub fn probe() { host_call("project.scan", {}) }"#,
1156 )
1157 .expect_err("wire closures must not be exportable");
1158 assert!(
1159 error
1160 .to_string()
1161 .contains("only explicit capability-value bindings"),
1162 "{error}"
1163 );
1164 }
1165
1166 #[test]
1167 fn privileged_wire_modules_cannot_reexport_imports() {
1168 let error = compile_privileged_wire_module_artifact_from_source(
1169 Path::new("<trusted>/wire.harn"),
1170 r#"pub import { probe } from "./other""#,
1171 )
1172 .expect_err("wire authority must be non-reexportable");
1173 assert!(
1174 error.to_string().contains("cannot re-export imports"),
1175 "{error}"
1176 );
1177 }
1178
1179 #[test]
1180 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
1181 let source = r#"
1182import { External } from "./external"
1183pub type Wrapped = {value: External}
1184"#;
1185 let source_path = Path::new("<test>/wrapped.harn");
1186 let artifact =
1187 compile_module_artifact_from_source(source_path, source).expect("module compiles");
1188 let chunk = artifact
1189 .type_schema_init_chunks
1190 .into_iter()
1191 .next()
1192 .expect("schema initializer");
1193 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
1194 assert!(chunk
1195 .constants
1196 .iter()
1197 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
1198 }
1199
1200 #[test]
1201 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
1202 let plain = parse_module_source(
1203 Path::new("<test>/plain.harn"),
1204 r#"
1205import { helper } from "./support"
1206pub fn run() -> int { return helper(1) }
1207"#,
1208 )
1209 .expect("plain module parses");
1210 assert!(!needs_imported_enum_candidates(&plain));
1211
1212 let qualified = parse_module_source(
1213 Path::new("<test>/qualified.harn"),
1214 r#"
1215import { Status } from "./status"
1216pub fn run(value: Status) {
1217 match value {
1218 Status.Ready -> { return 1 }
1219 _ -> { return 0 }
1220 }
1221}
1222"#,
1223 )
1224 .expect("qualified module parses");
1225 assert!(needs_imported_enum_candidates(&qualified));
1226 }
1227
1228 #[test]
1229 fn private_declarations_do_not_expand_module_init() {
1230 let artifact = compile_module_artifact_from_source(
1231 Path::new("<test>/private-declarations.harn"),
1232 r"
1233enum PrivateStatus { Ready }
1234struct PrivateConfig { value: int }
1235pub fn run() { return PrivateStatus.Ready }
1236",
1237 )
1238 .expect("private declarations compile");
1239
1240 assert!(artifact.init_chunk.is_none());
1241 assert!(artifact.functions.contains_key("PrivateConfig"));
1242 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
1243 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
1244 }
1245}