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 = 3;
18const HEADER_BYTES: usize = 8 + 2 + 2 + 4 + 32;
20const SEMANTIC_ABI_DOMAIN: &[u8] = b"harn-portable-kernel-semantic-abi-v3\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 namespace_imports.push((
509 alias.clone(),
510 harn_parser::NamespaceImportBinding {
511 module_path: import.path.clone(),
512 members: target.exports.keys().cloned().collect::<BTreeSet<_>>(),
513 member_types: std::collections::BTreeMap::new(),
521 member_param_names: std::collections::BTreeMap::new(),
522 member_required_params: std::collections::BTreeMap::new(),
523 },
524 ));
525 continue;
526 }
527 let names = import
528 .selected_names
529 .clone()
530 .unwrap_or_else(|| target.exports.keys().cloned().collect());
531 for name in names {
532 imported_names.insert(name.clone());
533 if let Some((declaration_owner, declaration)) =
534 resolve_export_declaration(target, &name, modules, &mut BTreeSet::new())
535 {
536 if imported_declaration_names.insert(name) {
537 imported_declarations.push(declaration);
538 }
539 collect_private_type_declarations(
540 declaration_owner,
541 &mut imported_declaration_names,
542 &mut imported_declarations,
543 );
544 }
545 }
546 collect_private_type_declarations(
551 target,
552 &mut imported_declaration_names,
553 &mut imported_declarations,
554 );
555 }
556
557 let checker = harn_parser::TypeChecker::new()
558 .with_imported_names(imported_names)
559 .with_imported_type_decls(imported_declarations.clone())
560 .with_imported_callable_decls(imported_declarations)
561 .with_namespace_imports(namespace_imports);
562 for error in checker
563 .check(program)
564 .into_iter()
565 .filter(|diagnostic| diagnostic.severity == harn_parser::DiagnosticSeverity::Error)
566 {
567 diagnostics.push(Diagnostic {
568 code: error.code.as_str().to_string(),
569 message: format!("{owner}: {}", error.message),
570 line: error
571 .span
572 .as_ref()
573 .map(|span| span.line.try_into().unwrap_or(u32::MAX)),
574 column: error
575 .span
576 .as_ref()
577 .map(|span| span.column.try_into().unwrap_or(u32::MAX)),
578 });
579 }
580}
581
582fn resolve_export_declaration<'a>(
583 module: &'a PortableModuleSource,
584 name: &str,
585 modules: &BTreeMap<&str, &'a PortableModuleSource>,
586 visiting: &mut BTreeSet<String>,
587) -> Option<(&'a PortableModuleSource, harn_parser::SNode)> {
588 if !module.exports.contains_key(name) || !visiting.insert(module.id.clone()) {
589 return None;
590 }
591 if let Some(declaration) = module
592 .program
593 .iter()
594 .find(|node| declaration_name(node).is_some_and(|candidate| candidate == name))
595 .cloned()
596 {
597 visiting.remove(&module.id);
598 return Some((module, declaration));
599 }
600 for import in module.imports.iter().filter(|import| import.is_pub) {
601 if import
602 .namespace_alias
603 .as_deref()
604 .is_some_and(|alias| alias == name)
605 {
606 continue;
607 }
608 if import
609 .selected_names
610 .as_ref()
611 .is_some_and(|names| !names.iter().any(|candidate| candidate == name))
612 {
613 continue;
614 }
615 let Some(target) = modules.get(import.target.as_str()).copied() else {
616 continue;
617 };
618 if let Some(declaration) = resolve_export_declaration(target, name, modules, visiting) {
619 visiting.remove(&module.id);
620 return Some(declaration);
621 }
622 }
623 visiting.remove(&module.id);
624 None
625}
626
627fn collect_private_type_declarations(
628 module: &PortableModuleSource,
629 names: &mut HashSet<String>,
630 declarations: &mut Vec<harn_parser::SNode>,
631) {
632 for declaration in module
633 .program
634 .iter()
635 .filter(|node| is_type_declaration(node))
636 {
637 let Some(name) = declaration_name(declaration).map(ToOwned::to_owned) else {
638 continue;
639 };
640 if names.insert(name) {
641 declarations.push(declaration.clone());
642 }
643 }
644}
645
646fn declaration_name(node: &harn_parser::SNode) -> Option<&str> {
647 use harn_parser::{BindingPattern, Node};
648
649 let node = match &node.node {
650 Node::AttributedDecl { inner, .. } => inner.as_ref(),
651 _ => node,
652 };
653 match &node.node {
654 Node::FnDecl { name, .. }
655 | Node::Pipeline { name, .. }
656 | Node::ToolDecl { name, .. }
657 | Node::StructDecl { name, .. }
658 | Node::EnumDecl { name, .. }
659 | Node::InterfaceDecl { name, .. }
660 | Node::TypeDecl { name, .. } => Some(name),
661 Node::SkillDecl { name, .. } => Some(name),
662 Node::EvalPackDecl { binding_name, .. } => Some(binding_name),
663 Node::LetBinding {
664 pattern: BindingPattern::Identifier(name),
665 ..
666 }
667 | Node::ConstBinding {
668 pattern: BindingPattern::Identifier(name),
669 ..
670 } => Some(name),
671 _ => None,
672 }
673}
674
675fn is_type_declaration(node: &harn_parser::SNode) -> bool {
676 let node = match &node.node {
677 harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
678 _ => node,
679 };
680 matches!(
681 node.node,
682 harn_parser::Node::StructDecl { .. }
683 | harn_parser::Node::EnumDecl { .. }
684 | harn_parser::Node::InterfaceDecl { .. }
685 | harn_parser::Node::TypeDecl { .. }
686 )
687}
688
689pub fn compile_program(
690 source: &str,
691 entry: &str,
692 entry_kind: EntryKind,
693) -> Result<ProgramArtifact, Vec<Diagnostic>> {
694 if source.len() > crate::PORTABLE_MAX_SOURCE_BYTES {
695 return Err(vec![Diagnostic::new(
696 "source_too_large",
697 "source exceeds the portable compiler's 1 MiB limit",
698 )]);
699 }
700 crate::portable_builtin::install_source_contracts();
701 let program = harn_parser::check_source_strict(source).map_err(|error| {
702 vec![Diagnostic {
703 code: "compile_frontend".to_string(),
704 message: error.to_string(),
705 line: None,
706 column: None,
707 }]
708 })?;
709 let compiled = match entry_kind {
710 EntryKind::Function => Compiler::with_options(CompilerOptions::portable_artifact())
711 .compile_named_function_entry(&program, entry),
712 EntryKind::Pipeline => Compiler::with_options(CompilerOptions::portable_artifact())
713 .compile_named_pipeline_entry(&program, entry, None),
714 }
715 .map_err(|error| {
716 vec![Diagnostic {
717 code: "compile_bytecode".to_string(),
718 message: error.message,
719 line: Some(error.line),
720 column: None,
721 }]
722 })?;
723 let wire = WireProgram::from_image(
724 &compiled.bootstrap,
725 entry.to_string(),
726 entry_kind,
727 compiled.expects_harness,
728 )
729 .map_err(|diagnostic| vec![diagnostic])?;
730 wire.validate_metadata(ArtifactLimits::default())
731 .map_err(|error| vec![error])?;
732 let payload = encode_wire_program(&wire).map_err(|error| vec![error])?;
733 encode_artifact_payload(payload)
734}
735
736fn encode_artifact_payload(payload: Vec<u8>) -> Result<ProgramArtifact, Vec<Diagnostic>> {
737 if payload.len() > u32::MAX as usize {
738 return Err(vec![Diagnostic::artifact(
739 "artifact_too_large",
740 "artifact payload exceeds the format's u32 length",
741 )]);
742 }
743 let digest = *blake3::hash(&payload).as_bytes();
744 let mut bytes = Vec::with_capacity(HEADER_BYTES + payload.len());
745 bytes.extend_from_slice(MAGIC);
746 bytes.extend_from_slice(&ARTIFACT_VERSION.to_be_bytes());
747 bytes.extend_from_slice(&0u16.to_be_bytes());
748 bytes.extend_from_slice(&(payload.len() as u32).to_be_bytes());
749 bytes.extend_from_slice(&digest);
750 bytes.extend_from_slice(&payload);
751 ProgramArtifact::decode(&bytes, ArtifactLimits::default()).map_err(|error| vec![error])
752}
753
754#[cfg(test)]
755mod tests;