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                },
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        // Imported callable signatures may refer to types that are private to
537        // their defining module. The canonical module graph makes those types
538        // visible to the checker (but not to source-level name lookup); carry
539        // the same declaration context in this path-independent projection.
540        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;