Skip to main content

harn_kernel/
artifact.rs

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;
18/// Maximum UTF-8 source size accepted by every portable compiler adapter.
19const HEADER_BYTES: usize = 8 + 2 + 2 + 4 + 32;
20const SEMANTIC_ABI_DOMAIN: &[u8] = b"harn-portable-kernel-semantic-abi-v2\0";
21
22/// Hex fingerprint of every opcode, portable builtin, and capability contract
23/// that contributes to artifact execution semantics.
24pub 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/// One compiled module embedded in a portable program package.
128///
129/// The chunks and functions are immutable and share their allocation across
130/// native threads and worker instances. Runtime environments are created per
131/// execution, so module state never leaks between dispatches.
132#[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    /// Resolved import edges of the root source module.
190    pub fn root_imports(&self) -> &[PortableImport] {
191        &self.root_imports
192    }
193
194    /// Embedded module closure, excluding the root source module.
195    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/// Parsed, graph-resolved source input for [`compile_program_package`].
296///
297/// A host (normally the CLI or a build service) owns path resolution and
298/// typechecking. The kernel receives only deterministic ASTs and the narrow
299/// import/export projections needed to link them, which keeps browser builds
300/// free of filesystem and package-manager authority.
301#[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
318/// Parse a resolved source package manifest using the canonical Harn lexer and
319/// parser, then compile it through [`compile_program_package`]. This keeps the
320/// browser and native source-to-artifact path on one frontend; hosts still own
321/// import resolution and typechecking before serializing the manifest.
322pub 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
377/// Compile one source module into the current artifact shape. Imports are rejected
378/// here because only [`compile_program_package`] has a resolved closure to
379/// bind them against; this prevents a second, path-sensitive loader from
380/// appearing in the browser adapter.
381pub 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
445/// Type-check a closed, host-resolved package without consulting paths or a
446/// package manager. This deliberately checks the requested root, matching
447/// `harn check <entry>`: dependency declarations contribute signatures and
448/// private supporting types, while dependency bodies remain their owning
449/// modules' responsibility. Re-checking every transitive stdlib body here
450/// would create a stricter, parallel frontend for portable builds.
451fn 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            // Artifact metadata validation owns the stable missing-target
503            // diagnostic. Avoid manufacturing a second checker error here.
504            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                    // Members stay gradual on the artifact path. Lowering a
514                    // signature needs the defining module's type declarations
515                    // to inline named types (see
516                    // `harn-modules::namespace_member_signatures`), and an
517                    // artifact carries resolved exports rather than that
518                    // declaration graph. Empty preserves the pre-#6172
519                    // behavior here instead of checking against a guess.
520                    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        // Imported callable signatures may refer to types that are private to
547        // their defining module. The canonical module graph makes those types
548        // visible to the checker (but not to source-level name lookup); carry
549        // the same declaration context in this path-independent projection.
550        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;