Skip to main content

harn_kernel/
artifact.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Chunk, Compiler, CompilerOptions};
6
7use self::wire::{encode_wire_program, ArtifactReader, WireProgram};
8
9mod validation;
10mod wire;
11
12const MAGIC: &[u8; 8] = b"HARNPK01";
13pub const ARTIFACT_VERSION: u16 = 1;
14const HEADER_BYTES: usize = 8 + 2 + 2 + 4 + 32;
15const SEMANTIC_ABI_DOMAIN: &[u8] = b"harn-portable-kernel-semantic-abi-v1\0";
16
17/// Hex fingerprint of every opcode, portable builtin, and capability contract
18/// that contributes to artifact execution semantics.
19pub fn semantic_abi_fingerprint_hex() -> String {
20    validation::semantic_abi_fingerprint()
21        .iter()
22        .map(|byte| format!("{byte:02x}"))
23        .collect()
24}
25
26#[derive(Debug, Clone, Copy)]
27pub struct ArtifactLimits {
28    pub max_bytes: usize,
29    pub max_chunks: usize,
30    pub max_functions: usize,
31    pub max_instructions: usize,
32    pub max_constants: usize,
33    pub max_string_bytes: usize,
34    pub max_metadata_entries: usize,
35    pub max_type_nodes: usize,
36    pub max_type_depth: usize,
37}
38
39impl Default for ArtifactLimits {
40    fn default() -> Self {
41        Self {
42            max_bytes: 8 * 1024 * 1024,
43            max_chunks: 16_384,
44            max_functions: 16_384,
45            max_instructions: 4 * 1024 * 1024,
46            max_constants: 1_048_576,
47            max_string_bytes: 4 * 1024 * 1024,
48            max_metadata_entries: 1_048_576,
49            max_type_nodes: 262_144,
50            max_type_depth: 128,
51        }
52    }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub struct Diagnostic {
58    pub code: String,
59    pub message: String,
60    pub line: Option<u32>,
61    pub column: Option<u32>,
62}
63
64impl Diagnostic {
65    fn artifact(code: &str, message: impl Into<String>) -> Self {
66        Self {
67            code: code.to_string(),
68            message: message.into(),
69            line: None,
70            column: None,
71        }
72    }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum EntryKind {
78    Function,
79    Pipeline,
80}
81
82#[derive(Debug, Clone)]
83pub struct ProgramArtifact {
84    bytes: Arc<[u8]>,
85    digest: [u8; 32],
86    image: Arc<Chunk>,
87    entry: String,
88    entry_kind: EntryKind,
89    expects_harness: bool,
90}
91
92impl ProgramArtifact {
93    pub fn bytes(&self) -> &[u8] {
94        &self.bytes
95    }
96    pub fn digest(&self) -> [u8; 32] {
97        self.digest
98    }
99    pub fn digest_hex(&self) -> String {
100        self.digest
101            .iter()
102            .map(|byte| format!("{byte:02x}"))
103            .collect()
104    }
105    pub fn image(&self) -> &Arc<Chunk> {
106        &self.image
107    }
108    pub fn entry(&self) -> &str {
109        &self.entry
110    }
111    pub fn entry_kind(&self) -> EntryKind {
112        self.entry_kind.clone()
113    }
114    pub fn expects_harness(&self) -> bool {
115        self.expects_harness
116    }
117
118    pub fn decode(bytes: &[u8], limits: ArtifactLimits) -> Result<Self, Diagnostic> {
119        if bytes.len() > limits.max_bytes {
120            return Err(Diagnostic::artifact(
121                "artifact_too_large",
122                format!(
123                    "artifact has {} bytes; limit is {}",
124                    bytes.len(),
125                    limits.max_bytes
126                ),
127            ));
128        }
129        if bytes.len() < HEADER_BYTES {
130            return Err(Diagnostic::artifact(
131                "artifact_truncated",
132                "artifact header is truncated",
133            ));
134        }
135        if &bytes[..8] != MAGIC {
136            return Err(Diagnostic::artifact(
137                "artifact_magic",
138                "artifact magic does not identify a portable Harn program",
139            ));
140        }
141        let version = u16::from_be_bytes([bytes[8], bytes[9]]);
142        if version != ARTIFACT_VERSION {
143            return Err(Diagnostic::artifact(
144                "artifact_version",
145                format!("artifact version {version} is not supported; expected {ARTIFACT_VERSION}"),
146            ));
147        }
148        let flags = u16::from_be_bytes([bytes[10], bytes[11]]);
149        if flags != 0 {
150            return Err(Diagnostic::artifact(
151                "artifact_features",
152                format!("artifact uses unsupported feature bits 0x{flags:04x}"),
153            ));
154        }
155        let payload_len =
156            u32::from_be_bytes(bytes[12..16].try_into().expect("header length checked")) as usize;
157        let total = HEADER_BYTES.checked_add(payload_len).ok_or_else(|| {
158            Diagnostic::artifact("artifact_too_large", "artifact length overflow")
159        })?;
160        if total != bytes.len() {
161            return Err(Diagnostic::artifact(
162                if total > bytes.len() {
163                    "artifact_truncated"
164                } else {
165                    "artifact_trailing_bytes"
166                },
167                format!(
168                    "header declares {payload_len} payload bytes but {} are present",
169                    bytes.len() - HEADER_BYTES
170                ),
171            ));
172        }
173        let expected_digest: [u8; 32] = bytes[16..48].try_into().expect("header length checked");
174        let payload = &bytes[HEADER_BYTES..];
175        let digest = *blake3::hash(payload).as_bytes();
176        if digest != expected_digest {
177            return Err(Diagnostic::artifact(
178                "artifact_corrupt",
179                "artifact payload digest does not match its header",
180            ));
181        }
182        let wire = ArtifactReader::new(payload, limits).read_program()?;
183        let image = wire.validate_and_build(limits)?;
184        Ok(Self {
185            bytes: Arc::from(bytes),
186            digest,
187            image: Arc::new(image),
188            entry: wire.entry,
189            entry_kind: wire.entry_kind,
190            expects_harness: wire.expects_harness,
191        })
192    }
193}
194
195pub fn compile_program(
196    source: &str,
197    entry: &str,
198    entry_kind: EntryKind,
199) -> Result<ProgramArtifact, Vec<Diagnostic>> {
200    let program = harn_parser::check_source_strict(source).map_err(|error| {
201        vec![Diagnostic {
202            code: "compile_frontend".to_string(),
203            message: error.to_string(),
204            line: None,
205            column: None,
206        }]
207    })?;
208    let portable_diagnostics = portable_frontend_diagnostics(&program);
209    if !portable_diagnostics.is_empty() {
210        return Err(portable_diagnostics);
211    }
212    let compiled = match entry_kind {
213        EntryKind::Function => Compiler::with_options(CompilerOptions::optimized())
214            .compile_named_function_entry(&program, entry),
215        EntryKind::Pipeline => Compiler::with_options(CompilerOptions::optimized())
216            .compile_named_pipeline_entry(&program, entry, None),
217    }
218    .map_err(|error| {
219        vec![Diagnostic {
220            code: "compile_bytecode".to_string(),
221            message: error.message,
222            line: Some(error.line),
223            column: None,
224        }]
225    })?;
226    let wire = WireProgram::from_image(
227        &compiled.bootstrap,
228        entry.to_string(),
229        entry_kind,
230        compiled.expects_harness,
231    )
232    .map_err(|diagnostic| vec![diagnostic])?;
233    wire.validate_metadata(ArtifactLimits::default())
234        .map_err(|error| vec![error])?;
235    let payload = encode_wire_program(&wire).map_err(|error| vec![error])?;
236    if payload.len() > u32::MAX as usize {
237        return Err(vec![Diagnostic::artifact(
238            "artifact_too_large",
239            "artifact payload exceeds the format's u32 length",
240        )]);
241    }
242    let digest = *blake3::hash(&payload).as_bytes();
243    let mut bytes = Vec::with_capacity(HEADER_BYTES + payload.len());
244    bytes.extend_from_slice(MAGIC);
245    bytes.extend_from_slice(&ARTIFACT_VERSION.to_be_bytes());
246    bytes.extend_from_slice(&0u16.to_be_bytes());
247    bytes.extend_from_slice(&(payload.len() as u32).to_be_bytes());
248    bytes.extend_from_slice(&digest);
249    bytes.extend_from_slice(&payload);
250    let artifact =
251        ProgramArtifact::decode(&bytes, ArtifactLimits::default()).map_err(|error| vec![error])?;
252    Ok(artifact)
253}
254
255/// Reject frontend constructs whose canonical bytecode currently delegates a
256/// semantic check to a host-only VM builtin. Keeping this boundary structural
257/// prevents a compiled artifact from failing later with a misleading missing-
258/// builtin error and gives native and Wasm callers the same exact diagnostic.
259fn portable_frontend_diagnostics(program: &[harn_parser::SNode]) -> Vec<Diagnostic> {
260    let mut diagnostics = Vec::new();
261    harn_parser::visit::walk_program(program, &mut |node| {
262        let (callable, params) = match &node.node {
263            harn_parser::Node::Pipeline { name, params, .. }
264            | harn_parser::Node::FnDecl { name, params, .. }
265            | harn_parser::Node::ToolDecl { name, params, .. } => {
266                (name.as_str(), params.as_slice())
267            }
268            harn_parser::Node::Closure { params, .. } => ("<closure>", params.as_slice()),
269            _ => return,
270        };
271        for parameter in params {
272            if parameter.type_expr.is_some() && parameter.default_value.is_some() {
273                diagnostics.push(Diagnostic {
274                    code: "unsupported_portable_typed_default".to_string(),
275                    message: format!(
276                        "typed default parameter `{}.{}` is outside Portable Kernel v1; use an untyped default or initialize and validate it in the function body",
277                        callable, parameter.name
278                    ),
279                    line: Some(parameter.span.line.try_into().unwrap_or(u32::MAX)),
280                    column: Some(parameter.span.column.try_into().unwrap_or(u32::MAX)),
281                });
282            }
283        }
284    });
285    diagnostics
286}
287
288#[cfg(test)]
289mod tests;