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 = 5;
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-v4\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    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/// One compiled module embedded in a portable program package.
125///
126/// The chunks and functions are immutable and share their allocation across
127/// native threads and worker instances. Runtime environments are created per
128/// execution, so module state never leaks between dispatches.
129#[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    /// Resolved import edges of the root source module.
184    pub fn root_imports(&self) -> &[PortableImport] {
185        &self.root_imports
186    }
187
188    /// Embedded module closure, excluding the root source module.
189    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/// Parsed, graph-resolved source input for [`compile_program_package`].
290///
291/// A host (normally the CLI or a build service) owns path resolution and
292/// typechecking. The kernel receives only deterministic ASTs and the narrow
293/// import/export projections needed to link them, which keeps browser builds
294/// free of filesystem and package-manager authority.
295#[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
312/// Parse a resolved source package manifest using the canonical Harn lexer and
313/// parser, then compile it through [`compile_program_package`]. This keeps the
314/// browser and native source-to-artifact path on one frontend; hosts still own
315/// import resolution and typechecking before serializing the manifest.
316pub 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
371/// Compile one source module into the current artifact shape. Imports are rejected
372/// here because only [`compile_program_package`] has a resolved closure to
373/// bind them against; this prevents a second, path-sensitive loader from
374/// appearing in the browser adapter.
375pub 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
439/// Type-check a closed, host-resolved package without consulting paths or a
440/// package manager. This deliberately checks the requested root, matching
441/// `harn check <entry>`: dependency declarations contribute signatures and
442/// private supporting types, while dependency bodies remain their owning
443/// modules' responsibility. Re-checking every transitive stdlib body here
444/// would create a stricter, parallel frontend for portable builds.
445fn 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            // Artifact metadata validation owns the stable missing-target
497            // diagnostic. Avoid manufacturing a second checker error here.
498            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        // Imported callable signatures may refer to types that are private to
563        // their defining module. The canonical module graph makes those types
564        // visible to the checker (but not to source-level name lookup); carry
565        // the same declaration context in this path-independent projection.
566        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;