1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 Chunk, CompiledFunction, Compiler, CompilerOptions, PortableExportKind, PortableImport,
8 PortableSourcePackage,
9};
10
11use self::wire::{encode_wire_program, ArtifactReader, WireProgram};
12
13mod validation;
14mod wire;
15
16const MAGIC: &[u8; 8] = b"HARNPK01";
17pub const ARTIFACT_VERSION: u16 = 5;
18const HEADER_BYTES: usize = 8 + 2 + 2 + 4 + 32;
20const SEMANTIC_ABI_DOMAIN: &[u8] = b"harn-portable-kernel-semantic-abi-v4\0";
21
22pub fn semantic_abi_fingerprint_hex() -> String {
25 validation::semantic_abi_fingerprint()
26 .iter()
27 .map(|byte| format!("{byte:02x}"))
28 .collect()
29}
30
31#[derive(Debug, Clone, Copy)]
32pub struct ArtifactLimits {
33 pub max_bytes: usize,
34 pub max_chunks: usize,
35 pub max_functions: usize,
36 pub max_instructions: usize,
37 pub max_constants: usize,
38 pub max_string_bytes: usize,
39 pub max_metadata_entries: usize,
40 pub max_type_nodes: usize,
41 pub max_type_depth: usize,
42}
43
44impl Default for ArtifactLimits {
45 fn default() -> Self {
46 Self {
47 max_bytes: 8 * 1024 * 1024,
48 max_chunks: 16_384,
49 max_functions: 16_384,
50 max_instructions: 4 * 1024 * 1024,
51 max_constants: 1_048_576,
52 max_string_bytes: 4 * 1024 * 1024,
53 max_metadata_entries: 1_048_576,
54 max_type_nodes: 262_144,
55 max_type_depth: 128,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub struct Diagnostic {
63 pub code: String,
64 pub message: String,
65 pub line: Option<u32>,
66 pub column: Option<u32>,
67}
68
69impl Diagnostic {
70 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
71 Self {
72 code: code.into(),
73 message: message.into(),
74 line: None,
75 column: None,
76 }
77 }
78
79 fn artifact(code: &str, message: impl Into<String>) -> Self {
80 Self::new(code, message)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum EntryKind {
87 Function,
88 Pipeline,
89}
90
91impl std::str::FromStr for EntryKind {
92 type Err = Diagnostic;
93
94 fn from_str(name: &str) -> Result<Self, Self::Err> {
95 match name {
96 "function" => Ok(Self::Function),
97 "pipeline" => Ok(Self::Pipeline),
98 _ => Err(Diagnostic::new(
99 "entry_kind",
100 format!("entry kind `{name}` is invalid; use `function` or `pipeline`"),
101 )),
102 }
103 }
104}
105
106impl EntryKind {
107 pub const fn name(&self) -> &'static str {
108 match self {
109 Self::Function => "function",
110 Self::Pipeline => "pipeline",
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
116pub struct ProgramArtifact {
117 bytes: Arc<[u8]>,
118 digest: [u8; 32],
119 image: Arc<Chunk>,
120 entry: String,
121 entry_kind: EntryKind,
122 expects_harness: bool,
123 root_imports: Arc<[PortableImport]>,
124 modules: Arc<[ProgramModule]>,
125}
126
127#[derive(Debug, Clone)]
133pub struct ProgramModule {
134 id: String,
135 imports: Arc<[PortableImport]>,
136 init: Option<Arc<Chunk>>,
137 functions: Arc<BTreeMap<String, Arc<CompiledFunction>>>,
138 exports: Arc<BTreeMap<String, PortableExportKind>>,
139}
140
141impl ProgramModule {
142 pub fn id(&self) -> &str {
143 &self.id
144 }
145
146 pub fn imports(&self) -> &[PortableImport] {
147 &self.imports
148 }
149
150 pub(crate) fn init(&self) -> Option<&Arc<Chunk>> {
151 self.init.as_ref()
152 }
153
154 pub(crate) fn functions(&self) -> &BTreeMap<String, Arc<CompiledFunction>> {
155 &self.functions
156 }
157
158 pub(crate) fn exports(&self) -> &BTreeMap<String, PortableExportKind> {
159 &self.exports
160 }
161}
162
163impl ProgramArtifact {
164 pub fn bytes(&self) -> &[u8] {
165 &self.bytes
166 }
167 pub fn digest(&self) -> [u8; 32] {
168 self.digest
169 }
170 pub fn digest_hex(&self) -> String {
171 self.digest
172 .iter()
173 .map(|byte| format!("{byte:02x}"))
174 .collect()
175 }
176 pub fn image(&self) -> &Arc<Chunk> {
177 &self.image
178 }
179 pub fn entry(&self) -> &str {
180 &self.entry
181 }
182 pub fn entry_kind(&self) -> EntryKind {
183 self.entry_kind.clone()
184 }
185 pub fn expects_harness(&self) -> bool {
186 self.expects_harness
187 }
188
189 pub fn root_imports(&self) -> &[PortableImport] {
191 &self.root_imports
192 }
193
194 pub fn modules(&self) -> &[ProgramModule] {
196 &self.modules
197 }
198
199 pub fn decode(bytes: &[u8], limits: ArtifactLimits) -> Result<Self, Diagnostic> {
200 if bytes.len() > limits.max_bytes {
201 return Err(Diagnostic::artifact(
202 "artifact_too_large",
203 format!(
204 "artifact has {} bytes; limit is {}",
205 bytes.len(),
206 limits.max_bytes
207 ),
208 ));
209 }
210 if bytes.len() < HEADER_BYTES {
211 return Err(Diagnostic::artifact(
212 "artifact_truncated",
213 "artifact header is truncated",
214 ));
215 }
216 if &bytes[..8] != MAGIC {
217 return Err(Diagnostic::artifact(
218 "artifact_magic",
219 "artifact magic does not identify a portable Harn program",
220 ));
221 }
222 let version = u16::from_be_bytes([bytes[8], bytes[9]]);
223 if version != ARTIFACT_VERSION {
224 return Err(Diagnostic::artifact(
225 "artifact_version",
226 format!("artifact version {version} is not supported; expected {ARTIFACT_VERSION}"),
227 ));
228 }
229 let flags = u16::from_be_bytes([bytes[10], bytes[11]]);
230 if flags != 0 {
231 return Err(Diagnostic::artifact(
232 "artifact_features",
233 format!("artifact uses unsupported feature bits 0x{flags:04x}"),
234 ));
235 }
236 let payload_len =
237 u32::from_be_bytes(bytes[12..16].try_into().expect("header length checked")) as usize;
238 let total = HEADER_BYTES.checked_add(payload_len).ok_or_else(|| {
239 Diagnostic::artifact("artifact_too_large", "artifact length overflow")
240 })?;
241 if total != bytes.len() {
242 return Err(Diagnostic::artifact(
243 if total > bytes.len() {
244 "artifact_truncated"
245 } else {
246 "artifact_trailing_bytes"
247 },
248 format!(
249 "header declares {payload_len} payload bytes but {} are present",
250 bytes.len() - HEADER_BYTES
251 ),
252 ));
253 }
254 let expected_digest: [u8; 32] = bytes[16..48].try_into().expect("header length checked");
255 let payload = &bytes[HEADER_BYTES..];
256 let digest = *blake3::hash(payload).as_bytes();
257 if digest != expected_digest {
258 return Err(Diagnostic::artifact(
259 "artifact_corrupt",
260 "artifact payload digest does not match its header",
261 ));
262 }
263 let wire = ArtifactReader::new(payload, limits).read_program()?;
264 let built = wire.validate_and_build(limits)?;
265 Ok(Self {
266 bytes: Arc::from(bytes),
267 digest,
268 image: Arc::new(built.root),
269 entry: wire.entry,
270 entry_kind: wire.entry_kind,
271 expects_harness: wire.expects_harness,
272 root_imports: built.root_imports.into(),
273 modules: built
274 .modules
275 .into_iter()
276 .map(ProgramModule::from_built)
277 .collect::<Vec<_>>()
278 .into(),
279 })
280 }
281}
282
283impl ProgramModule {
284 fn from_built(module: wire::BuiltModule) -> Self {
285 Self {
286 id: module.id,
287 imports: module.imports.into(),
288 init: module.init.map(Arc::new),
289 functions: Arc::new(module.functions),
290 exports: Arc::new(module.exports),
291 }
292 }
293}
294
295#[derive(Debug, Clone)]
302pub struct PortableModuleSource {
303 pub id: String,
304 pub program: Vec<harn_parser::SNode>,
305 pub imports: Vec<PortableImport>,
306 pub exports: BTreeMap<String, PortableExportKind>,
307 pub imported_enum_candidates: Vec<String>,
308 pub source_file: Option<String>,
309}
310
311#[derive(Debug, Clone)]
312pub struct PortablePackageSource {
313 pub root_program: Vec<harn_parser::SNode>,
314 pub root_imports: Vec<PortableImport>,
315 pub modules: Vec<PortableModuleSource>,
316}
317
318pub fn compile_source_package(
323 package: PortableSourcePackage,
324 entry: &str,
325 entry_kind: EntryKind,
326) -> Result<ProgramArtifact, Vec<Diagnostic>> {
327 crate::portable_builtin::install_source_contracts();
328 let root_program = parse_source_module(&package.root_source, None)?;
329 let mut modules = Vec::with_capacity(package.modules.len());
330 for module in package.modules {
331 let source_file = module.source_file;
332 let program = parse_source_module(&module.source, source_file.as_deref())?;
333 modules.push(PortableModuleSource {
334 id: module.id,
335 program,
336 imports: module.imports,
337 exports: module.exports,
338 imported_enum_candidates: module.imported_enum_candidates,
339 source_file,
340 });
341 }
342 compile_program_package(
343 PortablePackageSource {
344 root_program,
345 root_imports: package.root_imports,
346 modules,
347 },
348 entry,
349 entry_kind,
350 )
351}
352
353fn parse_source_module(
354 source: &str,
355 source_file: Option<&str>,
356) -> Result<Vec<harn_parser::SNode>, Vec<Diagnostic>> {
357 let mut lexer = harn_lexer::Lexer::new(source);
358 let tokens = lexer.tokenize().map_err(|error| {
359 vec![Diagnostic {
360 code: "compile_frontend".to_string(),
361 message: format!("{}: {error}", source_file.unwrap_or("portable source")),
362 line: None,
363 column: None,
364 }]
365 })?;
366 let mut parser = harn_parser::Parser::new(tokens);
367 parser.parse().map_err(|error| {
368 vec![Diagnostic {
369 code: "compile_frontend".to_string(),
370 message: format!("{}: {error}", source_file.unwrap_or("portable source")),
371 line: None,
372 column: None,
373 }]
374 })
375}
376
377pub fn compile_program_package(
382 package: PortablePackageSource,
383 entry: &str,
384 entry_kind: EntryKind,
385) -> Result<ProgramArtifact, Vec<Diagnostic>> {
386 crate::portable_builtin::install_source_contracts();
387 let frontend_diagnostics = package_typecheck_diagnostics(&package);
388 if !frontend_diagnostics.is_empty() {
389 return Err(frontend_diagnostics);
390 }
391 let options = CompilerOptions::portable_artifact();
392 let compiled = match entry_kind {
393 EntryKind::Function => Compiler::with_options(options)
394 .compile_named_function_entry(&package.root_program, entry),
395 EntryKind::Pipeline => Compiler::with_options(options).compile_named_pipeline_entry(
396 &package.root_program,
397 entry,
398 None,
399 ),
400 }
401 .map_err(|error| {
402 vec![Diagnostic {
403 code: "compile_bytecode".to_string(),
404 message: error.message,
405 line: Some(error.line),
406 column: None,
407 }]
408 })?;
409 let mut compiled_modules = Vec::with_capacity(package.modules.len());
410 for module in package.modules {
411 let image = Compiler::with_options(options)
412 .compile_portable_module(
413 module.id,
414 &module.program,
415 module.imports,
416 module.exports,
417 &module.imported_enum_candidates,
418 module.source_file,
419 )
420 .map_err(|error| {
421 vec![Diagnostic {
422 code: "compile_bytecode".to_string(),
423 message: error.message,
424 line: Some(error.line),
425 column: None,
426 }]
427 })?;
428 compiled_modules.push(image);
429 }
430 let wire = WireProgram::from_package(
431 &compiled.bootstrap,
432 &compiled_modules,
433 package.root_imports,
434 entry.to_string(),
435 entry_kind,
436 compiled.expects_harness,
437 )
438 .map_err(|diagnostic| vec![diagnostic])?;
439 wire.validate_metadata(ArtifactLimits::default())
440 .map_err(|error| vec![error])?;
441 let payload = encode_wire_program(&wire).map_err(|error| vec![error])?;
442 encode_artifact_payload(payload)
443}
444
445fn package_typecheck_diagnostics(package: &PortablePackageSource) -> Vec<Diagnostic> {
452 let mut module_ids = HashSet::with_capacity(package.modules.len());
453 for module in &package.modules {
454 if module.id.is_empty() || !module_ids.insert(module.id.as_str()) {
455 return vec![Diagnostic::artifact(
456 "artifact_invalid_module",
457 "package contains an empty or duplicate module id",
458 )];
459 }
460 }
461 if let Err(diagnostic) =
462 wire::validate_import_targets(&package.root_imports, &module_ids, "root")
463 {
464 return vec![diagnostic];
465 }
466 for module in &package.modules {
467 if let Err(diagnostic) =
468 wire::validate_import_targets(&module.imports, &module_ids, &module.id)
469 {
470 return vec![diagnostic];
471 }
472 }
473 let modules = package
474 .modules
475 .iter()
476 .map(|module| (module.id.as_str(), module))
477 .collect::<BTreeMap<_, _>>();
478 let mut diagnostics = Vec::new();
479 typecheck_package_module(
480 "root",
481 &package.root_program,
482 &package.root_imports,
483 &modules,
484 &mut diagnostics,
485 );
486 diagnostics
487}
488
489fn typecheck_package_module(
490 owner: &str,
491 program: &[harn_parser::SNode],
492 imports: &[PortableImport],
493 modules: &BTreeMap<&str, &PortableModuleSource>,
494 diagnostics: &mut Vec<Diagnostic>,
495) {
496 let mut imported_names = HashSet::new();
497 let mut imported_declarations = Vec::new();
498 let mut imported_declaration_names = HashSet::new();
499 let mut namespace_imports = Vec::new();
500 for import in imports {
501 let Some(target) = modules.get(import.target.as_str()).copied() else {
502 continue;
505 };
506 if let Some(alias) = &import.namespace_alias {
507 imported_names.insert(alias.clone());
508 let signatures = target
509 .exports
510 .keys()
511 .filter_map(|name| {
512 let (origin, declaration) =
513 resolve_export_declaration(target, name, modules, &mut BTreeSet::new())?;
514 let signature = harn_parser::namespace_signatures::lower_callable_signature(
515 &declaration,
516 |type_name| {
517 resolve_export_declaration(
518 origin,
519 type_name,
520 modules,
521 &mut BTreeSet::new(),
522 )
523 .map(|(_, declaration)| declaration)
524 .or_else(|| {
525 origin
526 .program
527 .iter()
528 .find(|node| {
529 is_type_declaration(node)
530 && declaration_name(node) == Some(type_name)
531 })
532 .cloned()
533 })
534 },
535 )?;
536 Some((name.clone(), signature))
537 })
538 .collect();
539 namespace_imports.push((
540 alias.clone(),
541 harn_parser::NamespaceImportBinding::from_member_signatures(
542 import.path.clone(),
543 target.exports.keys().cloned().collect(),
544 signatures,
545 ),
546 ));
547 continue;
548 }
549 let names = import
550 .selected_names
551 .clone()
552 .unwrap_or_else(|| target.exports.keys().cloned().collect());
553 for name in names {
554 imported_names.insert(name.clone());
555 if let Some((declaration_owner, declaration)) =
556 resolve_export_declaration(target, &name, modules, &mut BTreeSet::new())
557 {
558 if imported_declaration_names.insert(name) {
559 imported_declarations.push(declaration);
560 }
561 collect_private_type_declarations(
562 declaration_owner,
563 &mut imported_declaration_names,
564 &mut imported_declarations,
565 );
566 }
567 }
568 collect_private_type_declarations(
573 target,
574 &mut imported_declaration_names,
575 &mut imported_declarations,
576 );
577 }
578
579 let checker = harn_parser::TypeChecker::new()
580 .with_imported_names(imported_names)
581 .with_imported_type_decls(imported_declarations.clone())
582 .with_imported_callable_decls(imported_declarations)
583 .with_namespace_imports(namespace_imports);
584 for error in checker
585 .check(program)
586 .into_iter()
587 .filter(|diagnostic| diagnostic.severity == harn_parser::DiagnosticSeverity::Error)
588 {
589 diagnostics.push(Diagnostic {
590 code: error.code.as_str().to_string(),
591 message: format!("{owner}: {}", error.message),
592 line: error
593 .span
594 .as_ref()
595 .map(|span| span.line.try_into().unwrap_or(u32::MAX)),
596 column: error
597 .span
598 .as_ref()
599 .map(|span| span.column.try_into().unwrap_or(u32::MAX)),
600 });
601 }
602}
603
604fn resolve_export_declaration<'a>(
605 module: &'a PortableModuleSource,
606 name: &str,
607 modules: &BTreeMap<&str, &'a PortableModuleSource>,
608 visiting: &mut BTreeSet<String>,
609) -> Option<(&'a PortableModuleSource, harn_parser::SNode)> {
610 if !module.exports.contains_key(name) || !visiting.insert(module.id.clone()) {
611 return None;
612 }
613 if let Some(declaration) = module
614 .program
615 .iter()
616 .find(|node| declaration_name(node).is_some_and(|candidate| candidate == name))
617 .cloned()
618 {
619 visiting.remove(&module.id);
620 return Some((module, declaration));
621 }
622 for import in module.imports.iter().filter(|import| import.is_pub) {
623 if import
624 .namespace_alias
625 .as_deref()
626 .is_some_and(|alias| alias == name)
627 {
628 continue;
629 }
630 if import
631 .selected_names
632 .as_ref()
633 .is_some_and(|names| !names.iter().any(|candidate| candidate == name))
634 {
635 continue;
636 }
637 let Some(target) = modules.get(import.target.as_str()).copied() else {
638 continue;
639 };
640 if let Some(declaration) = resolve_export_declaration(target, name, modules, visiting) {
641 visiting.remove(&module.id);
642 return Some(declaration);
643 }
644 }
645 visiting.remove(&module.id);
646 None
647}
648
649fn collect_private_type_declarations(
650 module: &PortableModuleSource,
651 names: &mut HashSet<String>,
652 declarations: &mut Vec<harn_parser::SNode>,
653) {
654 for declaration in module
655 .program
656 .iter()
657 .filter(|node| is_type_declaration(node))
658 {
659 let Some(name) = declaration_name(declaration).map(ToOwned::to_owned) else {
660 continue;
661 };
662 if names.insert(name) {
663 declarations.push(declaration.clone());
664 }
665 }
666}
667
668fn declaration_name(node: &harn_parser::SNode) -> Option<&str> {
669 use harn_parser::{BindingPattern, Node};
670
671 let node = match &node.node {
672 Node::AttributedDecl { inner, .. } => inner.as_ref(),
673 _ => node,
674 };
675 match &node.node {
676 Node::FnDecl { name, .. }
677 | Node::Pipeline { name, .. }
678 | Node::ToolDecl { name, .. }
679 | Node::StructDecl { name, .. }
680 | Node::EnumDecl { name, .. }
681 | Node::InterfaceDecl { name, .. }
682 | Node::TypeDecl { name, .. } => Some(name),
683 Node::SkillDecl { name, .. } => Some(name),
684 Node::EvalPackDecl { binding_name, .. } => Some(binding_name),
685 Node::LetBinding {
686 pattern: BindingPattern::Identifier(name),
687 ..
688 }
689 | Node::ConstBinding {
690 pattern: BindingPattern::Identifier(name),
691 ..
692 } => Some(name),
693 _ => None,
694 }
695}
696
697fn is_type_declaration(node: &harn_parser::SNode) -> bool {
698 let node = match &node.node {
699 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
700 _ => node,
701 };
702 matches!(
703 node.node,
704 harn_parser::Node::StructDecl { .. }
705 | harn_parser::Node::EnumDecl { .. }
706 | harn_parser::Node::InterfaceDecl { .. }
707 | harn_parser::Node::TypeDecl { .. }
708 )
709}
710
711pub fn compile_program(
712 source: &str,
713 entry: &str,
714 entry_kind: EntryKind,
715) -> Result<ProgramArtifact, Vec<Diagnostic>> {
716 if source.len() > crate::PORTABLE_MAX_SOURCE_BYTES {
717 return Err(vec![Diagnostic::new(
718 "source_too_large",
719 "source exceeds the portable compiler's 1 MiB limit",
720 )]);
721 }
722 crate::portable_builtin::install_source_contracts();
723 let program = harn_parser::check_source_strict(source).map_err(|error| {
724 vec![Diagnostic {
725 code: "compile_frontend".to_string(),
726 message: error.to_string(),
727 line: None,
728 column: None,
729 }]
730 })?;
731 let compiled = match entry_kind {
732 EntryKind::Function => Compiler::with_options(CompilerOptions::portable_artifact())
733 .compile_named_function_entry(&program, entry),
734 EntryKind::Pipeline => Compiler::with_options(CompilerOptions::portable_artifact())
735 .compile_named_pipeline_entry(&program, entry, None),
736 }
737 .map_err(|error| {
738 vec![Diagnostic {
739 code: "compile_bytecode".to_string(),
740 message: error.message,
741 line: Some(error.line),
742 column: None,
743 }]
744 })?;
745 let wire = WireProgram::from_image(
746 &compiled.bootstrap,
747 entry.to_string(),
748 entry_kind,
749 compiled.expects_harness,
750 )
751 .map_err(|diagnostic| vec![diagnostic])?;
752 wire.validate_metadata(ArtifactLimits::default())
753 .map_err(|error| vec![error])?;
754 let payload = encode_wire_program(&wire).map_err(|error| vec![error])?;
755 encode_artifact_payload(payload)
756}
757
758fn encode_artifact_payload(payload: Vec<u8>) -> Result<ProgramArtifact, Vec<Diagnostic>> {
759 if payload.len() > u32::MAX as usize {
760 return Err(vec![Diagnostic::artifact(
761 "artifact_too_large",
762 "artifact payload exceeds the format's u32 length",
763 )]);
764 }
765 let digest = *blake3::hash(&payload).as_bytes();
766 let mut bytes = Vec::with_capacity(HEADER_BYTES + payload.len());
767 bytes.extend_from_slice(MAGIC);
768 bytes.extend_from_slice(&ARTIFACT_VERSION.to_be_bytes());
769 bytes.extend_from_slice(&0u16.to_be_bytes());
770 bytes.extend_from_slice(&(payload.len() as u32).to_be_bytes());
771 bytes.extend_from_slice(&digest);
772 bytes.extend_from_slice(&payload);
773 ProgramArtifact::decode(&bytes, ArtifactLimits::default()).map_err(|error| vec![error])
774}
775
776#[cfg(test)]
777mod tests;