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