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