1use crate::Bloq;
22
23pub(crate) mod instance_measurements {
26 use std::fmt;
27
28 use serde::{
29 Deserialize, Deserializer, Serialize, Serializer,
30 de::{Error, SeqAccess, Visitor},
31 ser::SerializeSeq,
32 };
33
34 use crate::{InstanceMeasurement, TemplateInstanceId};
35
36 pub(crate) fn serialize<S: Serializer>(
37 records: &[InstanceMeasurement],
38 serializer: S,
39 ) -> Result<S::Ok, S::Error> {
40 if serializer.is_human_readable() {
41 return records.serialize(serializer);
42 }
43 let runs = records.chunk_by(|a, b| a.instance == b.instance);
44 let mut sequence =
47 serializer.serialize_seq(Some(1 + 2 * runs.clone().count() + records.len()))?;
48 sequence.serialize_element(&records.len())?;
49 for run in runs {
50 sequence.serialize_element(&run.len())?;
51 sequence.serialize_element(&run[0].instance)?;
52 for record in run {
53 sequence.serialize_element(&record.measurement)?;
54 }
55 }
56 sequence.end()
57 }
58
59 struct Records;
60
61 impl<'de> Visitor<'de> for Records {
62 type Value = Vec<InstanceMeasurement>;
63
64 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 formatter.write_str("a record count followed by instance runs")
66 }
67
68 fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<Self::Value, A::Error> {
69 let count: usize = sequence
70 .next_element()?
71 .ok_or_else(|| A::Error::custom("missing record count"))?;
72 let mut records =
75 Vec::with_capacity(count.min((1 << 20) / size_of::<InstanceMeasurement>()));
76 while let Some(length) = sequence.next_element::<usize>()? {
77 if length == 0 || length > count - records.len() {
78 return Err(A::Error::custom("invalid record run length"));
79 }
80 let instance: TemplateInstanceId = sequence
81 .next_element()?
82 .ok_or_else(|| A::Error::custom("record run needs an instance"))?;
83 for _ in 0..length {
84 let measurement = sequence
85 .next_element()?
86 .ok_or_else(|| A::Error::custom("record run is truncated"))?;
87 records.push(InstanceMeasurement {
88 instance,
89 measurement,
90 });
91 }
92 }
93 if records.len() != count {
94 return Err(A::Error::custom("record count does not match its runs"));
95 }
96 Ok(records)
97 }
98 }
99
100 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
101 deserializer: D,
102 ) -> Result<Vec<InstanceMeasurement>, D::Error> {
103 if deserializer.is_human_readable() {
104 return Vec::deserialize(deserializer);
105 }
106 deserializer.deserialize_seq(Records)
107 }
108}
109
110pub const BLOQ_BINARY_MAGIC: [u8; 4] = *b"BLOQ";
112
113pub const BLOQ_BINARY_VERSION: u8 = 1;
115
116pub const BLOQ_BINARY_EXTENSION: &str = "bloq";
118
119#[derive(Debug, thiserror::Error)]
121#[non_exhaustive]
122pub enum BinaryDecodeError {
123 #[error("not a bloq binary artifact: missing `BLOQ` magic")]
125 BadMagic,
126 #[error("truncated bloq binary artifact: magic without a version byte")]
128 Truncated,
129 #[error(
131 "unsupported bloq binary version {found} (this reader supports only \
132 version {BLOQ_BINARY_VERSION}; there is no cross-version compatibility)"
133 )]
134 UnsupportedVersion {
135 found: u8,
137 },
138 #[error("malformed bloq binary payload")]
140 Payload(#[source] postcard::Error),
141 #[error("{extra} trailing byte(s) after the bloq binary payload")]
143 TrailingData {
144 extra: usize,
146 },
147}
148
149impl Bloq {
150 pub fn to_binary(&self) -> Vec<u8> {
156 let mut bytes = Vec::with_capacity(1024);
157 bytes.extend_from_slice(&BLOQ_BINARY_MAGIC);
158 bytes.push(BLOQ_BINARY_VERSION);
159 postcard::to_extend(self, bytes)
160 .expect("postcard serialization into a Vec cannot fail for serde-derived IR types")
161 }
162
163 pub fn from_binary(bytes: &[u8]) -> Result<Self, BinaryDecodeError> {
175 let payload = bytes
176 .strip_prefix(&BLOQ_BINARY_MAGIC)
177 .ok_or(BinaryDecodeError::BadMagic)?;
178 let (&version, payload) = payload.split_first().ok_or(BinaryDecodeError::Truncated)?;
179 if version != BLOQ_BINARY_VERSION {
180 return Err(BinaryDecodeError::UnsupportedVersion { found: version });
181 }
182 let (bloq, rest) =
186 postcard::take_from_bytes(payload).map_err(BinaryDecodeError::Payload)?;
187 if !rest.is_empty() {
188 return Err(BinaryDecodeError::TrailingData { extra: rest.len() });
189 }
190 Ok(bloq)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use crate::{BloqNodeId, QuantumTimeline, test_fixture::sample_bloq};
197
198 use super::*;
199
200 #[test]
201 fn retry_region_keeps_its_binary_tag_and_removed_regions_are_rejected() {
202 use crate::{ClassicalExpr, RegionNode, SubGraph};
203
204 let region = RegionNode::RepeatUntilSuccess {
205 body: SubGraph::new(),
206 restart_condition: ClassicalExpr::Const(false),
207 restart_source: None,
208 };
209 let bytes = postcard::to_extend(®ion, Vec::new()).unwrap();
210 assert_eq!(bytes[0], 1, "preserve the pre-removal retry tag");
211 let restored: RegionNode = postcard::from_bytes(&bytes).unwrap();
212 assert_eq!(postcard::to_extend(&restored, Vec::new()).unwrap(), bytes);
213
214 let mut program = Bloq::new();
215 program.add_node(crate::BloqNode::region(region));
216 let mut artifact = program.to_binary();
217 let offsets = artifact
218 .windows(bytes.len())
219 .enumerate()
220 .filter_map(|(offset, payload)| (payload == bytes).then_some(offset))
221 .collect::<Vec<_>>();
222 assert_eq!(offsets.len(), 1, "the single serialized region");
223 Bloq::from_binary(&artifact).unwrap();
224 artifact[offsets[0]] = 0;
225 assert!(matches!(
226 Bloq::from_binary(&artifact),
227 Err(BinaryDecodeError::Payload(_))
228 ));
229
230 postcard::from_bytes::<RegionNode>(&[0]).unwrap_err();
233 serde_json::from_str::<RegionNode>(r#"{"Cond":{"condition":{"Const":false},"body":{}}}"#)
234 .unwrap_err();
235 Bloq::from_text("BLOQIR 1\ngraph {\n n0 cond 0 {\n body {\n }\n }\n}").unwrap_err();
236 }
237
238 #[test]
239 fn record_runs_preserve_order_duplicates_and_full_width_ids() {
240 use crate::{ClassicalNode, InstanceMeasurement, TemplateInstanceId};
241
242 for pairs in [
243 vec![],
244 vec![(0, 0)],
245 vec![
246 (300, 8),
247 (300, 2),
248 (300, 2),
249 (0, 0),
250 (300, 1),
251 (u32::MAX, u32::MAX),
252 ],
253 (0..260).map(|i| (i / 130, i)).collect(),
254 ] {
255 let measurements = pairs
256 .into_iter()
257 .map(|(instance, measurement)| InstanceMeasurement {
258 instance: TemplateInstanceId(instance),
259 measurement,
260 })
261 .collect::<Vec<_>>();
262 let node = ClassicalNode::observable_fragment(measurements.clone(), Vec::new());
263 let bytes = postcard::to_extend(&node, Vec::new()).unwrap();
264 let restored: ClassicalNode = postcard::from_bytes(&bytes).unwrap();
265 assert_eq!(restored, node);
266 assert_eq!(postcard::to_extend(&restored, Vec::new()).unwrap(), bytes);
267 assert_eq!(
268 serde_json::to_value(&node).unwrap(),
269 serde_json::json!({"Observable": {"index": null, "measurements": measurements, "operators": []}})
270 );
271 assert_eq!(
272 serde_json::from_value::<ClassicalNode>(serde_json::to_value(&node).unwrap())
273 .unwrap(),
274 node
275 );
276 for length in 0..bytes.len() {
277 postcard::from_bytes::<ClassicalNode>(&bytes[..length]).unwrap_err();
278 }
279 }
280 #[derive(serde::Deserialize)]
281 struct Records(#[serde(with = "super::instance_measurements")] Vec<InstanceMeasurement>);
282 assert!(
283 postcard::from_bytes::<Records>(&postcard::to_extend(&vec![0u64], Vec::new()).unwrap())
284 .unwrap()
285 .0
286 .is_empty()
287 );
288 for stream in [
290 vec![],
291 vec![1],
292 vec![1, 0],
293 vec![1, 2, 0, 0, 1],
294 vec![1, 1],
295 vec![1, 1, 0],
296 vec![0, 1, 0, 0],
297 vec![u64::MAX],
298 ] {
299 let bytes = postcard::to_extend(&stream, Vec::new()).unwrap();
300 assert!(postcard::from_bytes::<Records>(&bytes).is_err());
301 }
302 }
303
304 #[test]
305 fn binary_round_trip_preserves_ids_edges_and_bytes() {
306 let bloq = sample_bloq();
307 let bytes = bloq.to_binary();
308 assert_eq!(&bytes[..5], b"BLOQ\x01");
309 let restored = Bloq::from_binary(&bytes).expect("round trip decodes");
310
311 let original_ids: Vec<BloqNodeId> = bloq.node_ids().collect();
313 let restored_ids: Vec<BloqNodeId> = restored.node_ids().collect();
314 assert_eq!(original_ids, restored_ids);
315 assert!(!original_ids.contains(&BloqNodeId(1)));
316
317 assert_eq!(
319 bloq.deterministic_emit_order().expect("acyclic"),
320 restored.deterministic_emit_order().expect("acyclic"),
321 );
322
323 assert_eq!(bloq.edge_count(), restored.edge_count());
324 assert_eq!(bloq.templates().len(), restored.templates().len());
325
326 assert_eq!(bytes, restored.to_binary());
328
329 let mut edited = restored.clone();
331 let mut seams = 0;
332 edited.top_mut().for_each_edge_mut(|edge| {
333 if let crate::BloqEdge::Quantum(quantum) = edge {
334 quantum.pipes.clear();
335 seams += 1;
336 }
337 });
338 assert!(seams > 0);
339 assert_eq!(bytes, restored.to_binary());
340 assert_ne!(bytes, edited.to_binary());
341 }
342
343 #[test]
344 fn binary_round_trip_keeps_one_shared_bundle_and_two_uses() {
345 let bloq = Bloq::from_text(
346 "BLOQIR 1\n\
347 template t0 {\n circuit {\n M (0,0):m0\n }\n}\n\
348 bundle b0 owners t0 {\n detector o0:m0\n}\n\
349 graph {\n\
350 n0 quantum {\n instance i0 t0 @ (0,0)\n use b0 i0 @ (0,0)\n }\n\
351 n1 quantum {\n instance i1 t0 @ (2,0)\n use b0 i1 @ (2,0)\n }\n\
352 }\n",
353 )
354 .unwrap();
355 let restored = Bloq::from_binary(&bloq.to_binary()).unwrap();
356 assert_eq!(restored.detector_bundles().len(), 1);
357 assert_eq!(
358 restored[BloqNodeId(0)]
359 .expect_quantum()
360 .detector_bundles
361 .len(),
362 1
363 );
364 assert_eq!(
365 restored[BloqNodeId(1)]
366 .expect_quantum()
367 .detector_bundles
368 .len(),
369 1
370 );
371 assert_eq!(restored.to_binary(), bloq.to_binary());
372 }
373
374 #[test]
375 fn from_binary_rejects_bad_magic_and_version() {
376 assert!(matches!(
377 Bloq::from_binary(b"nope"),
378 Err(BinaryDecodeError::BadMagic)
379 ));
380 assert!(matches!(
381 Bloq::from_binary(b"BLOQ"),
382 Err(BinaryDecodeError::Truncated)
383 ));
384 let mut bytes = sample_bloq().to_binary();
385 bytes[4] = 99;
386 assert!(matches!(
387 Bloq::from_binary(&bytes),
388 Err(BinaryDecodeError::UnsupportedVersion { found: 99 })
389 ));
390 }
391
392 #[test]
393 fn quantum_timeline_round_trips() {
394 let mut bloq = sample_bloq();
395 let timeline = QuantumTimeline {
396 layer_round_ends: vec![3, 3, 8],
397 };
398 bloq.node_mut(BloqNodeId(0))
399 .expect("fixture quantum node")
400 .expect_quantum_mut()
401 .timeline = Some(timeline.clone());
402
403 let restored = Bloq::from_binary(&bloq.to_binary()).expect("round trip decodes");
404
405 assert_eq!(
406 restored[BloqNodeId(0)].expect_quantum().timeline.as_ref(),
407 Some(&timeline)
408 );
409 }
410
411 #[test]
412 fn from_binary_rejects_trailing_bytes() {
413 let mut bytes = sample_bloq().to_binary();
415 bytes.extend_from_slice(b"junk");
416 assert!(matches!(
417 Bloq::from_binary(&bytes),
418 Err(BinaryDecodeError::TrailingData { extra: 4 })
419 ));
420 }
421}