1use std::collections::{BTreeMap, HashSet};
13use std::path::{Path, PathBuf};
14use std::sync::{Mutex, OnceLock};
15
16use harn_modules::{public_declarations, DefKind};
17use serde::{Deserialize, Serialize};
18
19use crate::chunk::{CachedChunk, CachedCompiledFunction};
20use crate::value::VmError;
21
22#[derive(Clone, Default)]
23struct ImportedSymbolProjection {
24 enum_candidates: Vec<String>,
25 source_callable_names: Vec<String>,
26}
27
28type ImportedSymbolCache = BTreeMap<PathBuf, ([u8; 32], ImportedSymbolProjection)>;
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37pub enum ModuleProvenance {
38 #[default]
39 User,
40 PrivilegedWire,
41 TrustedHostDispatch,
46}
47
48fn imported_symbol_cache() -> &'static Mutex<ImportedSymbolCache> {
49 static CACHE: OnceLock<Mutex<ImportedSymbolCache>> = OnceLock::new();
50 CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
57pub struct ModuleImportSpec {
58 pub path: String,
59 pub binding: ModuleImportBinding,
60 pub is_pub: bool,
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
65pub enum ModuleImportBinding {
66 Wildcard,
67 Selected(Vec<String>),
68 Namespace {
69 alias: String,
70 demand: harn_parser::NamespaceDemand,
71 },
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct ModuleArtifact {
81 #[serde(default)]
82 pub provenance: ModuleProvenance,
83 pub imports: Vec<ModuleImportSpec>,
84 pub type_schema_init_chunks: Vec<CachedChunk>,
87 pub init_chunk: Option<CachedChunk>,
88 pub functions: BTreeMap<String, CachedCompiledFunction>,
89 pub public_exports: BTreeMap<String, DefKind>,
94 pub public_value_names: HashSet<String>,
98 pub public_type_names: HashSet<String>,
103}
104
105pub fn specialize_module_artifact(
112 program: &[harn_parser::SNode],
113 source_file: Option<String>,
114 mut artifact: ModuleArtifact,
115 demand: &harn_modules::ExportDemand,
116) -> Result<ModuleArtifact, VmError> {
117 use harn_parser::Node;
118 use std::collections::{BTreeSet, HashMap};
119
120 if matches!(demand, harn_modules::ExportDemand::WholeNamespace) {
121 return Ok(artifact);
122 }
123
124 if artifact.imports.iter().any(|import| import.is_pub) {
129 return Ok(artifact);
130 }
131
132 let callable_names = artifact.functions.keys().cloned().collect::<HashSet<_>>();
133 let mut declarations = HashMap::<String, &harn_parser::SNode>::new();
134 for node in program {
135 let inner = match &node.node {
136 Node::AttributedDecl { inner, .. } => inner.as_ref(),
137 _ => node,
138 };
139 let name = match &inner.node {
140 Node::FnDecl { name, .. }
141 | Node::Pipeline { name, .. }
142 | Node::StructDecl { name, .. } => Some(name),
143 _ => None,
144 };
145 if let Some(name) = name {
146 declarations.insert(name.clone(), inner);
147 }
148 }
149
150 let mut pending = Vec::new();
151 if let harn_modules::ExportDemand::Members(members) = demand {
152 pending.extend(
153 members
154 .iter()
155 .filter(|name| callable_names.contains(*name))
156 .cloned(),
157 );
158 }
159 for node in program {
161 let inner = match &node.node {
162 Node::AttributedDecl { inner, .. } => inner.as_ref(),
163 _ => node,
164 };
165 if matches!(
166 &inner.node,
167 Node::LetBinding { .. }
168 | Node::ConstBinding { .. }
169 | Node::EnumDecl { is_pub: true, .. }
170 | Node::ToolDecl { .. }
171 | Node::SkillDecl { .. }
172 | Node::EvalPackDecl { .. }
173 ) {
174 collect_callable_references(inner, &callable_names, &mut pending);
175 }
176 }
177
178 let mut retained = HashSet::new();
179 while let Some(name) = pending.pop() {
180 if !retained.insert(name.clone()) {
181 continue;
182 }
183 if let Some(declaration) = declarations.get(&name) {
184 collect_callable_references(declaration, &callable_names, &mut pending);
185 }
186 }
187 artifact.functions.retain(|name, _| retained.contains(name));
188 artifact
189 .public_exports
190 .retain(|name, _| demand.contains(name));
191 artifact
192 .public_value_names
193 .retain(|name| demand.contains(name));
194 artifact
195 .public_type_names
196 .retain(|name| demand.contains(name));
197
198 let selected_type_names = artifact
199 .public_type_names
200 .iter()
201 .cloned()
202 .collect::<BTreeSet<_>>();
203 artifact.type_schema_init_chunks =
204 crate::Compiler::compile_selected_public_type_schema_initializers(
205 program,
206 source_file,
207 Some(&selected_type_names),
208 )
209 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
210 .into_iter()
211 .map(|chunk| chunk.freeze_for_cache())
212 .collect();
213 Ok(artifact)
214}
215
216fn collect_callable_references(
217 node: &harn_parser::SNode,
218 callable_names: &HashSet<String>,
219 out: &mut Vec<String>,
220) {
221 use harn_parser::Node;
222 let referenced = match &node.node {
223 Node::Identifier(name)
224 | Node::FunctionCall { name, .. }
225 | Node::StructConstruct {
226 struct_name: name, ..
227 }
228 | Node::EnumConstruct {
229 enum_name: name, ..
230 } => Some(name),
231 _ => None,
232 };
233 if let Some(name) = referenced.filter(|name| callable_names.contains(*name)) {
234 out.push(name.clone());
235 }
236 for child in harn_parser::visit::immediate_children(node) {
237 collect_callable_references(child, callable_names, out);
238 }
239}
240
241impl ModuleArtifact {
242 pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
249 let source_file = source_path.display().to_string();
250 for chunk in &mut self.type_schema_init_chunks {
251 bind_chunk_source_file(chunk, &source_file);
252 }
253 if let Some(chunk) = &mut self.init_chunk {
254 bind_chunk_source_file(chunk, &source_file);
255 }
256 for function in self.functions.values_mut() {
257 bind_chunk_source_file(&mut function.chunk, &source_file);
258 }
259 }
260}
261
262fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
263 chunk.source_file = Some(source_file.to_string());
264 for function in &mut chunk.functions {
265 bind_chunk_source_file(&mut function.chunk, source_file);
266 }
267}
268
269pub fn compile_module_artifact(
274 program: &[harn_parser::SNode],
275 module_source_file: Option<String>,
276) -> Result<ModuleArtifact, VmError> {
277 let imported_symbols = module_source_file
278 .as_deref()
279 .filter(|_| needs_imported_symbol_projection(program))
280 .map(|path| {
281 let graph = harn_modules::build(&[Path::new(path).to_path_buf()]);
282 sorted_imported_symbol_projection(&graph, Path::new(path))
283 })
284 .unwrap_or_default();
285 compile_module_artifact_with_imported_symbols(
286 program,
287 module_source_file,
288 &imported_symbols.enum_candidates,
289 &imported_symbols.source_callable_names,
290 )
291}
292
293fn compile_module_artifact_with_imported_symbols(
294 program: &[harn_parser::SNode],
295 module_source_file: Option<String>,
296 imported_enum_candidates: &[String],
297 imported_source_callable_names: &[String],
298) -> Result<ModuleArtifact, VmError> {
299 compile_module_artifact_with_provenance(
300 program,
301 module_source_file,
302 imported_enum_candidates,
303 imported_source_callable_names,
304 ModuleProvenance::User,
305 )
306}
307
308fn compile_module_artifact_with_provenance(
309 program: &[harn_parser::SNode],
310 module_source_file: Option<String>,
311 imported_enum_candidates: &[String],
312 imported_source_callable_names: &[String],
313 provenance: ModuleProvenance,
314) -> Result<ModuleArtifact, VmError> {
315 let namespace_demands = harn_parser::namespace_import_demands(program);
316 let imports: Vec<ModuleImportSpec> = program
317 .iter()
318 .filter_map(|node| match &node.node {
319 harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
320 path: path.clone(),
321 binding: ModuleImportBinding::Wildcard,
322 is_pub: *is_pub,
323 }),
324 harn_parser::Node::SelectiveImport {
325 names,
326 path,
327 is_pub,
328 } => Some(ModuleImportSpec {
329 path: path.clone(),
330 binding: ModuleImportBinding::Selected(names.clone()),
331 is_pub: *is_pub,
332 }),
333 harn_parser::Node::NamespaceImport {
334 alias,
335 path,
336 is_pub,
337 } => Some(ModuleImportSpec {
338 path: path.clone(),
339 binding: ModuleImportBinding::Namespace {
340 alias: alias.clone(),
341 demand: namespace_demands
342 .get(alias)
343 .cloned()
344 .unwrap_or(harn_parser::NamespaceDemand::Whole),
345 },
346 is_pub: *is_pub,
347 }),
348 _ => None,
349 })
350 .collect();
351
352 if provenance == ModuleProvenance::PrivilegedWire {
353 validate_privileged_wire_surface(program, &imports)?;
354 }
355
356 let compiler = || match provenance {
357 ModuleProvenance::User => crate::Compiler::new(),
358 ModuleProvenance::PrivilegedWire => {
359 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
360 }
361 ModuleProvenance::TrustedHostDispatch => {
362 crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
363 }
364 };
365
366 let init_nodes: Vec<harn_parser::SNode> = program
367 .iter()
368 .filter(|sn| {
369 let inner = match &sn.node {
370 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
371 _ => sn,
372 };
373 matches!(
374 &inner.node,
375 harn_parser::Node::LetBinding { .. }
376 | harn_parser::Node::ConstBinding { .. }
377 | harn_parser::Node::EnumDecl { is_pub: true, .. }
384 | harn_parser::Node::ToolDecl { .. }
385 | harn_parser::Node::SkillDecl { .. }
386 | harn_parser::Node::EvalPackDecl { .. }
387 )
388 })
389 .cloned()
390 .collect();
391 let init_chunk = if init_nodes.is_empty() {
392 None
393 } else {
394 let compiler = compiler();
395 Some(
396 compiler
397 .compile_module_init(
398 program,
399 &init_nodes,
400 imported_enum_candidates,
401 imported_source_callable_names,
402 )
403 .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
404 .freeze_for_cache(),
405 )
406 };
407
408 let public_exports: BTreeMap<String, DefKind> = program
409 .iter()
410 .flat_map(public_declarations)
411 .map(|export| (export.name, export.kind))
412 .collect();
413 let public_value_names = public_exports
414 .iter()
415 .filter(|(_, kind)| {
416 matches!(
417 kind,
418 DefKind::Variable
419 | DefKind::Enum
420 | DefKind::Tool
421 | DefKind::Skill
422 | DefKind::EvalPack
423 )
424 })
425 .map(|(name, _)| name.clone())
426 .collect();
427 let public_type_names = public_exports
428 .iter()
429 .filter(|(_, kind)| !kind.has_runtime_value())
430 .map(|(name, _)| name.clone())
431 .collect();
432
433 let mut functions = BTreeMap::new();
434 for node in program {
435 let inner = match &node.node {
436 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
437 _ => node,
438 };
439 if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
440 let constructor = compiler()
445 .compile_struct_constructor(name, fields)
446 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
447 functions.insert(name.clone(), constructor.freeze_for_cache());
448 continue;
449 }
450 if let harn_parser::Node::Pipeline {
451 name,
452 params,
453 body,
454 extends,
455 ..
456 } = &inner.node
457 {
458 let mut compiler = compiler();
459 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
460 compiler
461 .add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
462 let pipeline = compiler
463 .compile_pipeline_callable(program, name, params, body, extends.as_deref())
464 .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
465 functions.insert(name.clone(), pipeline.freeze_for_cache());
466 continue;
467 }
468 let harn_parser::Node::FnDecl {
469 name,
470 type_params,
471 params,
472 body,
473 ..
474 } = &inner.node
475 else {
476 continue;
477 };
478
479 let mut compiler = compiler();
480 compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
481 compiler.add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
482 compiler.prepare_module_context(program);
483 let func_chunk = compiler
484 .compile_fn_body(type_params, params, body, module_source_file.clone())
485 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
486 functions.insert(name.clone(), func_chunk.freeze_for_cache());
487 }
488
489 let type_schema_init_chunks =
490 crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
491 .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
492 .into_iter()
493 .map(|chunk| chunk.freeze_for_cache())
494 .collect();
495
496 Ok(ModuleArtifact {
497 provenance,
498 imports,
499 type_schema_init_chunks,
500 init_chunk,
501 functions,
502 public_exports,
503 public_value_names,
504 public_type_names,
505 })
506}
507
508fn validate_privileged_wire_surface(
509 program: &[harn_parser::SNode],
510 imports: &[ModuleImportSpec],
511) -> Result<(), VmError> {
512 if imports.iter().any(|import| import.is_pub) {
513 return Err(VmError::Runtime(
514 "Privileged wire modules cannot re-export imports".to_string(),
515 ));
516 }
517 for export in program.iter().flat_map(public_declarations) {
518 if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
519 return Err(VmError::Runtime(format!(
520 "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
521 export.name, export.kind
522 )));
523 }
524 }
525 Ok(())
526}
527
528pub fn compile_module_artifact_from_source(
532 source_path: &Path,
533 source: &str,
534) -> Result<ModuleArtifact, VmError> {
535 let program = parse_module_source(source_path, source)?;
536 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
537 compile_module_artifact_with_imported_symbols(
538 &program,
539 Some(source_path.display().to_string()),
540 &imported_symbols.enum_candidates,
541 &imported_symbols.source_callable_names,
542 )
543}
544
545pub fn compile_privileged_wire_module_artifact_from_source(
554 source_path: &Path,
555 source: &str,
556) -> Result<ModuleArtifact, VmError> {
557 let program = parse_module_source(source_path, source)?;
558 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
559 compile_module_artifact_with_provenance(
560 &program,
561 Some(source_path.display().to_string()),
562 &imported_symbols.enum_candidates,
563 &imported_symbols.source_callable_names,
564 ModuleProvenance::PrivilegedWire,
565 )
566}
567
568pub fn compile_trusted_host_dispatch_module_artifact_from_source(
573 source_path: &Path,
574 source: &str,
575) -> Result<ModuleArtifact, VmError> {
576 let program = parse_module_source(source_path, source)?;
577 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
578 compile_module_artifact_with_provenance(
579 &program,
580 Some(source_path.display().to_string()),
581 &imported_symbols.enum_candidates,
582 &imported_symbols.source_callable_names,
583 ModuleProvenance::TrustedHostDispatch,
584 )
585}
586
587pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_enums(
592 source_path: &Path,
593 source: &str,
594 imported_enum_candidates: impl IntoIterator<Item = String>,
595) -> Result<ModuleArtifact, VmError> {
596 let program = parse_module_source(source_path, source)?;
597 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
598 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
599 compile_module_artifact_with_provenance(
600 &program,
601 Some(source_path.display().to_string()),
602 &imported_enum_candidates,
603 &imported_symbols.source_callable_names,
604 ModuleProvenance::TrustedHostDispatch,
605 )
606}
607
608pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_symbols(
609 source_path: &Path,
610 source: &str,
611 imported_enum_candidates: impl IntoIterator<Item = String>,
612 imported_source_callable_names: impl IntoIterator<Item = String>,
613) -> Result<ModuleArtifact, VmError> {
614 let program = parse_module_source(source_path, source)?;
615 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
616 let imported_source_callable_names = imported_source_callable_names
617 .into_iter()
618 .collect::<Vec<_>>();
619 compile_module_artifact_with_provenance(
620 &program,
621 Some(source_path.display().to_string()),
622 &imported_enum_candidates,
623 &imported_source_callable_names,
624 ModuleProvenance::TrustedHostDispatch,
625 )
626}
627
628fn imported_symbol_projection_for_program(
634 source_path: &Path,
635 source: &str,
636 program: &[harn_parser::SNode],
637) -> ImportedSymbolProjection {
638 if !needs_imported_symbol_projection(program) {
639 return ImportedSymbolProjection::default();
640 }
641 let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
642 let cache_key = harn_modules::canonical_path(source_path);
643 let cacheable = is_immutable_stdlib_path(source_path);
644 if cacheable {
645 if let Some((_cached_hash, projection)) = imported_symbol_cache()
646 .lock()
647 .expect("imported symbol cache lock poisoned")
648 .get(&cache_key)
649 .filter(|(cached_hash, _)| *cached_hash == source_hash)
650 {
651 return projection.clone();
652 }
653 }
654
655 let graph = harn_modules::build_with_source(source_path, source);
659 if !cacheable {
660 return sorted_imported_symbol_projection(&graph, source_path);
661 }
662 let mut projections = Vec::new();
663 for path in graph.module_paths() {
664 let module_source = if path == cache_key {
665 Some(source.to_string())
666 } else {
667 harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
668 };
669 let Some(module_source) = module_source else {
670 continue;
671 };
672 let projection = sorted_imported_symbol_projection(&graph, &path);
673 projections.push((
674 path,
675 (
676 *blake3::hash(module_source.as_bytes()).as_bytes(),
677 projection,
678 ),
679 ));
680 }
681 let mut cache = imported_symbol_cache()
682 .lock()
683 .expect("imported symbol cache lock poisoned");
684 for (path, projection) in projections {
685 if is_immutable_stdlib_path(&path) {
686 cache.insert(path, projection);
687 }
688 }
689 cache
690 .get(&cache_key)
691 .filter(|(cached_hash, _)| *cached_hash == source_hash)
692 .map(|(_, projection)| projection.clone())
693 .unwrap_or_default()
694}
695
696fn sorted_imported_symbol_projection(
697 graph: &harn_modules::ModuleGraph,
698 source_path: &Path,
699) -> ImportedSymbolProjection {
700 let mut enum_candidates = graph
701 .imported_names_by_kind_for_file(source_path, DefKind::Enum)
702 .unwrap_or_default()
703 .into_iter()
704 .collect::<Vec<_>>();
705 enum_candidates.sort_unstable();
706 let mut source_callable_names = graph
707 .imported_callable_names_for_file(source_path)
708 .unwrap_or_default()
709 .into_iter()
710 .collect::<Vec<_>>();
711 source_callable_names.sort_unstable();
712 ImportedSymbolProjection {
713 enum_candidates,
714 source_callable_names,
715 }
716}
717
718fn is_immutable_stdlib_path(path: &Path) -> bool {
719 path.to_str()
720 .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
721}
722
723fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
724 harn_parser::visit::contains_identifier_enum_pattern(program)
725}
726
727fn needs_imported_symbol_projection(program: &[harn_parser::SNode]) -> bool {
728 needs_imported_enum_candidates(program) || needs_imported_source_callable_names(program)
729}
730
731fn needs_imported_source_callable_names(program: &[harn_parser::SNode]) -> bool {
732 let has_wildcard_import = program.iter().any(|node| match &node.node {
733 harn_parser::Node::ImportDecl { .. } => true,
734 harn_parser::Node::AttributedDecl { inner, .. } => {
735 matches!(&inner.node, harn_parser::Node::ImportDecl { .. })
736 }
737 _ => false,
738 });
739 if !has_wildcard_import {
740 return false;
741 }
742 let mut needs_projection = false;
743 harn_parser::visit::walk_program(program, &mut |node| {
744 if let harn_parser::Node::FunctionCall { name, .. } = &node.node {
745 needs_projection |= harn_parser::builtin_signatures::is_builtin(name);
746 }
747 });
748 needs_projection
749}
750
751fn parse_module_source(
752 source_path: &Path,
753 source: &str,
754) -> Result<Vec<harn_parser::SNode>, VmError> {
755 let mut lexer = harn_lexer::Lexer::new(source);
756 let tokens = lexer.tokenize().map_err(|e| {
757 VmError::Runtime(format!(
758 "Import lex error in {}: {e}",
759 source_path.display()
760 ))
761 })?;
762 let mut parser = harn_parser::Parser::new(tokens);
763 parser.parse().map_err(|e| {
764 VmError::Runtime(format!(
765 "Import parse error in {}: {e}",
766 source_path.display()
767 ))
768 })
769}
770
771pub fn compile_module_artifact_from_source_with_imported_enums(
776 source_path: &Path,
777 source: &str,
778 imported_enum_candidates: impl IntoIterator<Item = String>,
779) -> Result<ModuleArtifact, VmError> {
780 let program = parse_module_source(source_path, source)?;
781 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
782 let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
783 compile_module_artifact_with_imported_symbols(
784 &program,
785 Some(source_path.display().to_string()),
786 &imported_enum_candidates,
787 &imported_symbols.source_callable_names,
788 )
789}
790
791pub fn compile_module_artifact_from_source_with_imported_symbols(
795 source_path: &Path,
796 source: &str,
797 imported_enum_candidates: impl IntoIterator<Item = String>,
798 imported_source_callable_names: impl IntoIterator<Item = String>,
799) -> Result<ModuleArtifact, VmError> {
800 let program = parse_module_source(source_path, source)?;
801 let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
802 let imported_source_callable_names = imported_source_callable_names
803 .into_iter()
804 .collect::<Vec<_>>();
805 compile_module_artifact_with_imported_symbols(
806 &program,
807 Some(source_path.display().to_string()),
808 &imported_enum_candidates,
809 &imported_source_callable_names,
810 )
811}
812
813#[cfg(test)]
814mod tests {
815 use std::path::Path;
816
817 use harn_lexer::Lexer;
818 use harn_parser::Parser;
819
820 use super::{
821 compile_module_artifact, compile_module_artifact_from_source,
822 compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
823 parse_module_source, ModuleImportBinding, ModuleProvenance,
824 };
825 use crate::chunk::Constant;
826
827 #[test]
828 fn module_init_schema_of_uses_full_program_aliases() {
829 let source = r"
830pub type Item = {id: string}
831const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
832";
833 let mut lexer = Lexer::new(source);
834 let tokens = lexer.tokenize().unwrap();
835 let mut parser = Parser::new(tokens);
836 let program = parser.parse().unwrap();
837 let artifact = compile_module_artifact(&program, None).unwrap();
838 let constants = &artifact.init_chunk.expect("init chunk").constants;
839 let strings = constants
840 .iter()
841 .filter_map(|constant| match constant {
842 Constant::String(value) => Some(value.as_str()),
843 _ => None,
844 })
845 .collect::<Vec<_>>();
846 assert!(strings.contains(&"id"), "{strings:?}");
847 assert!(!strings.contains(&"Item"), "{strings:?}");
848 }
849
850 #[test]
851 fn type_only_modules_use_a_separate_schema_initializer() {
852 let source = r"
853pub type UserShape = {name: string, active?: bool}
854pub type UserList = list<UserShape>
855";
856
857 let artifact =
858 compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
859 .expect("module compiles");
860
861 assert!(
862 artifact.init_chunk.is_none(),
863 "erased type aliases must not inflate module init bytecode"
864 );
865 assert!(artifact.public_type_names.contains("UserShape"));
866 assert!(artifact.public_type_names.contains("UserList"));
867 assert_eq!(artifact.type_schema_init_chunks.len(), 2);
868 }
869
870 #[test]
871 fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
872 let source = r#"
873pub enum KeptStatus { Ready }
874pub enum DeadStatus { Gone }
875pub struct KeptConfig { value: int }
876pub struct DeadConfig { value: string }
877pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
878pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
879"#;
880 let source_path = Path::new("<test>/declarations.harn");
881 let parsed = parse_module_source(source_path, source).expect("module parses");
882 let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
883 .expect("module compiles");
884 let selected = super::specialize_module_artifact(
885 &parsed,
886 Some(source_path.display().to_string()),
887 full,
888 &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
889 "KeptStatus".to_string(),
890 "KeptConfig".to_string(),
891 "kept_pipeline".to_string(),
892 ])),
893 )
894 .expect("specialization succeeds");
895
896 assert!(selected.public_exports.contains_key("KeptStatus"));
897 assert!(selected.public_exports.contains_key("KeptConfig"));
898 assert!(selected.public_exports.contains_key("kept_pipeline"));
899 assert!(!selected.public_exports.contains_key("DeadStatus"));
900 assert!(!selected.public_exports.contains_key("DeadConfig"));
901 assert!(!selected.public_exports.contains_key("dead_pipeline"));
902 assert!(selected.functions.contains_key("KeptConfig"));
903 assert!(selected.functions.contains_key("kept_pipeline"));
904 assert!(!selected.functions.contains_key("DeadConfig"));
905 assert!(!selected.functions.contains_key("dead_pipeline"));
906 }
907
908 #[test]
909 fn nested_namespace_import_retains_static_member_demand() {
910 let artifact = compile_module_artifact_from_source(
911 Path::new("<test>/wrapper.harn"),
912 r#"
913import * as lib from "./lib"
914pub fn call() { return lib.greet() }
915"#,
916 )
917 .expect("module compiles");
918
919 let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
920 panic!("expected namespace import metadata");
921 };
922 assert_eq!(alias, "lib");
923 assert_eq!(
924 demand,
925 &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
926 "greet".to_string(),
927 ]))
928 );
929 }
930
931 #[test]
932 fn ordinary_modules_cannot_name_privileged_wire_builtins() {
933 let error = compile_module_artifact_from_source(
934 Path::new("<test>/user.harn"),
935 r#"fn probe() { host_call("project.scan", {}) }"#,
936 )
937 .expect_err("ordinary source must not acquire wire authority");
938 assert!(
939 error.to_string().contains("not callable source API"),
940 "{error}"
941 );
942 }
943
944 #[test]
945 fn explicit_privileged_compilation_stamps_private_wire_code() {
946 let artifact = compile_privileged_wire_module_artifact_from_source(
947 Path::new("<trusted>/wire.harn"),
948 r#"fn probe() { host_call("project.scan", {}) }"#,
949 )
950 .expect("trusted private wire function compiles");
951 assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
952 assert!(artifact.functions.contains_key("probe"));
953 assert!(artifact.public_exports.is_empty());
954 }
955
956 #[test]
957 fn privileged_wire_functions_cannot_cross_the_module_boundary() {
958 let error = compile_privileged_wire_module_artifact_from_source(
959 Path::new("<trusted>/wire.harn"),
960 r#"pub fn probe() { host_call("project.scan", {}) }"#,
961 )
962 .expect_err("wire closures must not be exportable");
963 assert!(
964 error
965 .to_string()
966 .contains("only explicit capability-value bindings"),
967 "{error}"
968 );
969 }
970
971 #[test]
972 fn privileged_wire_modules_cannot_reexport_imports() {
973 let error = compile_privileged_wire_module_artifact_from_source(
974 Path::new("<trusted>/wire.harn"),
975 r#"pub import { probe } from "./other""#,
976 )
977 .expect_err("wire authority must be non-reexportable");
978 assert!(
979 error.to_string().contains("cannot re-export imports"),
980 "{error}"
981 );
982 }
983
984 #[test]
985 fn schema_initializer_keeps_imported_alias_lookup_and_source() {
986 let source = r#"
987import { External } from "./external"
988pub type Wrapped = {value: External}
989"#;
990 let source_path = Path::new("<test>/wrapped.harn");
991 let artifact =
992 compile_module_artifact_from_source(source_path, source).expect("module compiles");
993 let chunk = artifact
994 .type_schema_init_chunks
995 .into_iter()
996 .next()
997 .expect("schema initializer");
998 assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
999 assert!(chunk
1000 .constants
1001 .iter()
1002 .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
1003 }
1004
1005 #[test]
1006 fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
1007 let plain = parse_module_source(
1008 Path::new("<test>/plain.harn"),
1009 r#"
1010import { helper } from "./support"
1011pub fn run() -> int { return helper(1) }
1012"#,
1013 )
1014 .expect("plain module parses");
1015 assert!(!needs_imported_enum_candidates(&plain));
1016
1017 let qualified = parse_module_source(
1018 Path::new("<test>/qualified.harn"),
1019 r#"
1020import { Status } from "./status"
1021pub fn run(value: Status) {
1022 match value {
1023 Status.Ready -> { return 1 }
1024 _ -> { return 0 }
1025 }
1026}
1027"#,
1028 )
1029 .expect("qualified module parses");
1030 assert!(needs_imported_enum_candidates(&qualified));
1031 }
1032
1033 #[test]
1034 fn private_declarations_do_not_expand_module_init() {
1035 let artifact = compile_module_artifact_from_source(
1036 Path::new("<test>/private-declarations.harn"),
1037 r"
1038enum PrivateStatus { Ready }
1039struct PrivateConfig { value: int }
1040pub fn run() { return PrivateStatus.Ready }
1041",
1042 )
1043 .expect("private declarations compile");
1044
1045 assert!(artifact.init_chunk.is_none());
1046 assert!(artifact.functions.contains_key("PrivateConfig"));
1047 assert!(!artifact.public_exports.contains_key("PrivateStatus"));
1048 assert!(!artifact.public_exports.contains_key("PrivateConfig"));
1049 }
1050}