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
17pub 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
255fn 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;