1mod parse;
99mod write;
100
101use std::fmt::{self, Write as _};
102
103use crate::Bloq;
104
105pub use parse::TextParseError;
106
107pub const BLOQ_TEXT_VERSION: u32 = 1;
110
111pub const BLOQ_TEXT_EXTENSION: &str = "bloqir";
113
114fn encode_metadata_token(value: &str) -> String {
115 if value.is_empty() {
116 return "~".to_owned();
117 }
118 let mut encoded = String::with_capacity(value.len());
119 for byte in value.bytes() {
120 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.') {
121 encoded.push(char::from(byte));
122 } else {
123 write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
124 }
125 }
126 encoded
127}
128
129fn decode_metadata_token(token: &str) -> Result<String, &'static str> {
130 if token == "~" {
131 return Ok(String::new());
132 }
133 let source = token.as_bytes();
134 let mut decoded = Vec::with_capacity(source.len());
135 let mut index = 0;
136 while index < source.len() {
137 if source[index] != b'%' {
138 decoded.push(source[index]);
139 index += 1;
140 continue;
141 }
142 let hex = source
143 .get(index + 1..index + 3)
144 .ok_or("incomplete percent escape")?;
145 let digit = |byte: u8| match byte {
146 b'0'..=b'9' => Some(byte - b'0'),
147 b'a'..=b'f' => Some(byte - b'a' + 10),
148 b'A'..=b'F' => Some(byte - b'A' + 10),
149 _ => None,
150 };
151 let high = digit(hex[0]).ok_or("invalid percent escape")?;
152 let low = digit(hex[1]).ok_or("invalid percent escape")?;
153 decoded.push((high << 4) | low);
154 index += 3;
155 }
156 String::from_utf8(decoded).map_err(|_| "metadata token is not UTF-8")
157}
158
159impl Bloq {
160 pub fn to_text(&self) -> String {
162 write::write_bloq_text(self)
163 }
164
165 pub fn from_text(text: &str) -> Result<Self, TextParseError> {
172 let mut bloq = parse::parse_bloq_text(text)?;
173 bloq.top_mut().share_classical_data();
174 Ok(bloq)
175 }
176}
177
178impl fmt::Display for Bloq {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str(&self.to_text())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use bloq_circuit::GateType;
187
188 use crate::test_fixture::sample_bloq;
189 use crate::{
190 Bloq, BloqNode, BloqNodeId, BloqValidationError, ClassicalNode, MetadataValue,
191 NodeTemplateInstanceMergeError, QuantumTimeline,
192 };
193
194 fn assert_same_program(a: &Bloq, b: &Bloq) {
197 assert_eq!(a.to_binary(), b.to_binary());
198 }
199
200 #[test]
201 fn text_round_trip_is_lossless() {
202 let bloq = sample_bloq();
203 let text = bloq.to_text();
204 assert!(text.starts_with("BLOQIR 1\n"));
205 let restored = Bloq::from_text(&text).unwrap_or_else(|error| {
206 panic!("fixture text must parse: {error}\n---\n{text}");
207 });
208 assert_same_program(&bloq, &restored);
209 assert_eq!(text, restored.to_text());
211 }
212
213 #[test]
214 fn detector_bundle_text_round_trip_preserves_sharing_and_guard() {
215 let bloq = Bloq::from_text(
216 "BLOQIR 1\n\
217 template t0 {\n circuit {\n M (0,0):m0\n }\n}\n\
218 bundle b0 owners t0 {\n detector -o0:m0 @ (1,2)\n}\n\
219 graph {\n\
220 n0 quantum {\n instance i0 t0 @ (0,0)\n detector -i0:m0 @ (1,2)\n use b0 i0 @ (0,0)\n guard 0 b0\n }\n\
221 n1 quantum {\n instance i1 t0 @ (2,0)\n use b0 i1 @ (2,0)\n }\n\
222 }\n",
223 )
224 .unwrap();
225 let text = bloq.to_text();
226 assert_eq!(text.matches("bundle b0 owners").count(), 1);
227 assert_eq!(text.matches("use b0").count(), 2);
228 let restored = Bloq::from_text(&text).unwrap();
229 assert_same_program(&bloq, &restored);
230 assert_eq!(restored.detector_bundles().len(), 1);
231 assert_eq!(
232 restored[BloqNodeId(0)].expect_quantum().guards[0].detector_bundles,
233 vec![0]
234 );
235 let mut rows = restored
236 .node_detectors(restored[BloqNodeId(0)].expect_quantum())
237 .unwrap();
238 assert_eq!(
239 rows.next().unwrap().to_owned(),
240 rows.next().unwrap().to_owned()
241 );
242 let second = restored
243 .node_detectors(restored[BloqNodeId(1)].expect_quantum())
244 .unwrap()
245 .next()
246 .unwrap()
247 .to_owned();
248 assert_eq!(second.coords.unwrap().as_slice(), &[3.0, 2.0]);
249 }
250
251 #[test]
252 fn malformed_bundle_tokens_return_parse_errors() {
253 for text in [
254 "BLOQIR 1\nbundle b1 owners {\n}\ngraph {\n}\n",
255 "BLOQIR 1\nbundle b0 owners {\n detector i0:m0\n}\ngraph {\n}\n",
256 "BLOQIR 1\nbundle b0 owners {\n detector o4294967296:m0\n}\ngraph {\n}\n",
257 "BLOQIR 1\ngraph {\n n0 quantum {\n use b0 i0\n }\n}\n",
258 ] {
259 assert!(Bloq::from_text(text).is_err(), "{text}");
260 }
261 }
262
263 #[test]
264 fn malformed_bundle_bindings_return_typed_errors() {
265 use crate::DetectorBundleError;
266
267 let bloq = Bloq::from_text(
268 "BLOQIR 1\n\
269 bundle b0 owners t0 t0 {\n detector o0:m0\n}\n\
270 graph {\n n0 quantum {\n use b0 i0 @ (0,0)\n }\n}\n",
271 )
272 .unwrap();
273 assert!(matches!(
274 bloq.node_detectors(bloq[BloqNodeId(0)].expect_quantum()),
275 Err(DetectorBundleError::BindingArity {
276 expected: 2,
277 actual: 1,
278 ..
279 })
280 ));
281
282 let bloq = Bloq::from_text(
283 "BLOQIR 1\n\
284 bundle b0 owners t0 {\n detector o1:m0\n}\n\
285 graph {\n n0 quantum {\n use b0 i0 @ (0,0)\n }\n}\n",
286 )
287 .unwrap();
288 assert!(matches!(
289 bloq.node_detectors(bloq[BloqNodeId(0)].expect_quantum()),
290 Err(DetectorBundleError::InvalidLocalOwner {
291 owner: 1,
292 owners: 1
293 })
294 ));
295 }
296
297 #[test]
298 fn expression_codecs_preserve_arity_nesting_and_selection_order() {
299 use crate::ClassicalExpr::{self, And, Const, In, Or, Xor};
300 let mut expressions = Vec::new();
301 for build in [Xor, And, Or] {
302 expressions.extend([
303 build(Box::new([])),
304 build(Box::new([In(0)])),
305 build(Box::new([In(0), In(1), In(2)])),
306 build(Box::new([
307 build(Box::new([In(0), In(1)])),
308 build(Box::new([In(2), Const(true)])),
309 ])),
310 ]);
311 }
312 expressions.extend([
313 ClassicalExpr::Parity {
314 inputs: Box::new([]),
315 constant: true,
316 },
317 ClassicalExpr::Parity {
318 inputs: Box::new([u32::MAX, 0, u32::MAX]),
319 constant: false,
320 },
321 ClassicalExpr::parity([0, 1, 2], true),
322 ]);
323 expressions.push(ClassicalExpr::select(
324 In(0),
325 Xor(Box::new([In(1), Const(true), Const(false)])),
326 ClassicalExpr::select(In(2), Or(Box::new([])), And(Box::new([]))),
327 ));
328 for expr in expressions {
329 let mut program = Bloq::new();
330 program.add_node(BloqNode::classical(ClassicalNode::Compute { expr }));
331 let text = program.to_text();
332 assert_same_program(&program, &Bloq::from_text(&text).unwrap());
333 assert_same_program(&program, &Bloq::from_binary(&program.to_binary()).unwrap());
334 }
335 let flat = Bloq::from_text("BLOQIR 1\ngraph {\n n0 compute in0 ^ in1 ^ in2\n}\n").unwrap();
336 let ClassicalNode::Compute {
337 expr: Xor(operands),
338 } = flat[BloqNodeId(0)].try_classical().unwrap()
339 else {
340 panic!("flat parity");
341 };
342 assert_eq!(operands.len(), 3);
343 for malformed in [
344 "parity()",
345 "parity(in0)",
346 "parity(0, 1)",
347 "parity(0, !in0)",
348 "select()",
349 "select(0)",
350 "select(0,1)",
351 "select(0,1,0,1)",
352 "select(0 1 0)",
353 "select(0,1,0,)",
354 "xor(0,)",
355 "in0 xor in1",
356 ] {
357 assert!(
358 Bloq::from_text(&format!(
359 "BLOQIR 1\ngraph {{\n n0 compute {malformed}\n}}\n"
360 ))
361 .is_err(),
362 "{malformed}"
363 );
364 }
365 }
366
367 #[test]
368 fn nonzero_entry_body_round_trips_without_renumbering() {
369 use bloq_circuit::{CircuitBody, CoordCircuit, Op};
370
371 let mut circuit = CoordCircuit::new();
372 circuit.do_gate(GateType::H, [glam::IVec2::ZERO]).unwrap();
373 let entry = circuit.add_body(CircuitBody::new());
374 circuit.set_entry_body(entry).unwrap();
375 circuit.push_repeat(bloq_circuit::BodyId(0), 2);
376 let mut bloq = Bloq::new();
377 let template = bloq
378 .templates_mut()
379 .insert(crate::BloqTemplate::new(circuit));
380 let mut quantum = BloqNode::from_members(vec![]);
381 quantum
382 .expect_quantum_mut()
383 .instances
384 .push(crate::TemplateInstance::new(
385 crate::TemplateInstanceId(0),
386 template,
387 glam::IVec2::ZERO,
388 ));
389 bloq.add_node(quantum);
390 bloq.validate().unwrap();
391 let restored = Bloq::from_binary(&bloq.to_binary()).unwrap();
392 let text = restored.to_text();
393 assert!(text.contains("circuit b1 {"));
394 let parsed = Bloq::from_text(&text).unwrap();
395 assert_same_program(&bloq, &parsed);
396 let circuit = &parsed.templates()[crate::TemplateId(0)].circuit;
397 assert_eq!(circuit.entry_body(), entry);
398 assert!(matches!(
399 circuit.body(entry).unwrap().ops(),
400 [Op::Repeat {
401 body: bloq_circuit::BodyId(0),
402 repetitions: 2
403 }]
404 ));
405 Bloq::from_text("BLOQIR 1\ntemplate t0 {\ncircuit {\n}\ncircuit b1 {\n}\n}\ngraph {\n}\n")
406 .unwrap_err();
407 }
408
409 #[test]
410 fn quantum_timeline_round_trips() {
411 let mut bloq = sample_bloq();
412 let quantum = bloq
413 .node_mut(BloqNodeId(0))
414 .expect("fixture quantum node")
415 .expect_quantum_mut();
416 quantum.timeline = Some(QuantumTimeline {
417 layer_round_ends: vec![3, 3, 8],
418 });
419
420 let text = bloq.to_text();
421 let restored = Bloq::from_text(&text).expect("timeline parses");
422 assert_same_program(&bloq, &restored);
423 }
424
425 #[test]
426 fn spatial_port_provenance_round_trips() {
427 let bloq = Bloq::from_text(
428 "BLOQIR 1\n\
429 template t0 {\n\
430 circuit {\n\
431 }\n\
432 }\n\
433 graph {\n\
434 n0 quantum {\n\
435 instance i0 t0 @ (0,0) from spatial-port input cube (1,2,3)\n\
436 from blocks (1,2,3)\n\
437 }\n\
438 n1 quantum {\n\
439 instance i1 t0 @ (0,0) from spatial-port output temporal-port (4,5,6)\n\
440 from spatial-port output (4,5,6)\n\
441 }\n\
442 }\n",
443 )
444 .expect("spatial-Port provenance parses");
445
446 let text = bloq.to_text();
447 let restored = Bloq::from_text(&text).expect("spatial-Port provenance re-parses");
448
449 assert_same_program(&bloq, &restored);
450 assert!(text.contains("from spatial-port input cube (1,2,3)"));
451 assert!(text.contains("from spatial-port output (4,5,6)"));
452 }
453
454 #[test]
455 fn text_round_trip_of_empty_program() {
456 let bloq = Bloq::new();
457 let restored = Bloq::from_text(&bloq.to_text()).expect("empty program parses");
458 assert_same_program(&bloq, &restored);
459 }
460
461 #[test]
462 fn signed_parities_and_flows_round_trip() {
463 let bloq = Bloq::from_text(
464 "BLOQIR 1\n\
465 template t0 {\n\
466 circuit {\n\
467 }\n\
468 detector -0\n\
469 flow _ -> Z(0,0) sign -1\n\
470 }\n\
471 graph {\n\
472 }\n",
473 )
474 .expect("signed IR parses");
475
476 let text = bloq.to_text();
477 assert!(text.contains("detector -0"));
478 assert!(text.contains("flow _ -> Z(0,0) sign -1"));
479 let restored = Bloq::from_text(&text).expect("written signed IR parses");
480 assert_same_program(&bloq, &restored);
481 }
482
483 #[test]
484 fn metadata_round_trip_is_lossless() {
485 let mut bloq = Bloq::new();
486 bloq.insert_metadata("compiler key#%", MetadataValue::U64(u64::MAX));
487 bloq.insert_metadata(
488 "compiler.string",
489 MetadataValue::String("value # % \u{96ea}".to_owned()),
490 );
491 bloq.insert_metadata("empty", MetadataValue::String(String::new()));
492
493 let text = bloq.to_text();
494 let restored = Bloq::from_text(&text).expect("metadata parses");
495
496 assert_eq!(bloq.metadata(), restored.metadata());
497 assert_same_program(&bloq, &restored);
498 assert_eq!(text, restored.to_text());
499 }
500
501 #[test]
502 fn from_text_rejects_duplicate_metadata_keys() {
503 let error = Bloq::from_text(
504 "BLOQIR 1\nmetadata key u64 1\nmetadata key string value\ngraph {\n}\n",
505 )
506 .expect_err("duplicate metadata key");
507
508 assert_eq!(error.line, 3);
509 assert!(error.to_string().contains("duplicate metadata key `key`"));
510 }
511
512 #[test]
513 fn output_declarations_have_explicit_defaults_and_reject_duplicates() {
514 let program =
515 Bloq::from_text("BLOQIR 1\ngraph {\n n0 compute 1\n result false\n bindings\n}\n")
516 .unwrap();
517 assert_eq!(program.value_output(), None);
518 assert!(program.boundary_outputs().is_empty());
519 assert!(!program.to_text().contains("result"));
520 for declaration in ["result false\n result n0", "bindings\n bindings n0"] {
521 let source = format!("BLOQIR 1\ngraph {{\n n0 compute 1\n {declaration}\n}}\n");
522 assert!(
523 Bloq::from_text(&source)
524 .unwrap_err()
525 .to_string()
526 .contains("duplicate")
527 );
528 }
529 }
530
531 #[test]
532 fn text_round_trip_of_empty_fragment() {
533 let mut bloq = Bloq::new();
534 bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
535 Vec::new(),
536 Vec::new(),
537 )));
538 let text = bloq.to_text();
539 assert!(text.contains("n0 observable fragment\n"));
540
541 let restored = Bloq::from_text(&text).expect("writer's empty fragment parses");
542
543 assert_same_program(&bloq, &restored);
544 }
545
546 #[test]
547 fn output_ports_and_composition_round_trip_in_every_reference() {
548 let source = "BLOQIR 1\ngraph {\n\
549 n0 observable 0\n\
550 n1 observable fragment\n\
551 n2 compute in0\n\
552 n3 quantum {\n }\n\
553 n4 quantum {\n }\n\
554 n5 rus in0 source n0:flip {\n\
555 body {\n n0 observable 1\n result n0:flip\n }\n\
556 }\n\
557 n1 -> n0 compose 0\n\
558 n0 -> n2 value 0 flip readout\n\
559 n0 -> n3 value 0 corrected\n\
560 n3 -> n4 quantum when n0:flip\n\
561 result n0:corrected\n\
562 }\n";
563 let program = Bloq::from_text(source).unwrap();
564 let text = program.to_text();
565 assert!(text.contains("n1 -> n0 compose 0"));
566 assert!(text.contains("value 0 flip readout"));
567 assert!(text.contains("quantum when n0:flip"));
568 assert!(text.contains("source n0:flip"));
569 assert!(text.contains("result n0:flip"));
570 assert!(!text.contains("corrected"), "corrected is the default port");
571 assert_same_program(&program, &Bloq::from_text(&text).unwrap());
572 assert_same_program(&program, &Bloq::from_binary(&program.to_binary()).unwrap());
573 }
574
575 #[test]
576 fn obsolete_nodes_and_unknown_ports_are_rejected() {
577 for statement in [
578 "n0 accumulate",
579 "n0 include",
580 "n0 observable-raw 0",
581 "n0 decode-accept 0",
582 "n0 readout-recipe",
583 "n0 compute 0\n result n0:raw",
584 "n0 compute 0\n n0 -> n0 value 0 raw",
585 "n0 observable fragment\n n0 -> n0 compose 0 flip",
586 ] {
587 let source = format!("BLOQIR 1\ngraph {{\n {statement}\n}}\n");
588 assert!(Bloq::from_text(&source).is_err(), "accepted {statement}");
589 }
590 }
591
592 #[test]
593 fn from_text_rejects_unknown_version() {
594 let error =
595 Bloq::from_text("BLOQIR 99\ngraph {\n}\n").expect_err("version 99 is unsupported");
596 assert_eq!(error.line, 1);
597 }
598
599 #[test]
600 fn from_text_reports_line_numbers() {
601 let error =
602 Bloq::from_text("BLOQIR 1\ngraph {\n n0 wobble\n}\n").expect_err("unknown node kind");
603 assert_eq!(error.line, 3);
604 }
605
606 #[test]
607 fn rus_without_body_local_restart_source_round_trips() {
608 let source = "BLOQIR 1\ngraph {\n n0 rus 0 {\n body {\n }\n }\n}\n";
609 let program = Bloq::from_text(source).unwrap();
610 program.validate().unwrap();
611 assert!(!program.to_text().contains("source"));
612 assert_same_program(&program, &Bloq::from_text(&program.to_text()).unwrap());
613 assert_same_program(&program, &Bloq::from_binary(&program.to_binary()).unwrap());
614 }
615
616 #[test]
617 fn rus_unfed_restart_input_without_source_fails_validation() {
618 let program =
619 Bloq::from_text("BLOQIR 1\ngraph {\n n0 rus in0 {\n body {\n }\n }\n}\n").unwrap();
620 assert!(program.validate().is_err());
621 }
622
623 #[test]
624 fn from_text_rejects_duplicate_node_at_its_line() {
625 let error = Bloq::from_text("BLOQIR 1\ngraph {\n n0 observable 0\n n0 observable 1\n}\n")
626 .expect_err("duplicate node id");
627 assert_eq!(error.line, 4);
628 }
629
630 #[test]
631 fn from_text_rejects_dangling_edge_endpoint_at_its_line() {
632 let error = Bloq::from_text("BLOQIR 1\ngraph {\n n0 observable 0\n n0 -> n7 order\n}\n")
633 .expect_err("edge endpoint is not a node");
634 assert_eq!(error.line, 4);
635 }
636
637 #[test]
640 fn from_text_rejects_implausibly_sparse_node_ids() {
641 let error = Bloq::from_text("BLOQIR 1\ngraph {\n n4294967295 observable 0\n}\n")
642 .expect_err("sparse id space");
643 assert_eq!(error.line, 3);
644 }
645
646 #[test]
647 fn from_text_rejects_sparse_body_ids_without_allocating_to_the_id() {
648 let error =
649 Bloq::from_text("BLOQIR 1\ntemplate t0 {\n body b4294967295 {\n }\n}\ngraph {\n}\n")
650 .expect_err("sparse body id");
651 assert_eq!(error.line, 3);
652 }
653
654 #[test]
655 fn from_text_rejects_empty_mpp_products() {
656 let error = Bloq::from_text(
657 "BLOQIR 1\ntemplate t0 {\n circuit {\n MPP _:m0\n }\n}\ngraph {\n}\n",
658 )
659 .expect_err("empty MPP product");
660 assert_eq!(error.line, 4);
661 }
662
663 #[test]
664 fn zero_product_mpp_is_invalid_before_and_after_binary_round_trip() {
665 let mut bloq = Bloq::from_text(
666 "BLOQIR 1\ntemplate t0 {\ncircuit {\n}\n}\ngraph {\nn0 quantum {\ninstance i0 t0 @ (0,0)\n}\n}\n",
667 ).unwrap();
668 bloq.templates_mut()
669 .make_mut(crate::TemplateId(0))
670 .unwrap()
671 .circuit
672 .body_mut(bloq_circuit::BodyId(0))
673 .unwrap()
674 .ops_mut()
675 .push(bloq_circuit::Op::MPP {
676 products: vec![],
677 measurements: vec![],
678 });
679 let restored = Bloq::from_binary(&bloq.to_binary()).unwrap();
680 for program in [&bloq, &restored] {
681 assert!(matches!(
682 program.validate(),
683 Err(BloqValidationError::InvalidTemplateCircuit {
684 source: NodeTemplateInstanceMergeError::EmptyMpp,
685 ..
686 })
687 ));
688 Bloq::from_text(&program.to_text()).unwrap_err();
689 }
690 }
691
692 #[test]
693 fn cyclic_repeat_from_text_validates_as_an_error() {
694 let bloq = Bloq::from_text(
695 "BLOQIR 1\ntemplate t0 {\n circuit {\n REPEAT 2 b0\n }\n}\ngraph {\n n0 quantum {\n instance i0 t0 @ (0,0)\n }\n}\n",
696 )
697 .expect("cyclic repeat is semantic, not syntax");
698 assert!(matches!(
699 bloq.validate().unwrap_err(),
700 BloqValidationError::InvalidTemplateCircuit {
701 source: NodeTemplateInstanceMergeError::CyclicBody(bloq_circuit::BodyId(0)),
702 ..
703 }
704 ));
705 }
706
707 #[test]
710 fn from_text_rejects_conflicting_measurement_records() {
711 let error = Bloq::from_text(
712 "BLOQIR 1\ntemplate t0 {\n circuit {\n M (0,0):m0\n }\n \
713 meas m0 @ (1,1)\n}\ngraph {\n}\n",
714 )
715 .expect_err("conflicting measurement qubits");
716 assert_eq!(error.line, 6);
717 }
718
719 #[test]
720 fn from_text_rejects_conflicting_implied_measurements_at_op_line() {
721 let error = Bloq::from_text(
722 "BLOQIR 1\ntemplate t0 {\n circuit {\n M (0,0):m0 (1,0):m0\n }\n}\ngraph {\n}\n",
723 )
724 .expect_err("one implied id cannot name two qubits");
725
726 assert_eq!(error.line, 4);
727 }
728
729 #[test]
730 fn from_text_rejects_exhausted_measurement_outputs() {
731 let cases = [
732 (
733 "BLOQIR 1\ntemplate t0 {\n meas m4294967295 @ (0,0)\n}\ngraph {\n}\n",
734 3,
735 ),
736 (
737 "BLOQIR 1\ntemplate t0 {\n circuit {\n M (0,0):m4294967295\n }\n}\ngraph {\n}\n",
738 4,
739 ),
740 (
741 "BLOQIR 1\ntemplate t0 {\n circuit {\n MPP X(0,0):m4294967295\n }\n}\ngraph {\n}\n",
742 4,
743 ),
744 ];
745
746 for (source, line) in cases {
747 let error = Bloq::from_text(source).expect_err("mMAX cannot be registered");
748 assert_eq!(error.line, line);
749 assert!(
750 error
751 .to_string()
752 .contains("exhausts the measurement id space")
753 );
754 }
755 }
756
757 #[test]
758 fn from_text_rejects_overflowing_instance_translation() {
759 let error = Bloq::from_text(
760 "BLOQIR 1\ntemplate t0 {\n circuit {\n H (2147483647,0)\n }\n}\ngraph {\n n0 quantum {\n instance i0 t0 @ (1,0)\n }\n}\n",
761 )
762 .expect_err("translated coordinate exceeds i32");
763
764 assert!(error.to_string().contains("overflows after offset"));
765 }
766
767 #[test]
769 fn from_text_rejects_plus_prefixed_ids() {
770 let error = Bloq::from_text("BLOQIR 1\ngraph {\n n+0 observable 0\n}\n")
771 .expect_err("`n+0` is not a node id");
772 assert_eq!(error.line, 3);
773 }
774
775 #[test]
776 fn from_text_rejects_plus_prefixed_correction_and_parity_ids() {
777 let cases = [
778 (
779 "BLOQIR 1\ntemplate t0 {\n circuit {\n CPAULI X[m+0]@(0,0)\n }\n}\ngraph {\n}\n",
780 4,
781 "expected `m<N>`",
782 ),
783 (
784 "BLOQIR 1\ntemplate t0 {\n loop(b0,s0) init s+0 next 0\n}\ngraph {\n}\n",
785 3,
786 "expected `s<N>`",
787 ),
788 (
789 "BLOQIR 1\ntemplate t0 {\n detector s+0\n}\ngraph {\n}\n",
790 3,
791 "expected `s<N>`",
792 ),
793 ];
794
795 for (source, line, expected) in cases {
796 let error = Bloq::from_text(source).expect_err("plus-prefixed id is malformed");
797 assert_eq!(error.line, line);
798 assert!(error.to_string().contains(expected));
799 }
800 }
801}