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