1use std::collections::BTreeSet;
2
3use bloq_circuit::{BodyId, DetectorTerm, LoopStateId, Op};
4use bloq_utils::boolean::BooleanDecisionDiagram;
5use glam::{IVec2, IVec3};
6use thiserror::Error;
7
8use crate::instantiation::{InstantiationOptions, MeasurementSet, NodeEmissionPlan};
9use crate::{
10 Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BloqTemplatePool, ClassicalExpr,
11 ClassicalNode, FxMap, FxSet, InstanceMeasurement, LevelPath, NodeTemplateInstanceMergeError,
12 RegionNode, SubGraph, TemplateDetectorScope, TemplateId, TemplateInstanceId, ValueRole,
13};
14
15#[derive(Debug, Error, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum BloqValidationError {
22 #[error("WF-6: {0}")]
24 DetectorBundle(#[from] crate::DetectorBundleError),
25 #[error("WF-7: node {node:?} cannot read bundle owner instance {instance:?}")]
27 UnavailableDetectorBundleOwner {
28 node: BloqNodeId,
30 instance: TemplateInstanceId,
32 },
33 #[error("WF-6: detector bundle {bundle:?} references loop state {state:?}")]
35 DetectorBundleLoopStateUnsupported {
36 bundle: crate::DetectorBundleId,
38 state: LoopStateId,
40 },
41 #[error("WF-4: detector bundle {bundle:?} references unknown template {template:?}")]
43 UnknownDetectorBundleTemplate {
44 bundle: crate::DetectorBundleId,
46 template: TemplateId,
48 },
49 #[error("resource limit: {0}")]
51 BooleanResource(#[from] bloq_utils::boolean::BooleanResourceError),
52 #[error("WF-7: node {node:?} references measurement {measurement} from later node {owner:?}")]
54 FutureMeasurement {
55 node: BloqNodeId,
57 owner: BloqNodeId,
59 measurement: u32,
61 },
62 #[error(
64 "WF-7: node {node:?} references measurement {measurement} from unrelated node {owner:?}"
65 )]
66 UnorderedNodeMeasurement {
67 node: BloqNodeId,
69 owner: BloqNodeId,
71 measurement: u32,
73 },
74 #[error("WF-4: node {node:?} references unknown template {template:?}")]
76 UnknownTemplate {
77 node: BloqNodeId,
79 template: TemplateId,
81 },
82 #[error("WF-4: pipe padding references unknown template {template:?}")]
84 UnknownPipePaddingTemplate {
85 template: TemplateId,
87 },
88 #[error("WF-4: one-round padding template {template:?} contains a top-level REPEAT")]
90 PipePaddingOneRoundLooped {
91 template: TemplateId,
93 },
94 #[error("WF-4: looped padding template {template:?} must contain exactly one top-level REPEAT")]
96 PipePaddingLoopedMalformed {
97 template: TemplateId,
99 },
100 #[error("WF-5: template instance id {instance:?} is used more than once")]
102 DuplicateTemplateInstance {
103 instance: TemplateInstanceId,
105 },
106 #[error("WF-6: node {node:?} references unknown template instance {instance:?}")]
108 UnknownTemplateInstance {
109 node: BloqNodeId,
111 instance: TemplateInstanceId,
113 },
114 #[error(
116 "WF-6: logical input ({},{},{}) references unknown template instance {instance:?}",
117 port.x, port.y, port.z
118 )]
119 LogicalInputUnknownInstance {
120 port: IVec3,
122 instance: TemplateInstanceId,
124 },
125 #[error("WF-6: logical output at {port:?} references unknown instance {instance:?}")]
127 LogicalOutputUnknownInstance {
128 port: IVec3,
130 instance: TemplateInstanceId,
132 },
133 #[error(
135 "WF-6: node {node:?} references unavailable measurement {measurement} in instance {instance:?}"
136 )]
137 UnknownInstanceMeasurement {
138 node: BloqNodeId,
140 instance: TemplateInstanceId,
142 measurement: u32,
144 },
145 #[error("WF-6: template {template:?} references unreachable repeat body {body:?}")]
147 UnknownTemplateBody {
148 template: TemplateId,
150 body: BodyId,
152 },
153 #[error("WF-6: template {template:?} references unavailable measurement {measurement}")]
155 UnknownTemplateMeasurement {
156 template: TemplateId,
158 measurement: u32,
160 },
161 #[error("WF-6: template {template:?} references unavailable loop state {state:?}")]
163 UnknownTemplateLoopState {
164 template: TemplateId,
166 state: LoopStateId,
168 },
169 #[error("WF-6: node {node:?} parity references loop state {state:?}")]
171 NodeLoopStateUnsupported {
172 node: BloqNodeId,
174 state: LoopStateId,
176 },
177 #[error("WF-6: template {template:?} declares loop state {state:?} more than once")]
179 DuplicateTemplateLoopState {
180 template: TemplateId,
182 state: LoopStateId,
184 },
185 #[error("WF-9: node {node:?} {context}: {source}", context = if .source.is_resource_limited() {
187 "hit a resource limit while merging template instances"
188 } else {
189 "template instances do not merge"
190 })]
191 InvalidInstanceMergeStructure {
192 node: BloqNodeId,
194 source: NodeTemplateInstanceMergeError,
196 },
197 #[error("WF-9: template {template:?} {context}: {source}", context = if .source.is_resource_limited() {
199 "hit a resource limit while checking circuit structure"
200 } else {
201 "has invalid circuit structure"
202 })]
203 InvalidTemplateCircuit {
204 template: TemplateId,
206 source: NodeTemplateInstanceMergeError,
208 },
209 #[error("WF-2: node {node:?} has multiple value producers on input slot {slot}")]
211 DuplicateValueSlot {
212 node: BloqNodeId,
214 slot: u32,
216 },
217 #[error("WF-3: node {node:?} has multiple quantum outputs for one face")]
219 QuantumEdgeFanout {
220 node: BloqNodeId,
222 },
223 #[error("WF-10: region {node:?} has no value input for predicate slot {slot}")]
225 MissingRegionSelectorInput {
226 node: BloqNodeId,
228 slot: u32,
230 },
231 #[error("WF-1: graph contains a cycle")]
233 CyclicGraph,
234 #[error("WF-11: classical node {node:?} has no value input for slot {slot}")]
236 MissingClassicalInput {
237 node: BloqNodeId,
239 slot: u32,
241 },
242 #[error("WF-11: classical node {node:?} does not read value input slot {slot}")]
244 UnusedClassicalInput {
245 node: BloqNodeId,
247 slot: u32,
249 },
250 #[error("WF-8: Discard node {node:?} is inside a RepeatUntilSuccess body")]
252 DiscardInsideRepeatUntilSuccess {
253 node: BloqNodeId,
255 },
256 #[error("WF-8: RepeatUntilSuccess node {node:?} is nested")]
258 NestedRepeatUntilSuccess {
259 node: BloqNodeId,
261 },
262 #[error("WF-13: Observable node {node:?} reuses index {index}")]
264 DuplicateObservableIndex {
265 node: BloqNodeId,
267 index: u32,
269 },
270 #[error("WF-8: node {node:?} has restart syndromes outside RepeatUntilSuccess")]
272 RestartOutsideRepeatUntilSuccess {
273 node: BloqNodeId,
275 },
276 #[error(
278 "WF-15: output frame ({},{},{}) references non-top-level-Compute node {node:?}",
279 port.x, port.y, port.z
280 )]
281 OutputFrameNotCompute {
282 port: IVec3,
284 node: BloqNodeId,
286 },
287 #[error(
289 "WF-15: output frame ({},{},{}) needs one X and one Z stamp",
290 port.x, port.y, port.z
291 )]
292 OutputFrameIncomplete {
293 port: IVec3,
295 },
296 #[error("WF-12: quantum node {node:?} has value input slot {slot}")]
298 UnusedQuantumValueInput {
299 node: BloqNodeId,
301 slot: u32,
303 },
304 #[error("{rule}: invalid classical activation on node {node:?}: {reason}")]
311 InvalidActivation {
312 node: BloqNodeId,
314 rule: &'static str,
316 reason: &'static str,
318 },
319 #[error("WF-3: invalid quantum seam guard {guard:?} before node {node:?}")]
321 InvalidSeamGuard {
322 node: BloqNodeId,
324 guard: crate::ValueRef,
326 },
327 #[error("WF-7: active node {node:?} can reference deselected instance {instance:?}")]
329 ConditionalReferenceUnavailable {
330 node: BloqNodeId,
332 instance: TemplateInstanceId,
334 },
335 #[error("WF-11: node {node:?} reads input slot {slot} more than once")]
337 DuplicateExprInput {
338 node: BloqNodeId,
340 slot: u32,
342 },
343 #[error("WF-16: node {node:?} has a value edge from non-value producer {producer:?}")]
345 InvalidValueProducer {
346 node: BloqNodeId,
348 producer: BloqNodeId,
350 },
351 #[error(
353 "WF-16: observable-fold edge {producer:?} -> {node:?} must carry a bit leaf into an Observable"
354 )]
355 InvalidObservableFold {
356 node: BloqNodeId,
358 producer: BloqNodeId,
360 },
361 #[error(
363 "WF-15: output frame ({},{},{}) has duplicate basis stamps",
364 port.x, port.y, port.z
365 )]
366 DuplicateOutputFrameStamp {
367 port: IVec3,
369 },
370 #[error(
372 "WF-10: RepeatUntilSuccess region {node:?} has invalid restart source {restart_source:?}"
373 )]
374 InvalidRestartSource {
375 node: BloqNodeId,
377 restart_source: crate::ValueRef,
379 },
380 #[error("WF-17: declared result {node:?} must name a live bit producer")]
382 InvalidValueOutput {
383 node: BloqNodeId,
385 },
386 #[error("WF-17: declared boundary output {node:?} must name a live Observable or region")]
388 InvalidBoundaryOutput {
389 node: BloqNodeId,
391 },
392 #[error("WF-17: boundary output {node:?} is declared more than once")]
394 DuplicateBoundaryOutput {
395 node: BloqNodeId,
397 },
398 #[error("WF-20: quantum node {node:?} has an invalid source-layer timeline")]
400 InvalidQuantumTimeline {
401 node: BloqNodeId,
403 },
404}
405
406impl BloqValidationError {
407 #[must_use]
410 pub fn is_resource_limited(&self) -> bool {
411 match self {
412 Self::BooleanResource(_) => true,
413 Self::InvalidInstanceMergeStructure { source, .. }
414 | Self::InvalidTemplateCircuit { source, .. } => source.is_resource_limited(),
415 _ => false,
416 }
417 }
418}
419
420#[derive(Debug, Clone, Default)]
426pub struct ValidatedPlans {
427 plans: FxMap<LevelPath, FxMap<BloqNodeId, NodeEmissionPlan>>,
428 memberships: FxMap<LevelPath, FxSet<BloqNodeId>>,
429}
430
431impl ValidatedPlans {
432 fn insert(&mut self, path: LevelPath, node: BloqNodeId, plan: NodeEmissionPlan) {
433 self.plans.entry(path).or_default().insert(node, plan);
434 }
435
436 #[must_use]
438 pub fn get(&self, path: &LevelPath, node: BloqNodeId) -> Option<&NodeEmissionPlan> {
439 self.plans.get(path)?.get(&node)
440 }
441
442 pub fn has_membership(&self, path: &LevelPath, node: BloqNodeId) -> bool {
444 self.memberships
445 .get(path)
446 .is_some_and(|nodes| nodes.contains(&node))
447 }
448
449 pub fn iter(&self) -> impl Iterator<Item = ((&LevelPath, BloqNodeId), &NodeEmissionPlan)> {
451 self.plans
452 .iter()
453 .flat_map(|(path, plans)| plans.iter().map(move |(&node, plan)| ((path, node), plan)))
454 }
455
456 #[must_use]
458 pub fn len(&self) -> usize {
459 self.plans.values().map(FxMap::len).sum::<usize>()
460 + self.memberships.values().map(FxSet::len).sum::<usize>()
461 }
462
463 #[must_use]
465 pub fn is_empty(&self) -> bool {
466 self.plans.is_empty() && self.memberships.is_empty()
467 }
468}
469
470fn valid_value_source(graph: &SubGraph, value: crate::ValueRef) -> bool {
471 graph
472 .node(value.node)
473 .is_some_and(|node| match value.output {
474 crate::ObservableOutput::Flip => matches!(
475 node.try_classical(),
476 Some(ClassicalNode::Observable { index: Some(_), .. })
477 ),
478 crate::ObservableOutput::Corrected => {
479 node.try_region().is_some()
480 || matches!(
481 node.try_classical(),
482 Some(ClassicalNode::Compute { .. } | ClassicalNode::Observable { .. })
483 )
484 }
485 })
486}
487
488fn consumes_observable_folds(kind: &BloqNodeKind) -> bool {
489 matches!(kind.try_classical(), Some(ClassicalNode::Observable { .. }))
490}
491
492fn validate_edge_cardinality(
495 graph: &SubGraph,
496 predicates: &mut crate::membership::PredicateAnalysis<'_>,
497) -> Result<(), BloqValidationError> {
498 for id in graph.node_ids() {
499 let mut seen_slots = BTreeSet::new();
500 for input in graph.incoming(id).filter_map(|edge| match edge.edge {
501 BloqEdge::Value { slot, role, output } => Some(crate::ValueInput {
502 slot: *slot,
503 producer: edge.source,
504 role,
505 output: Some(*output),
506 }),
507 BloqEdge::Compose { slot, role } => Some(crate::ValueInput {
508 slot: *slot,
509 producer: edge.source,
510 role,
511 output: None,
512 }),
513 _ => None,
514 }) {
515 if !seen_slots.insert(input.slot) {
516 return Err(BloqValidationError::DuplicateValueSlot {
517 node: id,
518 slot: input.slot,
519 });
520 }
521 let consumer = &graph[id];
522 let producer = &graph[input.producer].kind;
523 let valid_source = if let Some(output) = input.output {
524 valid_value_source(
525 graph,
526 crate::ValueRef {
527 node: input.producer,
528 output,
529 },
530 )
531 } else {
532 consumer.activation != Some(input.slot)
533 && consumes_observable_folds(&consumer.kind)
534 && (matches!(
535 producer.try_classical(),
536 Some(ClassicalNode::Observable { .. })
537 ) || matches!(producer, BloqNodeKind::Region(_)))
538 };
539 if !valid_source {
540 return Err(BloqValidationError::InvalidValueProducer {
541 node: id,
542 producer: input.producer,
543 });
544 }
545 if consumer.activation == Some(input.slot) && input.role != &ValueRole::Data {
546 return Err(BloqValidationError::InvalidActivation {
547 node: id,
548 rule: "WF-16",
549 reason: "activation is not an observable fold",
550 });
551 }
552 if matches!(
554 input.role,
555 ValueRole::FeedbackFold { .. } | ValueRole::ReadoutFold
556 ) && (!consumes_observable_folds(&consumer.kind) || input.output.is_none())
557 {
558 return Err(BloqValidationError::InvalidObservableFold {
559 node: id,
560 producer: input.producer,
561 });
562 }
563 }
564 let mut seen_faces: FxMap<[i32; 3], Vec<Option<crate::ValueRef>>> = FxMap::default();
565 for edge in graph.outgoing(id) {
566 let BloqEdge::Quantum(quantum) = edge.edge else {
567 continue;
568 };
569 if let Some(guard) = quantum.guard
570 && (!valid_value_source(graph, guard) || !graph.has_path(guard.node, edge.target))
571 {
572 return Err(BloqValidationError::InvalidSeamGuard {
573 node: edge.target,
574 guard,
575 });
576 }
577 for seam in edge.edge.pipes() {
578 let guards = seen_faces.entry(seam.pipe.src.to_array()).or_default();
579 for &other in guards.iter() {
580 if predicates
581 .overlap_values(other, quantum.guard)
582 .map_err(|error| BloqValidationError::InvalidInstanceMergeStructure {
583 node: id,
584 source: error,
585 })?
586 {
587 return Err(BloqValidationError::QuantumEdgeFanout { node: id });
588 }
589 }
590 guards.push(quantum.guard);
591 }
592 }
593 }
594 Ok(())
595}
596
597impl Bloq {
598 pub fn validate(&self) -> Result<(), BloqValidationError> {
615 self.validate_with_options(&InstantiationOptions::default())
616 }
617
618 pub fn validate_with_options(
626 &self,
627 options: &InstantiationOptions<'_>,
628 ) -> Result<(), BloqValidationError> {
629 self.validate_inner(options, &mut None)
630 }
631
632 pub fn validate_with_plans(
655 &self,
656 options: &InstantiationOptions<'_>,
657 ) -> Result<ValidatedPlans, BloqValidationError> {
658 let mut plans = Some(ValidatedPlans::default());
659 self.validate_inner(options, &mut plans)?;
660 Ok(plans.expect("emission plan collection was requested"))
661 }
662
663 pub fn emission_plans(
682 &self,
683 options: &InstantiationOptions<'_>,
684 ) -> Result<ValidatedPlans, BloqValidationError> {
685 let mut plans = ValidatedPlans::default();
686 for (path, level) in self.levels() {
687 for (id, node) in level.nodes() {
688 let Some(quantum) = node.try_quantum() else {
689 continue;
690 };
691 if !quantum.guards.is_empty() {
692 plans
693 .memberships
694 .entry(path.clone())
695 .or_default()
696 .insert(id);
697 continue;
698 }
699 let plan = node
700 .emission_plan_with_options(self.templates(), *options)
701 .map_err(
702 |source| BloqValidationError::InvalidInstanceMergeStructure {
703 node: id,
704 source,
705 },
706 )?;
707 plans.insert(path.clone(), id, plan);
708 }
709 }
710 Ok(plans)
711 }
712
713 fn validate_inner(
715 &self,
716 options: &InstantiationOptions<'_>,
717 plans: &mut Option<ValidatedPlans>,
718 ) -> Result<(), BloqValidationError> {
719 let mut work = BooleanDecisionDiagram::with_limits(options.boolean_limits());
720 work.charge(
721 self.logical_inputs()
722 .len()
723 .saturating_add(self.logical_outputs().len()),
724 )?;
725 for (_, level) in self.levels() {
727 work.charge(level.node_count().saturating_add(level.edge_count()))?;
728 for (_, quantum) in level.quantum_nodes() {
729 work.charge(
730 quantum
731 .instances
732 .len()
733 .saturating_add(quantum.guards.len())
734 .saturating_add(quantum.detectors.len())
735 .saturating_add(quantum.detector_bundles.len())
736 .saturating_add(quantum.restarts.len()),
737 )?;
738 }
739 }
740 let template_measurements = self
742 .templates()
743 .iter()
744 .map(|(template_id, template)| validate_template(template_id, template, &mut work))
745 .collect::<Result<Vec<_>, _>>()?;
746 for (bundle_id, bundle) in self.detector_bundles().iter() {
749 work.charge(
750 bundle
751 .detectors()
752 .len()
753 .saturating_add(bundle.owner_templates().len()),
754 )?;
755 for &template in bundle.owner_templates() {
756 if self.templates().get(template).is_none() {
757 return Err(BloqValidationError::UnknownDetectorBundleTemplate {
758 bundle: bundle_id,
759 template,
760 });
761 }
762 }
763 for row in bundle.detectors() {
764 work.charge(row.parity.terms().len().saturating_add(1))?;
765 }
766 bundle.used_owners()?;
767 for row in bundle.detectors() {
768 for term in row.parity.terms() {
769 let term = match term {
770 DetectorTerm::Measurement(term) => term,
771 DetectorTerm::LoopState(state) => {
772 return Err(BloqValidationError::DetectorBundleLoopStateUnsupported {
773 bundle: bundle_id,
774 state: *state,
775 });
776 }
777 };
778 let template = bundle.owner_templates()[term.owner as usize];
779 if !template_measurements
780 .get(template.0 as usize)
781 .is_some_and(|records| records.contains(term.measurement))
782 {
783 return Err(BloqValidationError::UnknownTemplateMeasurement {
784 template,
785 measurement: term.measurement,
786 });
787 }
788 }
789 }
790 }
791 let all_instances = collect_all_instances(self)?;
792 for (_, level) in self.levels() {
793 for (_, quantum) in level.quantum_nodes() {
794 work.charge(quantum.detector_bundles.len())?;
795 for use_ in &quantum.detector_bundles {
796 work.charge(use_.instances.len())?;
797 }
798 self.check_detector_bundle_bindings(quantum, |instance| {
799 all_instances.get(&instance).copied()
800 })?;
801 }
802 }
803
804 for entry in self.pipe_padding() {
807 work.charge(1)?;
808 for template in [entry.one_round, entry.looped] {
809 if self.templates().get(template).is_none() {
810 return Err(BloqValidationError::UnknownPipePaddingTemplate { template });
811 }
812 }
813 if !self.templates()[entry.one_round]
814 .circuit
815 .entry_top_level_repeats()
816 .is_empty()
817 {
818 return Err(BloqValidationError::PipePaddingOneRoundLooped {
819 template: entry.one_round,
820 });
821 }
822 if self.templates()[entry.looped]
823 .circuit
824 .entry_top_level_repeats()
825 .len()
826 != 1
827 {
828 return Err(BloqValidationError::PipePaddingLoopedMalformed {
829 template: entry.looped,
830 });
831 }
832 }
833
834 if let Some(input) = self
837 .logical_inputs()
838 .iter()
839 .find(|input| !all_instances.contains_key(&input.instance))
840 {
841 return Err(BloqValidationError::LogicalInputUnknownInstance {
842 port: input.port,
843 instance: input.instance,
844 });
845 }
846
847 if let Some(output) = self
848 .logical_outputs()
849 .iter()
850 .find(|output| !all_instances.contains_key(&output.instance))
851 {
852 return Err(BloqValidationError::LogicalOutputUnknownInstance {
853 port: output.port,
854 instance: output.instance,
855 });
856 }
857
858 let mut frame_stamps: FxMap<IVec3, (bool, bool)> = FxMap::default();
860 for (path, level) in self.levels() {
861 for (id, weight) in level.nodes() {
862 if let Some(timeline) = weight
863 .try_quantum()
864 .and_then(|quantum| quantum.timeline.as_ref())
865 && !valid_quantum_timeline(timeline, &weight.provenance)
866 {
867 return Err(BloqValidationError::InvalidQuantumTimeline { node: id });
868 }
869 let crate::NodeProvenance::OutputFrame { port, basis } = &weight.provenance else {
870 continue;
871 };
872 if !path.segments().is_empty()
873 || !matches!(weight.try_classical(), Some(ClassicalNode::Compute { .. }))
874 {
875 return Err(BloqValidationError::OutputFrameNotCompute {
876 port: *port,
877 node: id,
878 });
879 }
880 let entry = frame_stamps.entry(*port).or_default();
881 let seen = match basis {
882 crate::Basis::X => &mut entry.0,
883 crate::Basis::Z => &mut entry.1,
884 };
885 if std::mem::replace(seen, true) {
886 return Err(BloqValidationError::DuplicateOutputFrameStamp { port: *port });
887 }
888 }
889 }
890 let incomplete_stamp_pair = frame_stamps
891 .iter()
892 .filter(|(_, (x, z))| x != z)
893 .map(|(&port, _)| port);
894 let missing_logical_output_pair = self.logical_outputs().iter().filter_map(|logical| {
895 (frame_stamps.get(&logical.port).copied() != Some((true, true))).then_some(logical.port)
896 });
897 let incomplete = incomplete_stamp_pair
898 .chain(missing_logical_output_pair)
899 .min_by_key(|port| (port.x, port.y, port.z));
900 if let Some(port) = incomplete {
901 return Err(BloqValidationError::OutputFrameIncomplete { port });
902 }
903
904 let mut observable_indices = FxSet::default();
906 validate_graph(
907 self.top(),
908 &LevelPath::default(),
909 self.templates(),
910 self.detector_bundles(),
911 &all_instances,
912 &template_measurements,
913 &mut observable_indices,
914 *options,
915 plans,
916 &mut MergeProofCache::default(),
917 &mut work,
918 ValidationCtx {
919 in_rus: false,
920 pool_has_restarts: self
922 .templates()
923 .iter()
924 .any(|(_, template)| !template.restarts.is_empty()),
925 },
926 )?;
927 self.validate_membership_references(*options, &mut work)?;
928 Ok(())
929 }
930
931 fn validate_membership_references(
932 &self,
933 options: InstantiationOptions<'_>,
934 work: &mut BooleanDecisionDiagram,
935 ) -> Result<(), BloqValidationError> {
936 let external_instances: FxSet<_> = self
937 .logical_inputs()
938 .iter()
939 .map(|input| input.instance)
940 .chain(self.logical_outputs().iter().map(|output| output.instance))
941 .collect();
942 let mut nested_cut_owners = FxMap::default();
943 let mut availability = FxMap::default();
944 for (path, level) in self.levels() {
945 for (id, quantum) in level.quantum_nodes() {
946 if !path.is_top_level() {
947 for instance in &quantum.instances {
948 if external_instances.contains(&instance.id) {
949 nested_cut_owners.insert(instance.id, path.clone());
950 }
951 }
952 }
953 for guard in &quantum.guards {
954 let producer = level
955 .value_inputs(id)
956 .find(|input| input.slot == guard.input)
957 .expect("validated registration input")
958 .value_ref()
959 .expect("runtime input");
960 for &instance in &guard.instances {
961 availability.insert(instance, (path.clone(), producer));
962 }
963 }
964 }
965 }
966 let mut predicates = FxMap::default();
967 for instance in self
970 .logical_inputs()
971 .iter()
972 .map(|input| input.instance)
973 .chain(self.logical_outputs().iter().map(|output| output.instance))
974 {
975 if let Some(owner_path) = nested_cut_owners.get(&instance) {
976 let mut path = LevelPath::default();
977 for segment in owner_path.segments() {
978 let level = self.level_at(&path).expect("cut owner ancestor");
979 let node = &level[segment.region];
980 let relation = predicates.entry(path.clone()).or_insert_with(|| {
981 crate::membership::PredicateAnalysis::with_limits(
982 level,
983 options.boolean_limits(),
984 )
985 });
986 let invalid = |source| BloqValidationError::InvalidInstanceMergeStructure {
987 node: segment.region,
988 source,
989 };
990 let active = if let Some(slot) = node.activation {
991 let guard = level
992 .value_inputs(segment.region)
993 .find(|input| input.slot == slot)
994 .expect("validated ancestor activation")
995 .value_ref()
996 .expect("runtime input");
997 relation
998 .using_work(work, |relation| relation.implies_values(None, guard))
999 .map_err(invalid)?
1000 } else {
1001 true
1002 };
1003 if !active {
1005 return Err(invalid(NodeTemplateInstanceMergeError::InvalidMembership(
1006 format!(
1007 "external logical cut i{} is not always selected",
1008 instance.0
1009 ),
1010 )));
1011 }
1012 path = path.child(segment.region, segment.body);
1013 }
1014 }
1015 if let Some((path, guard)) = availability.get(&instance) {
1016 let relation = predicates.entry(path.clone()).or_insert_with(|| {
1017 crate::membership::PredicateAnalysis::with_limits(
1018 self.level_at(path).expect("owner level"),
1019 options.boolean_limits(),
1020 )
1021 });
1022 let invalid = |source| BloqValidationError::InvalidInstanceMergeStructure {
1023 node: guard.node,
1024 source,
1025 };
1026 if !relation
1027 .using_work(work, |relation| relation.implies_values(None, *guard))
1028 .map_err(invalid)?
1029 {
1030 return Err(invalid(NodeTemplateInstanceMergeError::InvalidMembership(
1031 format!(
1032 "external logical cut i{} is not always selected",
1033 instance.0
1034 ),
1035 )));
1036 }
1037 }
1038 }
1039 if availability.is_empty() {
1040 return Ok(());
1041 }
1042 for (path, level) in self.levels() {
1043 let mut check =
1044 |node: BloqNodeId, guards: &[crate::ValueRef], instance: TemplateInstanceId| {
1045 let Some((owner_path, required)) = availability.get(&instance) else {
1046 return Ok(());
1047 };
1048 let mut gates = Vec::new();
1049 let compatible = path.segments().starts_with(owner_path.segments());
1050 if compatible {
1051 if *owner_path == path {
1052 gates.extend(guards.iter().copied());
1053 } else {
1054 let level = self.level_at(owner_path).expect("registered owner level");
1055 let region = path.segments()[owner_path.segments().len()].region;
1056 if let Some(slot) = level[region].activation {
1057 gates.push(
1058 level
1059 .value_inputs(region)
1060 .find(|input| input.slot == slot)
1061 .expect("validated region activation")
1062 .value_ref()
1063 .expect("runtime input"),
1064 );
1065 }
1066 }
1067 }
1068 let relation = predicates.entry(owner_path.clone()).or_insert_with(|| {
1069 crate::membership::PredicateAnalysis::with_limits(
1070 self.level_at(owner_path).expect("registered owner level"),
1071 options.boolean_limits(),
1072 )
1073 });
1074 let available = compatible
1075 && relation
1076 .using_work(work, |relation| {
1077 relation.implies_all_values(&gates, *required)
1078 })
1079 .map_err(|source| {
1080 BloqValidationError::InvalidInstanceMergeStructure { node, source }
1081 })?;
1082 if !available {
1083 return Err(BloqValidationError::ConditionalReferenceUnavailable {
1084 node,
1085 instance,
1086 });
1087 }
1088 Ok(())
1089 };
1090 for (id, node) in level.nodes() {
1091 let input = |slot| {
1092 level
1093 .value_inputs(id)
1094 .find(|input| input.slot == slot)
1095 .expect("validated guard input")
1096 .value_ref()
1097 .expect("runtime input")
1098 };
1099 let guard = node.activation.map(input);
1100 if let Some(classical) = node.try_classical() {
1101 for term in classical.measurements() {
1102 check(id, guard.as_slice(), term.instance)?;
1103 }
1104 for operator in classical.operators() {
1105 check(id, guard.as_slice(), operator.instance)?;
1106 }
1107 }
1108 if let Some(quantum) = node.try_quantum() {
1109 for (index, detector) in quantum.detectors.iter().enumerate() {
1110 let guard = quantum
1111 .guards
1112 .iter()
1113 .find(|guard| guard.detectors.contains(&(index as u32)))
1114 .map(|guard| input(guard.input));
1115 for term in detector.parity.measurements() {
1116 check(id, guard.as_slice(), term.instance)?;
1117 }
1118 }
1119 for (index, use_) in quantum.detector_bundles.iter().enumerate() {
1120 let bundle = self
1121 .detector_bundles()
1122 .get(use_.bundle)
1123 .ok_or(crate::DetectorBundleError::UnknownBundle(use_.bundle))?;
1124 let guard = quantum
1125 .guards
1126 .iter()
1127 .find(|guard| guard.detector_bundles.contains(&(index as u32)))
1128 .map(|guard| input(guard.input));
1129 for &owner in bundle.used_owners()? {
1130 check(id, guard.as_slice(), use_.instances[owner as usize])?;
1131 }
1132 }
1133 for (index, restart) in quantum.restarts.iter().enumerate() {
1134 let guard = quantum
1135 .guards
1136 .iter()
1137 .find(|guard| guard.restarts.contains(&(index as u32)))
1138 .map(|guard| input(guard.input));
1139 for term in restart.parity.measurements() {
1140 check(id, guard.as_slice(), term.instance)?;
1141 }
1142 }
1143 for contribution in &quantum.guards {
1144 for (rows, restart) in [
1145 (&contribution.detector_parities, false),
1146 (&contribution.restart_parities, true),
1147 ] {
1148 for (index, parity) in rows {
1149 let mut guards = vec![input(contribution.input)];
1150 if let Some(guard) = quantum.guards.iter().find(|guard| {
1151 if restart {
1152 guard.restarts.contains(index)
1153 } else {
1154 guard.detectors.contains(index)
1155 }
1156 }) {
1157 guards.push(input(guard.input));
1158 }
1159 for term in parity.measurements() {
1160 check(id, &guards, term.instance)?;
1161 }
1162 }
1163 }
1164 }
1165 }
1166 }
1167 }
1168 Ok(())
1169 }
1170}
1171
1172pub(crate) fn valid_quantum_timeline(
1173 timeline: &crate::QuantumTimeline,
1174 provenance: &crate::NodeProvenance,
1175) -> bool {
1176 let crate::NodeProvenance::BlockComponent { members } = provenance else {
1177 return false;
1178 };
1179 let Some(min_member_z) = members.iter().map(|member| member.pos.z).min() else {
1180 return false;
1181 };
1182 let ends = &timeline.layer_round_ends;
1183 let valid_rounds = ends.len() >= 2
1184 && ends.last().is_some_and(|&last| last > 0)
1185 && ends.windows(2).all(|pair| pair[0] <= pair[1]);
1186 let valid_span = ends
1187 .len()
1188 .checked_sub(1)
1189 .and_then(|span| i64::try_from(span).ok())
1190 .and_then(|span| i64::from(min_member_z).checked_add(span))
1191 .is_some_and(|z_end| z_end <= i64::from(i32::MAX));
1192 valid_rounds && valid_span
1193}
1194
1195fn collect_all_instances(
1197 bloq: &Bloq,
1198) -> Result<FxMap<TemplateInstanceId, TemplateId>, BloqValidationError> {
1199 let mut instances = FxMap::default();
1200 for (_, level) in bloq.levels() {
1201 for (_, quantum) in level.quantum_nodes() {
1202 for instance in &quantum.instances {
1203 if bloq.templates().get(instance.template_id).is_some()
1204 && instances
1205 .insert(instance.id, instance.template_id)
1206 .is_some()
1207 {
1208 return Err(BloqValidationError::DuplicateTemplateInstance {
1209 instance: instance.id,
1210 });
1211 }
1212 }
1213 }
1214 }
1215 Ok(instances)
1216}
1217
1218#[derive(Clone, Copy)]
1219struct ValidationCtx {
1220 in_rus: bool,
1222 pool_has_restarts: bool,
1224}
1225
1226#[derive(Default)]
1230struct MergeProofCache {
1231 shapes: FxSet<Vec<(TemplateId, [i64; 2])>>,
1232 placements: FxSet<(TemplateId, IVec2)>,
1233}
1234
1235impl MergeProofCache {
1236 fn validate(
1237 &mut self,
1238 id: BloqNodeId,
1239 node: &BloqNode,
1240 templates: &BloqTemplatePool,
1241 options: InstantiationOptions<'_>,
1242 work: &mut BooleanDecisionDiagram,
1243 ) -> Result<(), BloqValidationError> {
1244 let instances = &node.expect_quantum().instances;
1245 work.charge(instances.len().saturating_add(1))?;
1246 let origin = instances
1247 .first()
1248 .map_or(IVec2::ZERO, |instance| instance.offset);
1249 let shape = instances
1251 .iter()
1252 .map(|instance| {
1253 (
1254 instance.template_id,
1255 [
1256 i64::from(instance.offset.x) - i64::from(origin.x),
1257 i64::from(instance.offset.y) - i64::from(origin.y),
1258 ],
1259 )
1260 })
1261 .collect::<Vec<_>>();
1262 let merge_error =
1263 |source| BloqValidationError::InvalidInstanceMergeStructure { node: id, source };
1264 if self.shapes.contains(&shape) {
1265 for instance in instances {
1266 let placement = (instance.template_id, instance.offset);
1267 work.charge(1)?;
1268 if self.placements.contains(&placement) {
1269 continue;
1270 }
1271 let qubits = templates[instance.template_id].qubits();
1272 work.charge(qubits.len())?;
1273 for &coordinate in qubits {
1276 bloq_circuit::checked_translate_coordinate(coordinate, instance.offset)
1277 .map_err(NodeTemplateInstanceMergeError::from)
1278 .map_err(merge_error)?;
1279 }
1280 self.placements.insert(placement);
1281 }
1282 return Ok(());
1283 }
1284
1285 for instance in instances {
1286 work.charge(crate::instantiation::circuit_work(
1287 &templates[instance.template_id].circuit,
1288 ))?;
1289 }
1290 node.emission_plan_with_options(templates, options)
1293 .map_err(merge_error)?;
1294 self.shapes.insert(shape);
1295 self.placements.extend(
1296 instances
1297 .iter()
1298 .map(|instance| (instance.template_id, instance.offset)),
1299 );
1300 Ok(())
1301 }
1302}
1303
1304fn validate_outputs(graph: &SubGraph) -> Result<(), BloqValidationError> {
1306 if let Some(value) = graph.value_output()
1307 && !valid_value_source(graph, value)
1308 {
1309 return Err(BloqValidationError::InvalidValueOutput { node: value.node });
1310 }
1311 let mut seen = FxSet::default();
1312 for &node in graph.boundary_outputs() {
1313 if !graph.node(node).is_some_and(|node| {
1314 node.try_region().is_some()
1315 || matches!(node.try_classical(), Some(ClassicalNode::Observable { .. }))
1316 }) {
1317 return Err(BloqValidationError::InvalidBoundaryOutput { node });
1318 }
1319 if !seen.insert(node) {
1320 return Err(BloqValidationError::DuplicateBoundaryOutput { node });
1321 }
1322 }
1323 Ok(())
1324}
1325
1326#[expect(
1330 clippy::too_many_arguments,
1331 reason = "whole-level validation shares scoped registries and bounded-work state"
1332)]
1333fn validate_graph(
1334 graph: &SubGraph,
1335 path: &LevelPath,
1336 templates: &BloqTemplatePool,
1337 bundles: &crate::DetectorBundlePool,
1338 all_instances: &FxMap<TemplateInstanceId, TemplateId>,
1339 template_measurements: &[&MeasurementSet],
1340 observable_indices: &mut FxSet<u32>,
1341 options: InstantiationOptions<'_>,
1342 plans: &mut Option<ValidatedPlans>,
1343 merge_proofs: &mut MergeProofCache,
1344 work: &mut BooleanDecisionDiagram,
1345 ctx: ValidationCtx,
1346) -> Result<(), BloqValidationError> {
1347 work.charge(graph.node_count().saturating_add(graph.edge_count()))?;
1348 let order = graph
1350 .deterministic_emit_order()
1351 .map_err(|_| BloqValidationError::CyclicGraph)?;
1352 let mut predicates =
1353 crate::membership::PredicateAnalysis::with_limits(graph, options.boolean_limits());
1354 predicates.charge(work.steps()).map_err(|source| {
1355 BloqValidationError::InvalidInstanceMergeStructure {
1356 node: order.first().copied().unwrap_or(BloqNodeId(0)),
1357 source,
1358 }
1359 })?;
1360 validate_edge_cardinality(graph, &mut predicates)?;
1361 work.charge(predicates.steps().saturating_sub(work.steps()))?;
1362 validate_outputs(graph)?;
1363 let order_index = order
1364 .iter()
1365 .enumerate()
1366 .map(|(index, &id)| (id, index))
1367 .collect::<FxMap<_, _>>();
1368
1369 let mut instances = FxMap::default();
1370 for &id in &order {
1371 let node = &graph[id];
1372 let Some(quantum) = node.try_quantum() else {
1373 continue; };
1375 for instance in &quantum.instances {
1376 if templates.get(instance.template_id).is_none() {
1377 return Err(BloqValidationError::UnknownTemplate {
1378 node: id,
1379 template: instance.template_id,
1380 });
1381 }
1382 assert!(
1383 instances
1384 .insert(
1385 instance.id,
1386 InstanceValidation {
1387 template: instance.template_id,
1388 owner: id,
1389 order: order_index[&id],
1390 },
1391 )
1392 .is_none(),
1393 "the whole-program instance preflight rejected duplicate ids"
1394 );
1395 }
1396 }
1397
1398 let mut path_scratch = graph.path_scratch();
1399 let mut verified_paths = FxSet::default();
1400 for (id, node) in graph.nodes() {
1401 if let Some(slot) = node.activation {
1402 if node.try_quantum().is_some() {
1403 return Err(BloqValidationError::InvalidActivation {
1404 node: id,
1405 rule: "WF-12",
1406 reason: "quantum nodes use member registrations",
1407 });
1408 }
1409 if !graph.value_inputs(id).any(|input| input.slot == slot) {
1410 return Err(BloqValidationError::MissingClassicalInput { node: id, slot });
1411 }
1412 }
1413 let consumer_order = order_index[&id];
1414 if let Some(classical) = node.try_classical() {
1415 work.charge(classical.measurements().len())?;
1418 validate_instance_measurement_refs(
1419 id,
1420 classical.measurements().iter().copied(),
1421 &instances,
1422 all_instances,
1423 template_measurements,
1424 consumer_order,
1425 )?;
1426 work.charge(classical.operators().len())?;
1427 for operator in classical.operators() {
1428 if !all_instances.contains_key(&operator.instance) {
1429 return Err(BloqValidationError::UnknownTemplateInstance {
1430 node: id,
1431 instance: operator.instance,
1432 });
1433 }
1434 }
1435 }
1436 if let Some(ClassicalNode::Observable {
1437 index: Some(observable),
1438 ..
1439 }) = node.try_classical()
1440 && !observable_indices.insert(*observable)
1441 {
1442 return Err(BloqValidationError::DuplicateObservableIndex {
1443 node: id,
1444 index: *observable,
1445 });
1446 }
1447 let Some(quantum) = node.try_quantum() else {
1448 continue; };
1450 for parity in quantum.stored_parities() {
1451 work.charge(parity.terms().len())?;
1452 validate_instance_parity_refs(
1453 graph,
1454 id,
1455 parity,
1456 &instances,
1457 all_instances,
1458 template_measurements,
1459 consumer_order,
1460 &mut path_scratch,
1461 &mut verified_paths,
1462 )?;
1463 }
1464 for use_ in &quantum.detector_bundles {
1465 work.charge(1)?;
1466 let bundle = bundles
1467 .get(use_.bundle)
1468 .ok_or(crate::DetectorBundleError::UnknownBundle(use_.bundle))?;
1469 for &owner in bundle.used_owners()? {
1470 work.charge(1)?;
1471 let instance = use_.instances[owner as usize];
1472 if let Some(source) = instances.get(&instance) {
1473 if source.order > consumer_order
1474 || (source.owner != id
1475 && !verified_paths.contains(&(source.owner, id))
1476 && !graph.has_path_backwards_with_scratch(
1477 source.owner,
1478 id,
1479 &mut path_scratch,
1480 ))
1481 {
1482 return Err(BloqValidationError::UnavailableDetectorBundleOwner {
1483 node: id,
1484 instance,
1485 });
1486 }
1487 verified_paths.insert((source.owner, id));
1488 }
1489 }
1490 }
1491 if !ctx.in_rus
1492 && (!quantum.restarts.is_empty()
1493 || (ctx.pool_has_restarts
1494 && quantum.instances.iter().any(|instance| {
1495 templates
1496 .get(instance.template_id)
1497 .is_some_and(|template| !template.restarts.is_empty())
1498 })))
1499 {
1500 return Err(BloqValidationError::RestartOutsideRepeatUntilSuccess { node: id });
1501 }
1502 let merge_error =
1508 |source| BloqValidationError::InvalidInstanceMergeStructure { node: id, source };
1509 if quantum.guards.is_empty() && plans.is_none() && options.is_noiseless() {
1510 merge_proofs.validate(id, node, templates, options, work)?;
1511 } else if quantum.guards.is_empty() {
1512 work.charge(quantum.instances.len())?;
1513 for instance in &quantum.instances {
1514 work.charge(crate::instantiation::circuit_work(
1515 &templates[instance.template_id].circuit,
1516 ))?;
1517 }
1518 let plan = node
1519 .emission_plan_with_options(templates, options)
1520 .map_err(merge_error)?;
1521 if let Some(plans) = plans {
1522 plans.insert(path.clone(), id, plan);
1523 }
1524 } else {
1525 predicates
1526 .using_work(work, |predicates| {
1527 crate::membership::validate_merge(graph, id, templates, options, predicates)
1528 })
1529 .map_err(merge_error)?;
1530 if let Some(plans) = plans {
1531 plans
1532 .memberships
1533 .entry(path.clone())
1534 .or_default()
1535 .insert(id);
1536 }
1537 }
1538
1539 if let Some(input) = graph
1540 .value_inputs(id)
1541 .find(|input| !quantum.guards.iter().any(|guard| guard.input == input.slot))
1542 {
1543 return Err(BloqValidationError::UnusedQuantumValueInput {
1544 node: id,
1545 slot: input.slot,
1546 });
1547 }
1548 }
1549
1550 if ctx.in_rus
1552 && let Some((node, _)) = graph
1553 .nodes()
1554 .find(|(_, node)| matches!(node.try_classical(), Some(ClassicalNode::Discard { .. })))
1555 {
1556 return Err(BloqValidationError::DiscardInsideRepeatUntilSuccess { node });
1557 }
1558
1559 for (region_id, node) in graph.nodes() {
1560 let BloqNodeKind::Region(region) = &node.kind else {
1561 continue;
1562 };
1563 if ctx.in_rus {
1564 return Err(BloqValidationError::NestedRepeatUntilSuccess { node: region_id });
1565 }
1566 let RegionNode::RepeatUntilSuccess {
1567 body,
1568 restart_condition: predicate,
1569 restart_source,
1570 } = region;
1571 let mut read = expression_inputs(region_id, predicate, work)?;
1573 if let Some(slot) = node.activation
1574 && !read.insert(slot)
1575 {
1576 return Err(BloqValidationError::InvalidActivation {
1577 node: region_id,
1578 rule: "WF-11",
1579 reason: "activation slot is also a predicate operand",
1580 });
1581 }
1582 let fed: BTreeSet<_> = graph
1583 .value_inputs(region_id)
1584 .map(|input| input.slot)
1585 .collect();
1586
1587 if let Some(source) = restart_source
1589 && !valid_value_source(body, *source)
1590 {
1591 return Err(BloqValidationError::InvalidRestartSource {
1592 node: region_id,
1593 restart_source: *source,
1594 });
1595 }
1596
1597 if restart_source.is_none()
1600 && let Some(&slot) = read.difference(&fed).next()
1601 {
1602 return Err(BloqValidationError::MissingRegionSelectorInput {
1603 node: region_id,
1604 slot,
1605 });
1606 }
1607 if let Some(&slot) = fed.difference(&read).next() {
1608 return Err(BloqValidationError::UnusedClassicalInput {
1609 node: region_id,
1610 slot,
1611 });
1612 }
1613
1614 let body_ctx = ValidationCtx {
1615 in_rus: true,
1616 ..ctx
1617 };
1618 for (selector, body) in region.bodies() {
1619 validate_graph(
1620 body,
1621 &path.child(region_id, selector),
1622 templates,
1623 bundles,
1624 all_instances,
1625 template_measurements,
1626 observable_indices,
1627 options,
1628 plans,
1629 merge_proofs,
1630 work,
1631 body_ctx,
1632 )?;
1633 }
1634 }
1635
1636 for (id, node) in graph.nodes() {
1638 let expr = match node.try_classical() {
1639 Some(ClassicalNode::Compute { expr }) => expr,
1640 Some(ClassicalNode::Discard { condition }) => condition,
1641 _ => continue,
1642 };
1643 let mut read = expression_inputs(id, expr, work)?;
1644 if let Some(slot) = node.activation
1645 && !read.insert(slot)
1646 {
1647 return Err(BloqValidationError::InvalidActivation {
1648 node: id,
1649 rule: "WF-11",
1650 reason: "activation slot is also a data operand",
1651 });
1652 }
1653 let fed: BTreeSet<_> = graph.value_inputs(id).map(|input| input.slot).collect();
1654 if let Some(&slot) = read.difference(&fed).next() {
1655 return Err(BloqValidationError::MissingClassicalInput { node: id, slot });
1656 }
1657 if let Some(&slot) = fed.difference(&read).next() {
1658 return Err(BloqValidationError::UnusedClassicalInput { node: id, slot });
1659 }
1660 }
1661
1662 Ok(())
1663}
1664
1665fn expression_inputs(
1668 node: BloqNodeId,
1669 expr: &ClassicalExpr,
1670 work: &mut BooleanDecisionDiagram,
1671) -> Result<BTreeSet<u32>, BloqValidationError> {
1672 work.charge(1)?;
1673 let mut pending = vec![expr];
1674 let mut read = BTreeSet::new();
1675 let mut linear = true;
1676 let mut duplicate = None;
1677 while let Some(expr) = pending.pop() {
1678 work.charge(1)?;
1679 let mut insert = |slot| {
1680 if !read.insert(slot) && duplicate.is_none() {
1681 duplicate = Some(slot);
1682 }
1683 };
1684 match expr {
1685 ClassicalExpr::In(slot) => insert(*slot),
1686 ClassicalExpr::Parity { inputs, .. } => {
1687 work.charge(inputs.len())?;
1688 for &slot in inputs {
1689 insert(slot);
1690 }
1691 }
1692 ClassicalExpr::And(..) | ClassicalExpr::Or(..) | ClassicalExpr::Select(..) => {
1693 linear = false;
1694 }
1695 _ => {}
1696 }
1697 work.charge(expr.operands().len())?;
1698 pending.extend(expr.operands().iter().rev());
1699 }
1700 if linear && let Some(slot) = duplicate {
1701 return Err(BloqValidationError::DuplicateExprInput { node, slot });
1702 }
1703 Ok(read)
1704}
1705
1706#[derive(Debug, Clone, Copy)]
1707struct InstanceValidation {
1708 template: TemplateId,
1709 owner: BloqNodeId,
1710 order: usize,
1711}
1712
1713fn validate_template<'a>(
1714 template_id: TemplateId,
1715 template: &'a crate::BloqTemplate,
1716 work: &mut bloq_utils::boolean::BooleanDecisionDiagram,
1717) -> Result<&'a MeasurementSet, BloqValidationError> {
1718 charge_template_work(template_id, work, template.circuit.body_count())?;
1721 for index in 0..template.circuit.body_count() {
1722 let ops = template
1723 .circuit
1724 .body(BodyId(index as u32))
1725 .expect("allocated body")
1726 .ops();
1727 charge_template_work(template_id, work, ops.len())?;
1728 for op in ops {
1729 let terms = match op {
1730 Op::Measure {
1731 qubits,
1732 measurements,
1733 ..
1734 } => qubits.len().saturating_add(measurements.len()),
1735 Op::Gate { qubits, .. }
1736 | Op::Depolarize1 { qubits, .. }
1737 | Op::Depolarize2 { qubits, .. }
1738 | Op::PauliError { qubits, .. } => qubits.len(),
1739 Op::MPP {
1740 products,
1741 measurements,
1742 } => {
1743 charge_template_work(template_id, work, products.len())?;
1744 for product in products {
1745 charge_template_work(template_id, work, product.len())?;
1746 }
1747 measurements.len()
1748 }
1749 Op::ConditionalPauli(corrections) => corrections.len(),
1750 Op::Tick | Op::Repeat { .. } => 0,
1751 };
1752 charge_template_work(template_id, work, terms)?;
1753 }
1754 }
1755 charge_template_work(
1756 template_id,
1757 work,
1758 template.circuit.meas_registry().records().len(),
1759 )?;
1760 let analysis = template.circuit_analysis().map_err(|source| {
1761 BloqValidationError::InvalidTemplateCircuit {
1762 template: template_id,
1763 source,
1764 }
1765 })?;
1766 let produced_measurements = &analysis.produced_measurements;
1767 if template.validation_cached() {
1768 return Ok(produced_measurements);
1769 }
1770
1771 let produced = |measurement| produced_measurements.contains(measurement);
1772
1773 charge_template_work(template_id, work, template.repeat_states.len())?;
1774 charge_template_work(template_id, work, template.detectors.len())?;
1775 charge_template_work(template_id, work, template.restarts.len())?;
1776 charge_template_work(template_id, work, template.boundary_flows.len())?;
1777 for parity in template
1778 .repeat_states
1779 .iter()
1780 .flat_map(|state| [&state.initial, &state.next])
1781 .chain(template.detectors.iter().map(|detector| &detector.parity))
1782 .chain(template.restarts.iter().map(|restart| &restart.parity))
1783 {
1784 charge_template_work(template_id, work, parity.terms().len())?;
1785 }
1786 for flow in &template.boundary_flows {
1787 charge_template_work(template_id, work, flow.measurements.len())?;
1788 }
1789
1790 let mut states_by_body: FxMap<BodyId, Vec<&crate::TemplateRepeatState>> = FxMap::default();
1791 let mut state_ids =
1792 FxSet::with_capacity_and_hasher(template.repeat_states.len(), Default::default());
1793 for state in &template.repeat_states {
1794 if !state_ids.insert(state.state) {
1795 return Err(BloqValidationError::DuplicateTemplateLoopState {
1796 template: template_id,
1797 state: state.state,
1798 });
1799 }
1800 validate_template_body_ref(template_id, state.body, &analysis.reachable_bodies)?;
1801 for parity in [&state.initial, &state.next] {
1802 validate_template_measurement_refs(template_id, parity, produced)?;
1803 }
1804 states_by_body.entry(state.body).or_default().push(state);
1805 }
1806
1807 let mut detectors_by_body: FxMap<BodyId, Vec<&crate::TemplateDetector>> = FxMap::default();
1808 for detector in &template.detectors {
1809 validate_template_measurement_refs(template_id, &detector.parity, produced)?;
1810 if let TemplateDetectorScope::RepeatBody { body } = detector.scope {
1811 validate_template_body_ref(template_id, body, &analysis.reachable_bodies)?;
1812 detectors_by_body.entry(body).or_default().push(detector);
1813 }
1814 }
1815
1816 for flow in &template.boundary_flows {
1818 for &measurement in &flow.measurements {
1819 if !produced_measurements.contains(measurement) {
1820 return Err(BloqValidationError::UnknownTemplateMeasurement {
1821 template: template_id,
1822 measurement,
1823 });
1824 }
1825 }
1826 }
1827 for restart in &template.restarts {
1828 validate_template_measurement_refs(template_id, &restart.parity, produced)?;
1829 }
1830
1831 for event in &analysis.repeat_events {
1832 for state in states_by_body.get(&event.body).into_iter().flatten() {
1833 validate_template_measurement_refs_before(
1834 template_id,
1835 &analysis.measurement_order,
1836 event.before,
1837 &state.initial,
1838 )?;
1839 validate_template_measurement_refs_before(
1840 template_id,
1841 &analysis.measurement_order,
1842 event.after,
1843 &state.next,
1844 )?;
1845 }
1846 for detector in detectors_by_body.get(&event.body).into_iter().flatten() {
1847 validate_template_measurement_refs_before(
1848 template_id,
1849 &analysis.measurement_order,
1850 event.after,
1851 &detector.parity,
1852 )?;
1853 }
1854 }
1855
1856 let available_states = validate_template_body_events(
1857 template_id,
1858 template,
1859 &analysis.body_order,
1860 &states_by_body,
1861 &detectors_by_body,
1862 work,
1863 )?;
1864 for detector in &template.detectors {
1865 if detector.scope == TemplateDetectorScope::TopLevel {
1866 validate_template_loop_state_refs(template_id, &detector.parity, &available_states)?;
1867 }
1868 }
1869 for restart in &template.restarts {
1870 validate_template_loop_state_refs(template_id, &restart.parity, &available_states)?;
1871 }
1872 template.cache_validation();
1873 Ok(produced_measurements)
1874}
1875
1876fn validate_template_body_events<'a>(
1877 template_id: TemplateId,
1878 template: &'a crate::BloqTemplate,
1879 body_order: &[BodyId],
1880 states_by_body: &FxMap<BodyId, Vec<&'a crate::TemplateRepeatState>>,
1881 detectors_by_body: &FxMap<BodyId, Vec<&'a crate::TemplateDetector>>,
1882 work: &mut bloq_utils::boolean::BooleanDecisionDiagram,
1883) -> Result<FxSet<LoopStateId>, BloqValidationError> {
1884 #[derive(Default)]
1885 struct StateEffects {
1886 required: Vec<LoopStateId>,
1888 provided: FxSet<LoopStateId>,
1889 }
1890 let mut effects = FxMap::<BodyId, StateEffects>::default();
1891 for &body in body_order {
1892 charge_template_work(template_id, work, 1)?;
1893 let mut effect = StateEffects::default();
1894 let mut required = FxSet::default();
1895 let mut require = |state| {
1896 if required.insert(state) {
1897 effect.required.push(state);
1898 }
1899 };
1900 for op in template
1901 .circuit
1902 .body(body)
1903 .expect("preflight checked bodies")
1904 .ops()
1905 {
1906 charge_template_work(template_id, work, 1)?;
1907 let Op::Repeat {
1908 body: child,
1909 repetitions,
1910 } = op
1911 else {
1912 continue;
1913 };
1914 if *repetitions == 0 {
1915 continue;
1916 }
1917 let states = states_by_body.get(child);
1918 let child_effect = &effects[child];
1919 charge_template_work(template_id, work, child_effect.required.len())?;
1920 charge_template_work(template_id, work, states.map_or(0, Vec::len))?;
1921 let declared: FxSet<_> = states
1922 .into_iter()
1923 .flatten()
1924 .map(|state| state.state)
1925 .collect();
1926 for state in states.into_iter().flatten() {
1929 charge_template_work(template_id, work, state.initial.terms().len())?;
1930 for term in state.initial.terms() {
1931 if let DetectorTerm::LoopState(state) = *term
1932 && !effect.provided.contains(&state)
1933 {
1934 require(state);
1935 }
1936 }
1937 }
1938 let in_iteration =
1939 |state| effect.provided.contains(&state) || declared.contains(&state);
1940 for &state in &child_effect.required {
1941 if !in_iteration(state) {
1942 require(state);
1943 }
1944 }
1945 for parity in detectors_by_body
1948 .get(child)
1949 .into_iter()
1950 .flatten()
1951 .map(|detector| &detector.parity)
1952 .chain(states.into_iter().flatten().map(|state| &state.next))
1953 {
1954 charge_template_work(template_id, work, parity.terms().len())?;
1955 for term in parity.terms() {
1956 if let DetectorTerm::LoopState(state) = *term
1957 && !in_iteration(state)
1958 && !child_effect.provided.contains(&state)
1959 {
1960 require(state);
1961 }
1962 }
1963 }
1964 effect.provided.extend(declared);
1965 }
1966 effects.insert(body, effect);
1967 }
1968 let entry = effects
1969 .remove(&template.circuit.entry_body())
1970 .expect("entry was visited");
1971 if let Some(&state) = entry.required.first() {
1972 return Err(BloqValidationError::UnknownTemplateLoopState {
1973 template: template_id,
1974 state,
1975 });
1976 }
1977 Ok(entry.provided)
1978}
1979
1980fn charge_template_work(
1981 template: TemplateId,
1982 work: &mut bloq_utils::boolean::BooleanDecisionDiagram,
1983 amount: usize,
1984) -> Result<(), BloqValidationError> {
1985 work.charge(amount)
1986 .map_err(|source| BloqValidationError::InvalidTemplateCircuit {
1987 template,
1988 source: crate::NodeTemplateInstanceMergeError::BooleanResource(source),
1989 })
1990}
1991
1992fn validate_template_measurement_refs(
1994 template: TemplateId,
1995 parity: &crate::TemplateDetectorParity,
1996 available: impl Fn(u32) -> bool,
1997) -> Result<(), BloqValidationError> {
1998 for term in parity.terms() {
1999 if let DetectorTerm::Measurement(measurement) = *term
2000 && !available(measurement)
2001 {
2002 return Err(BloqValidationError::UnknownTemplateMeasurement {
2003 template,
2004 measurement,
2005 });
2006 }
2007 }
2008 Ok(())
2009}
2010
2011fn validate_template_measurement_refs_before(
2014 template: TemplateId,
2015 order: &FxMap<u32, usize>,
2016 before: usize,
2017 parity: &crate::TemplateDetectorParity,
2018) -> Result<(), BloqValidationError> {
2019 validate_template_measurement_refs(template, parity, |measurement| {
2020 order.get(&measurement).is_some_and(|&index| index < before)
2021 })
2022}
2023
2024fn validate_template_loop_state_refs(
2025 template: TemplateId,
2026 parity: &crate::TemplateDetectorParity,
2027 available_states: &FxSet<LoopStateId>,
2028) -> Result<(), BloqValidationError> {
2029 for term in parity.terms() {
2030 if let DetectorTerm::LoopState(state) = *term
2031 && !available_states.contains(&state)
2032 {
2033 return Err(BloqValidationError::UnknownTemplateLoopState { template, state });
2034 }
2035 }
2036 Ok(())
2037}
2038
2039fn validate_template_body_ref(
2040 template: TemplateId,
2041 body: BodyId,
2042 reachable_bodies: &FxSet<BodyId>,
2043) -> Result<(), BloqValidationError> {
2044 if !reachable_bodies.contains(&body) {
2045 return Err(BloqValidationError::UnknownTemplateBody { template, body });
2046 }
2047 Ok(())
2048}
2049
2050#[expect(
2051 clippy::too_many_arguments,
2052 reason = "parity validation needs both local and whole-program ownership tables"
2053)]
2054fn validate_instance_parity_refs(
2055 graph: &SubGraph,
2056 node: BloqNodeId,
2057 parity: &crate::NodeDetectorParity,
2058 instances: &FxMap<TemplateInstanceId, InstanceValidation>,
2059 all_instances: &FxMap<TemplateInstanceId, TemplateId>,
2060 template_measurements: &[&MeasurementSet],
2061 consumer_order: usize,
2062 path_scratch: &mut crate::PathScratch,
2063 verified_paths: &mut FxSet<(BloqNodeId, BloqNodeId)>,
2064) -> Result<(), BloqValidationError> {
2065 if let Some(state) = parity.terms().iter().find_map(|term| match *term {
2066 DetectorTerm::LoopState(state) => Some(state),
2067 DetectorTerm::Measurement(_) => None,
2068 }) {
2069 return Err(BloqValidationError::NodeLoopStateUnsupported { node, state });
2070 }
2071 validate_instance_measurement_refs(
2072 node,
2073 parity.measurements(),
2074 instances,
2075 all_instances,
2076 template_measurements,
2077 consumer_order,
2078 )?;
2079 for measurement in parity.measurements() {
2080 let Some(instance) = instances.get(&measurement.instance) else {
2081 continue;
2082 };
2083 let pair = (instance.owner, node);
2084 if instance.owner != node && !verified_paths.contains(&pair) {
2085 if !graph.has_path_backwards_with_scratch(instance.owner, node, path_scratch) {
2086 return Err(BloqValidationError::UnorderedNodeMeasurement {
2087 node,
2088 owner: instance.owner,
2089 measurement: measurement.measurement,
2090 });
2091 }
2092 verified_paths.insert(pair);
2093 }
2094 }
2095 Ok(())
2096}
2097
2098fn validate_instance_measurement_refs(
2099 node: BloqNodeId,
2100 measurements: impl IntoIterator<Item = InstanceMeasurement>,
2101 instances: &FxMap<TemplateInstanceId, InstanceValidation>,
2102 all_instances: &FxMap<TemplateInstanceId, TemplateId>,
2103 template_measurements: &[&MeasurementSet],
2104 consumer_order: usize,
2105) -> Result<(), BloqValidationError> {
2106 let mut cached = None;
2107 for measurement in measurements {
2108 if cached
2110 .as_ref()
2111 .is_none_or(|(instance, _, _)| *instance != measurement.instance)
2112 {
2113 let local = instances.get(&measurement.instance);
2116 let template = match local {
2117 Some(instance) => instance.template,
2118 None => all_instances.get(&measurement.instance).copied().ok_or(
2119 BloqValidationError::UnknownTemplateInstance {
2120 node,
2121 instance: measurement.instance,
2122 },
2123 )?,
2124 };
2125 let produced = template_measurements.get(template.0 as usize).copied();
2126 let future_owner = local
2127 .filter(|instance| instance.order > consumer_order)
2128 .map(|instance| instance.owner);
2129 cached = Some((measurement.instance, produced, future_owner));
2130 }
2131 let (_, produced, future_owner) = cached.expect("current instance was resolved");
2132 if !produced.is_some_and(|produced| produced.contains(measurement.measurement)) {
2133 return Err(BloqValidationError::UnknownInstanceMeasurement {
2134 node,
2135 instance: measurement.instance,
2136 measurement: measurement.measurement,
2137 });
2138 }
2139 if let Some(owner) = future_owner {
2140 return Err(BloqValidationError::FutureMeasurement {
2141 node,
2142 owner,
2143 measurement: measurement.measurement,
2144 });
2145 }
2146 }
2147 Ok(())
2148}
2149
2150#[cfg(test)]
2151mod tests {
2152 use bloq_circuit::{
2153 BodyId, CircuitBody, ConditionalCorrection, CoordCircuit, DetectorParity, DetectorTerm,
2154 GateType, LoopStateId, Op, Pauli, PauliBasis, PauliMap,
2155 };
2156 use glam::{ivec2, ivec3};
2157
2158 use crate::PipeSeam;
2159 use crate::instantiation::InstantiationOptions;
2160 use crate::test_fixture::instance_measurement;
2161 use crate::{
2162 Bloq, BloqEdge, BloqNode, BloqTemplate, BloqValidationError, BoundaryFace, ClassicalExpr,
2163 ClassicalNode, InstanceBoundaryOperator, InstanceMeasurement, LevelPath, LogicalInput,
2164 LogicalOutput, NodeDetector, NodeRestart, NodeTemplateInstanceMergeError, ObservableOutput,
2165 PipePadding, QuantumEdge, QuantumTimeline, RegionNode, SourceBlockRef, SubGraph,
2166 TemplateDetector, TemplateDetectorScope, TemplateId, TemplateInstance, TemplateInstanceId,
2167 TemplateRepeatState, TemplateRestart, TemporalPipeRef, ValueRef,
2168 };
2169
2170 fn node(pos: glam::IVec3) -> BloqNode {
2171 BloqNode::from_members(vec![SourceBlockRef { pos }])
2172 }
2173
2174 #[test]
2179 fn well_formedness_messages_lead_with_their_rule_id() {
2180 for error in [
2181 BloqValidationError::InvalidActivation {
2182 node: crate::BloqNodeId(0),
2183 rule: "WF-16",
2184 reason: "activation is not an observable fold",
2185 },
2186 BloqValidationError::InvalidSeamGuard {
2187 node: crate::BloqNodeId(1),
2188 guard: crate::BloqNodeId(0).into(),
2189 },
2190 ] {
2191 let message = error.to_string();
2192 assert!(
2193 message.starts_with("WF-"),
2194 "message does not lead with a rule id: {message}"
2195 );
2196 }
2197 }
2198
2199 #[test]
2200 fn unused_bundle_signature_still_requires_known_templates() {
2201 let mut bloq = Bloq::new();
2202 let id =
2203 bloq.add_detector_bundle(crate::DetectorBundle::new(vec![TemplateId(99)], Vec::new()));
2204 assert!(
2205 matches!(bloq.validate(), Err(BloqValidationError::UnknownDetectorBundleTemplate { bundle, template: TemplateId(99) }) if bundle == id)
2206 );
2207 }
2208
2209 #[test]
2210 fn membership_certification_does_not_enumerate_independent_selectors() {
2211 const COUNT: u32 = 40;
2212 let mut bloq = Bloq::new();
2213 let mut controls = CoordCircuit::new();
2214 controls.measure(
2215 PauliBasis::Z,
2216 (0..COUNT).map(|index| ivec2(index as i32, 8)),
2217 );
2218 let control_template = bloq.add_template(BloqTemplate::new(controls));
2219 let mut control = node(ivec3(0, 0, 0));
2220 control
2221 .expect_quantum_mut()
2222 .instances
2223 .push(TemplateInstance::new(
2224 TemplateInstanceId(0),
2225 control_template,
2226 ivec2(0, 0),
2227 ));
2228 let control = bloq.add_node(control);
2229 let mut circuit = CoordCircuit::new();
2230 circuit.do_gate(GateType::H, [ivec2(0, 0)]).unwrap();
2231 let template = bloq.add_template(BloqTemplate::new(circuit));
2232 let mut selected = node(ivec3(0, 0, 1));
2233 for index in 0..COUNT {
2234 let instance = TemplateInstanceId(index + 1);
2235 selected
2236 .expect_quantum_mut()
2237 .instances
2238 .push(TemplateInstance::new(
2239 instance,
2240 template,
2241 ivec2(index as i32, 0),
2242 ));
2243 selected
2244 .expect_quantum_mut()
2245 .guards
2246 .push(crate::QuantumGuard {
2247 input: index,
2248 instances: vec![instance],
2249 ..Default::default()
2250 });
2251 }
2252 let selected = bloq.add_node(selected);
2253 for index in 0..COUNT {
2254 let read = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2255 index: None,
2256 operators: Vec::new(),
2257 measurements: vec![instance_measurement(0, index)],
2258 }));
2259 bloq.add_edge(control, read, BloqEdge::Order);
2260 bloq.add_edge(read, selected, BloqEdge::value(index));
2261 }
2262 let plans = bloq
2263 .validate_with_plans(&InstantiationOptions::default())
2264 .unwrap();
2265 assert!(plans.has_membership(&LevelPath::default(), selected));
2266 let materialized = bloq
2267 .emission_plans(&InstantiationOptions::default().with_boolean_limits(
2268 bloq_utils::boolean::BooleanLimits {
2269 max_nodes: 0,
2270 max_steps: 0,
2271 },
2272 ))
2273 .unwrap();
2274 assert!(materialized.has_membership(&LevelPath::default(), selected));
2275 assert!(materialized.get(&LevelPath::default(), selected).is_none());
2276 let raw = bloq.top().value_inputs(selected).next().unwrap().producer;
2278 let guard = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2279 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2280 }));
2281 bloq.add_edge(raw, guard, BloqEdge::value(0));
2282 let end = bloq.add_node(node(ivec3(0, 0, 2)));
2283 bloq.add_edge(guard, end, BloqEdge::Order);
2284 let mut seam = BloqEdge::quantum(vec![TemporalPipeRef {
2285 src: ivec3(0, 0, 1),
2286 dst: ivec3(0, 0, 2),
2287 hadamard: false,
2288 }]);
2289 if let BloqEdge::Quantum(quantum) = &mut seam {
2290 quantum.guard = Some(guard.into());
2291 }
2292 bloq.add_edge(selected, end, seam);
2293 let pinned = bloq.pin_membership(&Default::default()).unwrap();
2294 assert!(pinned.node(guard).is_some());
2295
2296 let mut conflict = CoordCircuit::new();
2297 conflict.do_gate(GateType::X, [ivec2(0, 0)]).unwrap();
2298 let conflict = bloq.add_template(BloqTemplate::new(conflict));
2299 let second = &mut bloq
2300 .node_mut(selected)
2301 .unwrap()
2302 .expect_quantum_mut()
2303 .instances[1];
2304 second.offset = ivec2(0, 0);
2305 second.template_id = conflict;
2306 assert!(matches!(
2307 bloq.validate(),
2308 Err(BloqValidationError::InvalidInstanceMergeStructure {
2309 source: NodeTemplateInstanceMergeError::QubitConflict(_),
2310 ..
2311 })
2312 ));
2313 }
2314
2315 #[test]
2316 fn complementary_membership_checks_domains_references_and_pins() {
2317 use crate::{BloqNodeKind, MembershipPinError, NodeProvenance, QuantumGuard};
2318
2319 let mut bloq = Bloq::new();
2320 let mut control = CoordCircuit::new();
2321 control.measure(PauliBasis::X, [ivec2(8, 0)]);
2322 let template = bloq.add_template(BloqTemplate::new(control));
2323 let mut stage = node(ivec3(8, 0, 0));
2324 stage
2325 .expect_quantum_mut()
2326 .instances
2327 .push(TemplateInstance::new(
2328 TemplateInstanceId(0),
2329 template,
2330 ivec2(0, 0),
2331 ));
2332 let measured = bloq.add_node(stage);
2333 let raw = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2334 index: None,
2335 operators: Vec::new(),
2336 measurements: vec![instance_measurement(0, 0)],
2337 }));
2338 bloq.add_edge(measured, raw, BloqEdge::Order);
2339 let selectors = [false, true].map(|invert| {
2340 let mut selector = BloqNode::classical(ClassicalNode::Compute {
2341 expr: if invert {
2342 ClassicalExpr::Not(Box::new(ClassicalExpr::In(0)))
2343 } else {
2344 ClassicalExpr::In(0)
2345 },
2346 });
2347 selector.provenance = NodeProvenance::BranchSelector {
2348 name: if invert { "off" } else { "on #測 %" }.into(),
2349 };
2350 let id = bloq.add_node(selector);
2351 bloq.add_edge(raw, id, BloqEdge::value(0));
2352 id
2353 });
2354 let mut alternatives = node(ivec3(0, 0, 1));
2355 for (index, gate) in [GateType::H, GateType::X].into_iter().enumerate() {
2356 let mut circuit = CoordCircuit::new();
2357 circuit.do_gate(gate, [ivec2(0, 0)]).unwrap();
2358 circuit.tick();
2359 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
2360 let template = bloq.add_template(BloqTemplate::new(circuit));
2361 let instance = TemplateInstanceId(index as u32 + 1);
2362 alternatives
2363 .expect_quantum_mut()
2364 .instances
2365 .push(TemplateInstance::new(instance, template, ivec2(0, 0)));
2366 alternatives.expect_quantum_mut().guards.push(QuantumGuard {
2367 input: index as u32,
2368 instances: vec![instance],
2369 ..Default::default()
2370 });
2371 }
2372 alternatives
2373 .expect_quantum_mut()
2374 .detectors
2375 .push(NodeDetector {
2376 parity: Default::default(),
2377 coords: None,
2378 });
2379 alternatives.expect_quantum_mut().guards[0]
2380 .detectors
2381 .push(0);
2382 alternatives.expect_quantum_mut().guards[0]
2383 .detector_parities
2384 .push((
2385 0,
2386 crate::NodeDetectorParity::from_measurements([instance_measurement(1, 0)]),
2387 ));
2388 let quantum = bloq.add_node(alternatives);
2389 for (slot, selector) in selectors.into_iter().enumerate() {
2390 bloq.add_edge(selector, quantum, BloqEdge::value(slot as u32));
2391 }
2392 let mut bundled = bloq.clone();
2394 let owner_template = bundled[quantum].expect_quantum().instances[0].template_id;
2395 let bundle = bundled.add_detector_bundle(crate::DetectorBundle::new(
2396 vec![owner_template],
2397 vec![crate::BundleDetector {
2398 parity: DetectorParity::from_measurements([crate::BundleMeasurement {
2399 owner: 0,
2400 measurement: 0,
2401 }]),
2402 coords: None,
2403 }],
2404 ));
2405 let payload = bundled.node_mut(quantum).unwrap().expect_quantum_mut();
2406 payload.detector_bundles.push(crate::DetectorBundleUse {
2407 bundle,
2408 instances: vec![TemplateInstanceId(1)],
2409 offset: ivec2(0, 0),
2410 });
2411 payload.guards[0].detector_bundles.push(0);
2412 bundled.validate().unwrap();
2413 let payload = bundled.node_mut(quantum).unwrap().expect_quantum_mut();
2414 payload.guards[0].detector_bundles.clear();
2415 payload.guards[1].detector_bundles.push(0);
2416 assert!(matches!(
2417 bundled.validate(),
2418 Err(BloqValidationError::ConditionalReferenceUnavailable { .. })
2419 ));
2420 let mut read = BloqNode::classical(ClassicalNode::Observable {
2421 index: None,
2422 operators: Vec::new(),
2423 measurements: vec![instance_measurement(1, 0)],
2424 });
2425 read.activation = Some(0);
2426 let read = bloq.add_node(read);
2427 bloq.add_edge(quantum, read, BloqEdge::Order);
2428 bloq.add_edge(selectors[0], read, BloqEdge::value(0));
2429 let plans = bloq
2430 .validate_with_plans(&InstantiationOptions::default())
2431 .unwrap();
2432 assert!(plans.has_membership(&LevelPath::default(), quantum));
2433 let mut partial = bloq.clone();
2435 partial.node_mut(selectors[1]).unwrap().kind = BloqNodeKind::Classical(
2436 ClassicalNode::Compute {
2437 expr: ClassicalExpr::And(Box::new([
2438 ClassicalExpr::In(0),
2439 ClassicalExpr::Const(false),
2440 ])),
2441 }
2442 .into(),
2443 );
2444 partial.validate().unwrap();
2445 let original = partial.to_binary();
2446 assert!(matches!(
2447 partial.insert_memory_rounds_batch(&[], 1),
2448 Err(crate::EditError::MembershipSelectionRequired)
2449 ));
2450 assert_eq!(partial.to_binary(), original);
2451 for restored in [
2452 Bloq::from_text(&bloq.to_text()).unwrap(),
2453 Bloq::from_binary(&bloq.to_binary()).unwrap(),
2454 ] {
2455 restored.validate().unwrap();
2456 assert!(matches!(
2457 restored.pin_membership(&[("on #測 %".into(), true), ("off".into(), true)].into()),
2458 Err(MembershipPinError::UnreachableAssignment)
2459 ));
2460 let pinned = restored
2461 .pin_membership(&[("on #測 %".into(), false), ("off".into(), true)].into())
2462 .unwrap();
2463 assert!(!pinned.has_conditional_membership());
2464 assert_eq!(
2465 pinned.node(quantum).unwrap().expect_quantum().instances[0].id,
2466 TemplateInstanceId(2)
2467 );
2468 }
2469 let mut invalid = bloq.clone();
2470 invalid.set_logical_inputs(vec![LogicalInput {
2471 port: ivec3(0, 0, 0),
2472 instance: TemplateInstanceId(1),
2473 x: PauliMap::empty(),
2474 z: PauliMap::empty(),
2475 }]);
2476 assert!(matches!(
2477 invalid.validate(),
2478 Err(BloqValidationError::InvalidInstanceMergeStructure {
2479 source: NodeTemplateInstanceMergeError::InvalidMembership(_),
2480 ..
2481 })
2482 ));
2483 let mut invalid = bloq.clone();
2485 invalid.node_mut(read).unwrap().activation = None;
2486 let graph = invalid.top_mut().graph_mut();
2487 let guard_edge = graph
2488 .find_edge(
2489 petgraph::stable_graph::NodeIndex::new(selectors[0].0 as usize),
2490 petgraph::stable_graph::NodeIndex::new(read.0 as usize),
2491 )
2492 .unwrap();
2493 graph.remove_edge(guard_edge);
2494 assert!(matches!(
2495 invalid.validate(),
2496 Err(BloqValidationError::ConditionalReferenceUnavailable { .. })
2497 ));
2498 for record_terms in [true, false] {
2501 let mut complete = bloq.clone();
2502 complete.node_mut(read).unwrap().kind = BloqNodeKind::Classical(
2503 ClassicalNode::Observable {
2504 index: Some(0),
2505 measurements: if record_terms {
2506 vec![instance_measurement(1, 0)]
2507 } else {
2508 Vec::new()
2509 },
2510 operators: if record_terms {
2511 Vec::new()
2512 } else {
2513 vec![InstanceBoundaryOperator {
2514 instance: TemplateInstanceId(1),
2515 face: BoundaryFace::Output,
2516 operator: PauliMap::empty(),
2517 }]
2518 },
2519 }
2520 .into(),
2521 );
2522 complete.validate().unwrap();
2523 complete.node_mut(read).unwrap().activation = None;
2524 assert!(matches!(
2525 complete.validate(),
2526 Err(BloqValidationError::ConditionalReferenceUnavailable { .. })
2527 ));
2528 }
2529 }
2530
2531 fn assert_invalid_timeline(mut weight: BloqNode, layer_round_ends: Vec<u32>) {
2532 weight.expect_quantum_mut().timeline = Some(QuantumTimeline { layer_round_ends });
2533 let mut bloq = Bloq::new();
2534 let node = bloq.add_node(weight);
2535 assert_eq!(
2536 bloq.validate(),
2537 Err(BloqValidationError::InvalidQuantumTimeline { node })
2538 );
2539 }
2540
2541 fn invalid_template_circuit_source(circuit: CoordCircuit) -> NodeTemplateInstanceMergeError {
2542 let mut bloq = Bloq::new();
2543 bloq.add_template(BloqTemplate::new(circuit));
2544 match bloq.validate().unwrap_err() {
2545 BloqValidationError::InvalidTemplateCircuit { source, .. } => source,
2546 error => panic!("expected invalid template circuit, got {error}"),
2547 }
2548 }
2549
2550 fn sparse_measurement_circuit() -> CoordCircuit {
2551 let q = ivec2(0, 0);
2552 let mut circuit = CoordCircuit::new();
2553 circuit
2554 .body_mut(circuit.entry_body())
2555 .unwrap()
2556 .ops_mut()
2557 .push(Op::Measure {
2558 basis: PauliBasis::Z,
2559 qubits: vec![q],
2560 measurements: vec![9],
2561 flip_probability: 0.0,
2562 });
2563 circuit.register_measurement_id(9, q);
2564 circuit
2565 }
2566
2567 fn empty_repeat_circuit() -> (CoordCircuit, BodyId) {
2568 let mut circuit = CoordCircuit::new();
2569 let body = circuit.add_body(CircuitBody::new());
2570 circuit
2571 .body_mut(circuit.entry_body())
2572 .unwrap()
2573 .ops_mut()
2574 .push(Op::Repeat {
2575 body,
2576 repetitions: 2,
2577 });
2578 (circuit, body)
2579 }
2580
2581 fn two_repeat_circuit() -> (CoordCircuit, BodyId, BodyId) {
2582 let mut circuit = CoordCircuit::new();
2583 let first = circuit.add_body(CircuitBody::new());
2584 let second = circuit.add_body(CircuitBody::new());
2585 circuit
2586 .body_mut(circuit.entry_body())
2587 .unwrap()
2588 .ops_mut()
2589 .extend([
2590 Op::Repeat {
2591 body: first,
2592 repetitions: 2,
2593 },
2594 Op::Repeat {
2595 body: second,
2596 repetitions: 2,
2597 },
2598 ]);
2599 (circuit, first, second)
2600 }
2601
2602 fn padding_template_pair(bloq: &mut Bloq) -> (TemplateId, TemplateId) {
2604 let plain = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2605 let mut looped_circuit = CoordCircuit::new();
2606 let body = looped_circuit.add_body(CircuitBody::new());
2607 looped_circuit
2608 .body_mut(looped_circuit.entry_body())
2609 .unwrap()
2610 .ops_mut()
2611 .push(Op::Repeat {
2612 body,
2613 repetitions: 2,
2614 });
2615 let looped = bloq.add_template(BloqTemplate::new(looped_circuit));
2616 (plain, looped)
2617 }
2618
2619 fn padding_ref(one_round: TemplateId, looped: TemplateId) -> PipePadding {
2620 PipePadding {
2621 offset: ivec2(0, 0),
2622 one_round,
2623 looped,
2624 }
2625 }
2626
2627 fn set_padding(bloq: &mut Bloq, padding: PipePadding) {
2628 let lower = bloq.add_node(BloqNode::from_members(vec![]));
2629 let upper = bloq.add_node(BloqNode::from_members(vec![]));
2630 bloq.add_edge(
2631 lower,
2632 upper,
2633 BloqEdge::Quantum(Box::new(QuantumEdge {
2634 guard: None,
2635 pipes: vec![PipeSeam {
2636 pipe: TemporalPipeRef {
2637 src: ivec3(0, 0, 0),
2638 dst: ivec3(0, 0, 1),
2639 hadamard: false,
2640 },
2641 padding: Some(padding),
2642 }],
2643 })),
2644 );
2645 }
2646
2647 #[test]
2648 fn well_formed_pipe_padding_validates() {
2649 let mut bloq = Bloq::new();
2650 let (plain, looped) = padding_template_pair(&mut bloq);
2651 set_padding(&mut bloq, padding_ref(plain, looped));
2652 bloq.validate()
2653 .expect("a plain one-round + looped pair validates");
2654 }
2655
2656 #[test]
2657 fn pipe_padding_one_round_with_repeat_is_rejected() {
2658 let mut bloq = Bloq::new();
2659 let (plain, looped) = padding_template_pair(&mut bloq);
2660 set_padding(&mut bloq, padding_ref(looped, looped));
2661 let _ = plain;
2662 assert!(matches!(
2663 bloq.validate().unwrap_err(),
2664 BloqValidationError::PipePaddingOneRoundLooped { .. }
2665 ));
2666 }
2667
2668 #[test]
2669 fn pipe_padding_looped_without_repeat_is_rejected() {
2670 let mut bloq = Bloq::new();
2671 let (plain, _looped) = padding_template_pair(&mut bloq);
2672 set_padding(&mut bloq, padding_ref(plain, plain));
2673 assert!(matches!(
2674 bloq.validate().unwrap_err(),
2675 BloqValidationError::PipePaddingLoopedMalformed { .. }
2676 ));
2677 }
2678
2679 #[test]
2680 fn cardinality_rejects_two_producers_on_one_value_slot() {
2681 let mut bloq = Bloq::new();
2682 let p0 = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2683 expr: ClassicalExpr::In(0),
2684 }));
2685 let p1 = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2686 expr: ClassicalExpr::In(0),
2687 }));
2688 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2689 condition: ClassicalExpr::In(0),
2690 }));
2691 bloq.add_edge(p0, consumer, BloqEdge::value(0));
2692 bloq.add_edge(p1, consumer, BloqEdge::value(0));
2693 assert!(matches!(
2694 bloq.validate().unwrap_err(),
2695 BloqValidationError::DuplicateValueSlot { slot: 0, .. }
2696 ));
2697 }
2698
2699 #[test]
2700 fn cardinality_allows_value_fanout_and_distinct_slots() {
2701 let mut bloq = Bloq::new();
2702 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2703 expr: ClassicalExpr::Const(false),
2704 }));
2705 let c0 = bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2706 condition: ClassicalExpr::In(0),
2707 }));
2708 let c1 = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2709 expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2710 }));
2711 bloq.add_edge(producer, c0, BloqEdge::value(0));
2712 bloq.add_edge(producer, c1, BloqEdge::value(0));
2713 let other = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2714 expr: ClassicalExpr::Const(false),
2715 }));
2716 bloq.add_edge(other, c1, BloqEdge::value(1));
2717 bloq.validate().unwrap();
2718 }
2719
2720 #[test]
2721 fn validation_recurses_into_region_bodies() {
2722 let mut bloq = Bloq::new();
2723 let mut body = SubGraph::new();
2724 let mut bad = node(ivec3(0, 0, 0));
2725 bad.expect_quantum_mut()
2726 .instances
2727 .push(TemplateInstance::new(
2728 TemplateInstanceId(0),
2729 crate::TemplateId(7), ivec2(0, 0),
2731 ));
2732 body.add_node(bad);
2733 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2735 restart_source: None,
2736 restart_condition: ClassicalExpr::Const(false),
2737 body,
2738 }));
2739 assert!(matches!(
2740 bloq.validate().unwrap_err(),
2741 BloqValidationError::UnknownTemplate { .. }
2742 ));
2743 }
2744
2745 #[test]
2746 fn empty_region_body_validates() {
2747 let mut bloq = Bloq::new();
2748 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2749 restart_source: None,
2750 restart_condition: ClassicalExpr::Const(false),
2751 body: SubGraph::new(),
2752 }));
2753 bloq.validate().unwrap();
2754 }
2755
2756 #[test]
2757 fn declared_outputs_validate_even_in_unconsumed_regions() {
2758 for invalid in [
2759 "result n99",
2760 "result n0:flip",
2761 "bindings n1",
2762 "bindings n99",
2763 "bindings n0 n0",
2764 ] {
2765 let program = Bloq::from_text(&format!(
2766 "BLOQIR 1
2767graph {{
2768 n0 rus 0 {{
2769 body {{
2770 n0 observable fragment
2771 n1 compute 1
2772 {invalid}
2773 }}
2774 }}
2775}}"
2776 ))
2777 .unwrap();
2778 let error = program.validate().unwrap_err();
2779 match invalid {
2780 "result n99" | "result n0:flip" => assert!(matches!(
2781 error,
2782 BloqValidationError::InvalidValueOutput { .. }
2783 )),
2784 "bindings n0 n0" => assert!(matches!(
2785 error,
2786 BloqValidationError::DuplicateBoundaryOutput { .. }
2787 )),
2788 _ => assert!(matches!(
2789 error,
2790 BloqValidationError::InvalidBoundaryOutput { .. }
2791 )),
2792 }
2793 }
2794 }
2795
2796 #[test]
2797 fn independent_outputs_allow_multiple_unused_bits_and_local_consumers() {
2798 let program = Bloq::from_text(
2799 "BLOQIR 1
2800 graph {
2801 n0 rus 0 {
2802 body {
2803 n0 observable fragment
2804 n1 compute 1
2805 n2 compute !in0
2806 n3 compute 0
2807 n1 -> n2 value 0
2808 result n1
2809 bindings n0
2810 }
2811 }
2812 n1 observable 0
2813 n0 -> n1 compose 0
2814 }",
2815 )
2816 .unwrap();
2817 program.validate().unwrap();
2818 }
2819
2820 #[test]
2821 fn region_selector_without_value_input_is_rejected() {
2822 let mut bloq = Bloq::new();
2823 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2824 restart_source: None,
2825 restart_condition: ClassicalExpr::In(0),
2826 body: SubGraph::new(),
2827 }));
2828 assert!(matches!(
2829 bloq.validate().unwrap_err(),
2830 BloqValidationError::MissingRegionSelectorInput { slot: 0, .. }
2831 ));
2832 }
2833
2834 #[test]
2835 fn region_selector_rejects_an_unused_value_input() {
2836 let mut bloq = Bloq::new();
2837 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2838 expr: ClassicalExpr::Const(false),
2839 }));
2840 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2841 restart_source: None,
2842 restart_condition: ClassicalExpr::Const(false),
2843 body: SubGraph::new(),
2844 }));
2845 bloq.add_edge(producer, region, BloqEdge::value(7));
2846
2847 assert!(matches!(
2848 bloq.validate().unwrap_err(),
2849 BloqValidationError::UnusedClassicalInput { node, slot: 7 } if node == region
2850 ));
2851 }
2852
2853 #[test]
2854 fn quantum_timeline_must_be_multi_layer_ordered_positive_and_in_range() {
2855 for layer_round_ends in [vec![], vec![1], vec![0, 0], vec![2, 1]] {
2856 assert_invalid_timeline(node(ivec3(0, 0, 0)), layer_round_ends);
2857 }
2858
2859 assert_invalid_timeline(node(ivec3(0, 0, i32::MAX)), vec![1, 2]);
2860
2861 let mut bloq = Bloq::new();
2862 let mut weight = node(ivec3(0, 0, i32::MAX - 2));
2863 weight.expect_quantum_mut().timeline = Some(QuantumTimeline {
2864 layer_round_ends: vec![0, 0, 3],
2865 });
2866 bloq.add_node(weight);
2867 bloq.validate()
2868 .expect("equal cuts and empty leading layers are valid");
2869 }
2870
2871 #[test]
2872 fn quantum_timeline_requires_nonempty_block_component_provenance() {
2873 for weight in [
2874 BloqNode::from_members(vec![]),
2875 BloqNode::from_temporal_pipe(TemporalPipeRef {
2876 src: ivec3(0, 0, 0),
2877 dst: ivec3(0, 0, 1),
2878 hadamard: false,
2879 }),
2880 ] {
2881 assert_invalid_timeline(weight, vec![1, 2]);
2882 }
2883 }
2884
2885 #[test]
2886 fn cyclic_graph_is_rejected_not_panicked() {
2887 let mut bloq = Bloq::new();
2888 let a = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2889 expr: ClassicalExpr::In(0),
2890 }));
2891 let b = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2892 expr: ClassicalExpr::In(0),
2893 }));
2894 bloq.add_edge(a, b, BloqEdge::value(0));
2895 bloq.add_edge(b, a, BloqEdge::value(0));
2896 assert!(matches!(
2897 bloq.validate().unwrap_err(),
2898 BloqValidationError::CyclicGraph
2899 ));
2900 }
2901
2902 #[test]
2903 fn compute_slot_read_without_producer_is_rejected() {
2904 let mut bloq = Bloq::new();
2905 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2906 expr: ClassicalExpr::Const(false),
2907 }));
2908 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2909 expr: ClassicalExpr::Xor(Box::new([
2910 ClassicalExpr::In(0),
2911 ClassicalExpr::In(1), ])),
2913 }));
2914 bloq.add_edge(producer, compute, BloqEdge::value(0));
2915 assert!(matches!(
2916 bloq.validate().unwrap_err(),
2917 BloqValidationError::MissingClassicalInput { slot: 1, .. }
2918 ));
2919 }
2920
2921 #[test]
2922 fn compute_value_edge_never_read_is_rejected() {
2923 let mut bloq = Bloq::new();
2924 let used = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2925 expr: ClassicalExpr::Const(false),
2926 }));
2927 let unused = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2928 expr: ClassicalExpr::Const(false),
2929 }));
2930 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2931 expr: ClassicalExpr::In(0),
2932 }));
2933 bloq.add_edge(used, compute, BloqEdge::value(0));
2934 bloq.add_edge(unused, compute, BloqEdge::value(1));
2935 assert!(matches!(
2936 bloq.validate().unwrap_err(),
2937 BloqValidationError::UnusedClassicalInput { slot: 1, .. }
2938 ));
2939 }
2940
2941 #[test]
2942 fn output_frame_stamp_on_non_compute_node_is_rejected() {
2943 let mut bloq = Bloq::new();
2944 bloq.add_node(
2945 BloqNode::classical(ClassicalNode::Compute {
2946 expr: ClassicalExpr::Const(false),
2947 })
2948 .with_provenance(crate::NodeProvenance::OutputFrame {
2949 port: ivec3(0, 0, 1),
2950 basis: crate::Basis::X,
2951 }),
2952 );
2953 let observable = bloq.add_node(
2954 BloqNode::classical(ClassicalNode::observable(0)).with_provenance(
2955 crate::NodeProvenance::OutputFrame {
2956 port: ivec3(0, 0, 1),
2957 basis: crate::Basis::Z,
2958 },
2959 ),
2960 );
2961 assert!(matches!(
2962 bloq.validate().unwrap_err(),
2963 BloqValidationError::OutputFrameNotCompute { node, .. } if node == observable
2964 ));
2965 }
2966
2967 #[test]
2968 fn output_frame_stamp_inside_region_is_rejected() {
2969 let mut body = SubGraph::new();
2970 let nested = body.add_node(
2971 BloqNode::classical(ClassicalNode::Compute {
2972 expr: ClassicalExpr::Const(false),
2973 })
2974 .with_provenance(crate::NodeProvenance::OutputFrame {
2975 port: ivec3(0, 0, 1),
2976 basis: crate::Basis::X,
2977 }),
2978 );
2979 let mut bloq = Bloq::new();
2980 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2981 restart_source: None,
2982 restart_condition: ClassicalExpr::Const(false),
2983 body,
2984 }));
2985
2986 assert!(matches!(
2987 bloq.validate().unwrap_err(),
2988 BloqValidationError::OutputFrameNotCompute { node, .. } if node == nested
2989 ));
2990 }
2991
2992 #[test]
2993 fn output_frame_missing_one_basis_is_rejected() {
2994 let mut bloq = Bloq::new();
2995 bloq.add_node(
2996 BloqNode::classical(ClassicalNode::Compute {
2997 expr: ClassicalExpr::Const(false),
2998 })
2999 .with_provenance(crate::NodeProvenance::OutputFrame {
3000 port: ivec3(0, 0, 1),
3001 basis: crate::Basis::X,
3002 }),
3003 );
3004 assert!(matches!(
3005 bloq.validate().unwrap_err(),
3006 BloqValidationError::OutputFrameIncomplete { port } if port == ivec3(0, 0, 1)
3007 ));
3008 }
3009
3010 #[test]
3011 fn logical_cut_owners_require_active_region_ancestors() {
3012 let port = ivec3(0, 0, 0);
3013 let mut base = Bloq::new();
3014 let template = base.add_template(BloqTemplate::new(CoordCircuit::new()));
3015 let mut owner = BloqNode::from_members(Vec::new());
3016 owner
3017 .expect_quantum_mut()
3018 .instances
3019 .push(TemplateInstance::new(
3020 TemplateInstanceId(0),
3021 template,
3022 ivec2(0, 0),
3023 ));
3024 let mut body = SubGraph::new();
3025 body.add_node(owner);
3026 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Compute {
3027 expr: ClassicalExpr::Const(false),
3028 }));
3029 for basis in [crate::Basis::X, crate::Basis::Z] {
3030 base.add_node(
3031 BloqNode::classical(ClassicalNode::Compute {
3032 expr: ClassicalExpr::Const(false),
3033 })
3034 .with_provenance(crate::NodeProvenance::OutputFrame { port, basis }),
3035 );
3036 }
3037 let cases = [
3038 (
3039 "source-free-rus",
3040 RegionNode::RepeatUntilSuccess {
3041 restart_source: None,
3042 restart_condition: ClassicalExpr::Const(false),
3043 body: body.clone(),
3044 },
3045 ),
3046 (
3047 "rus",
3048 RegionNode::RepeatUntilSuccess {
3049 restart_condition: ClassicalExpr::Const(false),
3050 restart_source: Some(restart_source.into()),
3051 body: body.clone(),
3052 },
3053 ),
3054 ];
3055 for (name, region) in cases {
3056 for activation in [None, Some(false), Some(true)] {
3057 let mut program = base.clone();
3058 let mut node = BloqNode::region(region.clone());
3059 node.activation = activation.map(|_| 1);
3060 let id = program.add_node(node);
3061 if let Some(value) = activation {
3062 let guard = program.add_node(BloqNode::classical(ClassicalNode::Compute {
3063 expr: ClassicalExpr::Const(value),
3064 }));
3065 program.add_edge(guard, id, BloqEdge::value(1));
3066 }
3067 for output in [false, true] {
3069 program.set_logical_inputs(if output {
3070 Vec::new()
3071 } else {
3072 vec![LogicalInput {
3073 port,
3074 instance: TemplateInstanceId(0),
3075 x: PauliMap::empty(),
3076 z: PauliMap::empty(),
3077 }]
3078 });
3079 program.set_logical_outputs(if output {
3080 vec![LogicalOutput {
3081 port,
3082 instance: TemplateInstanceId(0),
3083 x: PauliMap::empty(),
3084 z: PauliMap::empty(),
3085 }]
3086 } else {
3087 Vec::new()
3088 });
3089 let result = program.validate();
3090 if activation != Some(false) {
3091 result.unwrap_or_else(|error| panic!("{name} {activation:?}: {error}"));
3092 } else {
3093 assert!(
3094 matches!(
3095 result,
3096 Err(BloqValidationError::InvalidInstanceMergeStructure {
3097 source: NodeTemplateInstanceMergeError::InvalidMembership(_),
3098 ..
3099 })
3100 ),
3101 "{name} {activation:?} output={output}: {result:?}"
3102 );
3103 }
3104 }
3105 }
3106 }
3107 }
3108
3109 #[test]
3110 fn output_frame_requires_pair_for_every_logical_output() {
3111 let port = ivec3(3, 4, 5);
3112 let mut bloq = Bloq::new();
3113 let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
3114 let mut owner = BloqNode::from_members(Vec::new());
3115 owner
3116 .expect_quantum_mut()
3117 .instances
3118 .push(TemplateInstance::new(
3119 TemplateInstanceId(0),
3120 template,
3121 ivec2(0, 0),
3122 ));
3123 bloq.add_node(owner);
3124 bloq.set_logical_outputs(vec![LogicalOutput {
3125 port,
3126 instance: TemplateInstanceId(0),
3127 x: PauliMap::empty(),
3128 z: PauliMap::empty(),
3129 }]);
3130
3131 assert!(matches!(
3132 bloq.validate().unwrap_err(),
3133 BloqValidationError::OutputFrameIncomplete { port: missing } if missing == port
3134 ));
3135 let mut outputs = bloq.logical_outputs().to_vec();
3136 outputs[0].instance = TemplateInstanceId(9);
3137 bloq.set_logical_outputs(outputs);
3138 assert!(matches!(
3139 bloq.validate().unwrap_err(),
3140 BloqValidationError::LogicalOutputUnknownInstance {
3141 instance: TemplateInstanceId(9),
3142 ..
3143 }
3144 ));
3145 }
3146
3147 #[test]
3148 fn logical_input_requires_existing_instance() {
3149 let port = ivec3(3, 4, 5);
3150 let instance = TemplateInstanceId(9);
3151 let mut bloq = Bloq::new();
3152 bloq.set_logical_inputs(vec![LogicalInput {
3153 port,
3154 instance,
3155 x: PauliMap::empty(),
3156 z: PauliMap::empty(),
3157 }]);
3158
3159 assert!(matches!(
3160 bloq.validate().unwrap_err(),
3161 BloqValidationError::LogicalInputUnknownInstance {
3162 port: missing_port,
3163 instance: missing_instance,
3164 } if missing_port == port && missing_instance == instance
3165 ));
3166 }
3167
3168 #[test]
3169 fn complete_hand_built_output_frame_without_logical_metadata_is_allowed() {
3170 let mut bloq = Bloq::new();
3171 for basis in [crate::Basis::X, crate::Basis::Z] {
3172 bloq.add_node(
3173 BloqNode::classical(ClassicalNode::Compute {
3174 expr: ClassicalExpr::Const(false),
3175 })
3176 .with_provenance(crate::NodeProvenance::OutputFrame {
3177 port: ivec3(0, 0, 1),
3178 basis,
3179 }),
3180 );
3181 }
3182
3183 bloq.validate()
3184 .expect("complete hand-built frame pairs remain legal");
3185 }
3186
3187 fn sparse_instance_bloq(measurement: u32) -> Bloq {
3188 let q = ivec2(0, 0);
3189 let mut bloq = Bloq::new();
3190 let template_id = bloq.add_template(BloqTemplate::new(sparse_measurement_circuit()));
3191 let mut producer = node(ivec3(0, 0, 0));
3192 producer
3193 .expect_quantum_mut()
3194 .instances
3195 .push(TemplateInstance::new(TemplateInstanceId(0), template_id, q));
3196 let producer = bloq.add_node(producer);
3197 let accumulate = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
3198 index: None,
3199 operators: Vec::new(),
3200 measurements: vec![InstanceMeasurement {
3201 instance: TemplateInstanceId(0),
3202 measurement,
3203 }],
3204 }));
3205 bloq.add_edge(producer, accumulate, BloqEdge::Order);
3206 bloq
3207 }
3208
3209 #[test]
3210 fn sparse_instance_measurement_ids_use_registry_membership() {
3211 sparse_instance_bloq(9).validate().unwrap();
3212 assert!(matches!(
3213 sparse_instance_bloq(0).validate().unwrap_err(),
3214 BloqValidationError::UnknownInstanceMeasurement { measurement: 0, .. }
3215 ));
3216 }
3217
3218 #[test]
3219 fn complete_observable_checks_local_record_sites() {
3220 for measurement in [9, 0] {
3221 let mut bloq = sparse_instance_bloq(measurement);
3222 let read = bloq
3223 .nodes()
3224 .find(|(_, node)| {
3225 matches!(
3226 node.try_classical(),
3227 Some(ClassicalNode::Observable { index: None, .. })
3228 )
3229 })
3230 .unwrap()
3231 .0;
3232 let measurements = bloq[read].try_classical().unwrap().measurements().to_vec();
3233 bloq.node_mut(read).unwrap().kind = crate::BloqNodeKind::Classical(
3234 ClassicalNode::Observable {
3235 index: Some(0),
3236 measurements,
3237 operators: Vec::new(),
3238 }
3239 .into(),
3240 );
3241 if measurement == 9 {
3242 bloq.validate().unwrap();
3243 } else {
3244 assert!(matches!(
3245 bloq.validate(),
3246 Err(BloqValidationError::UnknownInstanceMeasurement { measurement: 0, .. })
3247 ));
3248 }
3249 }
3250 }
3251
3252 #[test]
3253 fn sparse_template_side_tables_use_registry_membership() {
3254 let mut template = BloqTemplate::new(sparse_measurement_circuit());
3255 template.detectors.push(TemplateDetector {
3256 scope: TemplateDetectorScope::TopLevel,
3257 parity: DetectorParity::from_measurements([9]),
3258 coords: None,
3259 });
3260 template.boundary_flows.push(
3261 bloq_circuit::Flow::new(PauliMap::empty(), PauliMap::empty()).with_measurements([9]),
3262 );
3263 template.restarts.push(TemplateRestart {
3264 parity: DetectorParity::from_measurements([9]),
3265 });
3266 let mut bloq = Bloq::new();
3267 bloq.add_template(template);
3268
3269 bloq.validate().unwrap();
3270 }
3271
3272 #[test]
3273 fn unused_registry_measurement_is_legal_only_when_unreferenced() {
3274 let mut circuit = CoordCircuit::new();
3275 circuit.register_measurement_id(9, ivec2(0, 0));
3276 let mut bloq = Bloq::new();
3277 bloq.add_template(BloqTemplate::new(circuit.clone()));
3278 bloq.validate()
3279 .expect("an unreferenced registry-only record is legal");
3280
3281 let mut template = BloqTemplate::new(circuit);
3282 template.detectors.push(TemplateDetector {
3283 scope: TemplateDetectorScope::TopLevel,
3284 parity: DetectorParity::from_measurements([9]),
3285 coords: None,
3286 });
3287 let mut bloq = Bloq::new();
3288 bloq.add_template(template);
3289 assert!(matches!(
3290 bloq.validate().unwrap_err(),
3291 BloqValidationError::UnknownTemplateMeasurement { measurement: 9, .. }
3292 ));
3293 }
3294
3295 #[test]
3296 fn node_side_table_rejects_registry_only_measurement() {
3297 let mut circuit = CoordCircuit::new();
3298 circuit.register_measurement_id(9, ivec2(0, 0));
3299 let mut bloq = Bloq::new();
3300 let template = bloq.add_template(BloqTemplate::new(circuit));
3301 let mut quantum = node(ivec3(0, 0, 0));
3302 quantum
3303 .expect_quantum_mut()
3304 .instances
3305 .push(TemplateInstance::new(
3306 TemplateInstanceId(0),
3307 template,
3308 ivec2(0, 0),
3309 ));
3310 let producer = bloq.add_node(quantum);
3311 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
3312 index: None,
3313 operators: Vec::new(),
3314 measurements: vec![instance_measurement(0, 9)],
3315 }));
3316 bloq.add_edge(producer, consumer, BloqEdge::Order);
3317
3318 assert!(matches!(
3319 bloq.validate().unwrap_err(),
3320 BloqValidationError::UnknownInstanceMeasurement { measurement: 9, .. }
3321 ));
3322 }
3323
3324 #[test]
3325 fn validation_checks_unused_registry_coordinate_translation() {
3326 let coordinate = ivec2(i32::MAX, 0);
3327 let offset = ivec2(1, 0);
3328 let mut circuit = CoordCircuit::new();
3329 circuit.register_measurement_id(0, coordinate);
3330 let mut bloq = Bloq::new();
3331 let template = bloq.add_template(BloqTemplate::new(circuit));
3332 let mut quantum = node(ivec3(0, 0, 0));
3333 quantum
3334 .expect_quantum_mut()
3335 .instances
3336 .push(TemplateInstance::new(
3337 TemplateInstanceId(0),
3338 template,
3339 offset,
3340 ));
3341 bloq.add_node(quantum);
3342
3343 assert!(matches!(
3344 bloq.validate().unwrap_err(),
3345 BloqValidationError::InvalidInstanceMergeStructure {
3346 source: NodeTemplateInstanceMergeError::CoordinateOverflow(_),
3347 ..
3348 }
3349 ));
3350 }
3351
3352 #[test]
3353 fn unused_template_rejects_unknown_measurement_control() {
3354 let mut circuit = CoordCircuit::new();
3355 circuit
3356 .body_mut(circuit.entry_body())
3357 .unwrap()
3358 .ops_mut()
3359 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
3360 pauli: PauliBasis::X,
3361 control: 7,
3362 target: ivec2(0, 0),
3363 }]));
3364 let mut bloq = Bloq::new();
3365 bloq.add_template(BloqTemplate::new(circuit));
3366
3367 assert!(matches!(
3368 bloq.validate().unwrap_err(),
3369 BloqValidationError::InvalidTemplateCircuit {
3370 source: NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(7),
3371 ..
3372 }
3373 ));
3374 }
3375
3376 #[test]
3377 fn template_parities_reject_unknown_measurements_and_loop_states() {
3378 let mut template = BloqTemplate::new(sparse_measurement_circuit());
3379 template.detectors.push(TemplateDetector {
3380 scope: TemplateDetectorScope::TopLevel,
3381 parity: DetectorParity::from_measurements([0]),
3382 coords: None,
3383 });
3384 let mut bloq = Bloq::new();
3385 bloq.add_template(template);
3386 assert!(matches!(
3387 bloq.validate().unwrap_err(),
3388 BloqValidationError::UnknownTemplateMeasurement { measurement: 0, .. }
3389 ));
3390
3391 let mut template = BloqTemplate::new(sparse_measurement_circuit());
3392 template.detectors.push(TemplateDetector {
3393 scope: TemplateDetectorScope::TopLevel,
3394 parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(7))]),
3395 coords: None,
3396 });
3397 let mut bloq = Bloq::new();
3398 bloq.add_template(template);
3399 assert!(matches!(
3400 bloq.validate().unwrap_err(),
3401 BloqValidationError::UnknownTemplateLoopState {
3402 state: LoopStateId(7),
3403 ..
3404 }
3405 ));
3406 }
3407
3408 #[test]
3409 fn node_parities_reject_loop_states() {
3410 let mut bloq = Bloq::new();
3411 let mut quantum = node(ivec3(0, 0, 0));
3412 quantum.expect_quantum_mut().detectors.push(NodeDetector {
3413 parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
3414 coords: None,
3415 });
3416 let node = bloq.add_node(quantum);
3417
3418 assert_eq!(
3419 bloq.validate().unwrap_err(),
3420 BloqValidationError::NodeLoopStateUnsupported {
3421 node,
3422 state: LoopStateId(0),
3423 }
3424 );
3425 }
3426
3427 #[test]
3428 fn repeat_state_parities_reject_unknown_measurements() {
3429 let mut circuit = sparse_measurement_circuit();
3430 let body = circuit.add_body(CircuitBody::new());
3431 circuit
3432 .body_mut(circuit.entry_body())
3433 .unwrap()
3434 .ops_mut()
3435 .push(Op::Repeat {
3436 body,
3437 repetitions: 2,
3438 });
3439 let mut template = BloqTemplate::new(circuit);
3440 template.repeat_states.push(TemplateRepeatState {
3441 body,
3442 state: LoopStateId(0),
3443 initial: DetectorParity::from_measurements([9]),
3444 next: DetectorParity::from_measurements([0]),
3445 });
3446 let mut bloq = Bloq::new();
3447 bloq.add_template(template);
3448
3449 assert!(matches!(
3450 bloq.validate().unwrap_err(),
3451 BloqValidationError::UnknownTemplateMeasurement { measurement: 0, .. }
3452 ));
3453 }
3454
3455 #[test]
3456 fn duplicate_template_loop_state_ids_are_rejected() {
3457 let (circuit, body) = empty_repeat_circuit();
3458 let state = |state| TemplateRepeatState {
3459 body,
3460 state,
3461 initial: DetectorParity::default(),
3462 next: DetectorParity::default(),
3463 };
3464 let template = BloqTemplate::with_parts(
3465 circuit,
3466 Vec::new(),
3467 vec![state(LoopStateId(0)), state(LoopStateId(0))],
3468 Vec::new(),
3469 Vec::new(),
3470 );
3471 let mut bloq = Bloq::new();
3472 bloq.add_template(template);
3473
3474 assert!(matches!(
3475 bloq.validate().unwrap_err(),
3476 BloqValidationError::DuplicateTemplateLoopState {
3477 state: LoopStateId(0),
3478 ..
3479 }
3480 ));
3481 }
3482
3483 #[test]
3484 fn validation_resource_diagnostics_retain_counts_and_semantic_messages() {
3485 use crate::BloqNodeId;
3486 use bloq_circuit::CircuitError;
3487 use bloq_utils::boolean::BooleanResourceError;
3488
3489 let boolean = BooleanResourceError {
3490 resource: "Boolean work",
3491 observed: 11,
3492 limit: 10,
3493 };
3494 let direct = BloqValidationError::BooleanResource(boolean);
3495 assert!(direct.is_resource_limited());
3496 assert_eq!(
3497 direct.to_string(),
3498 "resource limit: Boolean work requires 11, limit is 10"
3499 );
3500 for (source, counts) in [
3501 (
3502 NodeTemplateInstanceMergeError::BooleanResource(boolean),
3503 "requires 11, limit is 10",
3504 ),
3505 (
3506 NodeTemplateInstanceMergeError::NoiseRepeatExpansion(crate::FlattenError::Circuit(
3507 CircuitError::FlattenResourceLimit {
3508 observed: 31,
3509 limit: 30,
3510 },
3511 )),
3512 "31 work units (limit 30)",
3513 ),
3514 ] {
3515 assert!(source.is_resource_limited());
3516 assert!(source.to_string().contains(counts));
3517 for error in [
3518 BloqValidationError::InvalidInstanceMergeStructure {
3519 node: BloqNodeId(3),
3520 source: source.clone(),
3521 },
3522 BloqValidationError::InvalidTemplateCircuit {
3523 template: TemplateId(7),
3524 source,
3525 },
3526 ] {
3527 assert!(error.is_resource_limited());
3528 let message = error.to_string();
3529 assert!(message.contains("resource limit"), "{message}");
3530 assert!(message.contains(counts), "{message}");
3531 assert!(
3532 !message.contains("invalid") && !message.contains("do not merge"),
3533 "{message}"
3534 );
3535 }
3536 }
3537
3538 let source = NodeTemplateInstanceMergeError::TickSegmentCountMismatch;
3539 assert!(!source.is_resource_limited());
3540 let merge = BloqValidationError::InvalidInstanceMergeStructure {
3541 node: BloqNodeId(3),
3542 source: source.clone(),
3543 };
3544 assert!(!merge.is_resource_limited());
3545 assert_eq!(
3546 merge.to_string(),
3547 format!(
3548 "WF-9: node {:?} template instances do not merge: {source}",
3549 BloqNodeId(3)
3550 )
3551 );
3552 let template = BloqValidationError::InvalidTemplateCircuit {
3553 template: TemplateId(7),
3554 source: source.clone(),
3555 };
3556 assert!(!template.is_resource_limited());
3557 assert_eq!(
3558 template.to_string(),
3559 format!(
3560 "WF-9: template {:?} has invalid circuit structure: {source}",
3561 TemplateId(7)
3562 )
3563 );
3564 let circuit = CircuitError::InvalidCircuitBody(BodyId(9));
3565 let flatten = crate::FlattenError::Circuit(circuit.clone());
3566 assert!(!flatten.is_resource_limited());
3567 assert_eq!(
3568 flatten.to_string(),
3569 format!("flattening the circuit failed: {circuit}")
3570 );
3571 let noise = NodeTemplateInstanceMergeError::NoiseRepeatExpansion(
3572 crate::FlattenError::UnresolvedLoopState(LoopStateId(0)),
3573 );
3574 assert!(!noise.is_resource_limited());
3575 assert_eq!(
3576 noise.to_string(),
3577 "expanding repeat moments before idle noise: parity references undefined loop state LoopStateId(0)"
3578 );
3579 }
3580
3581 #[test]
3582 fn shared_circuit_dag_validation_preserves_measurement_and_state_cuts() {
3583 let mut circuit = CoordCircuit::new();
3584 let qubit = ivec2(0, 0);
3585 let measurement = circuit.reserve_measurement_id(qubit);
3586 let leaf = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
3587 basis: PauliBasis::Z,
3588 qubits: vec![qubit],
3589 measurements: vec![measurement],
3590 flip_probability: 0.0,
3591 }]));
3592 let mut body = leaf;
3593 for _ in 0..48 {
3594 body = circuit.add_body(CircuitBody::from_ops(vec![
3595 Op::Repeat {
3596 body,
3597 repetitions: 1,
3598 },
3599 Op::Repeat {
3600 body,
3601 repetitions: 1,
3602 },
3603 ]));
3604 }
3605 circuit.push_repeat(body, 1);
3606 let state = LoopStateId(0);
3607 let mut template = BloqTemplate::new(circuit);
3608 template.repeat_states.push(TemplateRepeatState {
3609 body: leaf,
3610 state,
3611 initial: DetectorParity::default(),
3612 next: DetectorParity::from_measurements([measurement]),
3613 });
3614 template.detectors.push(TemplateDetector {
3615 scope: TemplateDetectorScope::RepeatBody { body: leaf },
3616 parity: DetectorParity::from_terms([
3617 DetectorTerm::LoopState(state),
3618 DetectorTerm::Measurement(measurement),
3619 ]),
3620 coords: None,
3621 });
3622 template.restarts.push(TemplateRestart {
3623 parity: DetectorParity::from_measurements([measurement]),
3624 });
3625 let mut bloq = Bloq::new();
3626 bloq.add_template(template.clone());
3627 bloq.validate()
3628 .expect("shared bodies do not multiply validation work");
3629
3630 template.restarts.push(TemplateRestart {
3631 parity: DetectorParity::from_terms([DetectorTerm::LoopState(state)]),
3632 });
3633 let mut invalid = Bloq::new();
3634 invalid.add_template(template);
3635 assert!(matches!(
3636 invalid.validate(),
3637 Err(BloqValidationError::UnknownTemplateLoopState { .. })
3638 ));
3639 }
3640
3641 #[test]
3642 fn shared_body_state_requirements_are_checked_in_each_enclosing_scope() {
3643 for outside in [false, true] {
3644 let mut circuit = CoordCircuit::new();
3645 let owner = circuit.add_body(CircuitBody::new());
3646 let shared = circuit.add_body(CircuitBody::new());
3647 let outer = circuit.add_body(CircuitBody::from_ops(vec![
3648 Op::Repeat {
3649 body: owner,
3650 repetitions: 1,
3651 },
3652 Op::Repeat {
3653 body: shared,
3654 repetitions: 1,
3655 },
3656 Op::Repeat {
3657 body: shared,
3658 repetitions: 1,
3659 },
3660 ]));
3661 circuit.push_repeat(outer, 1);
3662 if outside {
3663 circuit.push_repeat(shared, 1);
3664 }
3665 let state = LoopStateId(0);
3666 let mut template = BloqTemplate::new(circuit);
3667 template.repeat_states.push(TemplateRepeatState {
3668 body: owner,
3669 state,
3670 initial: DetectorParity::default(),
3671 next: DetectorParity::default(),
3672 });
3673 template.detectors.push(TemplateDetector {
3674 scope: TemplateDetectorScope::RepeatBody { body: shared },
3675 parity: DetectorParity::from_terms([DetectorTerm::LoopState(state)]),
3676 coords: None,
3677 });
3678 let mut bloq = Bloq::new();
3679 bloq.add_template(template);
3680 if outside {
3681 assert!(matches!(
3682 bloq.validate(),
3683 Err(BloqValidationError::UnknownTemplateLoopState { .. })
3684 ));
3685 } else {
3686 bloq.validate().unwrap();
3687 }
3688 }
3689 }
3690
3691 #[test]
3692 fn deep_circuit_body_validation_does_not_use_recursive_calls() {
3693 let mut circuit = CoordCircuit::new();
3694 let mut body = circuit.add_body(CircuitBody::new());
3695 for _ in 0..4096 {
3696 body = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
3697 body,
3698 repetitions: 1,
3699 }]));
3700 }
3701 circuit.push_repeat(body, 1);
3702 let mut bloq = Bloq::new();
3703 let template = bloq.add_template(BloqTemplate::new(circuit));
3704 let mut node = BloqNode::from_members(Vec::new());
3705 node.expect_quantum_mut()
3706 .instances
3707 .push(TemplateInstance::new(
3708 TemplateInstanceId(0),
3709 template,
3710 ivec2(0, 0),
3711 ));
3712 let node = bloq.add_node(node);
3713 bloq.validate().unwrap();
3714 bloq.node_mut(node)
3715 .unwrap()
3716 .expect_quantum_mut()
3717 .instances
3718 .push(TemplateInstance::new(
3719 TemplateInstanceId(1),
3720 template,
3721 ivec2(1, 0),
3722 ));
3723 bloq.validate().unwrap();
3724 assert!(
3725 bloq.validate_with_options(&InstantiationOptions::default().with_boolean_limits(
3726 bloq_utils::boolean::BooleanLimits {
3727 max_steps: 100,
3728 ..bloq_utils::boolean::BooleanLimits::DEFAULT
3729 },
3730 ),)
3731 .unwrap_err()
3732 .is_resource_limited()
3733 );
3734 }
3735
3736 #[test]
3737 fn repeat_state_initial_cannot_reference_itself() {
3738 let (circuit, body) = empty_repeat_circuit();
3739 let template = BloqTemplate::with_parts(
3740 circuit,
3741 Vec::new(),
3742 vec![TemplateRepeatState {
3743 body,
3744 state: LoopStateId(0),
3745 initial: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
3746 next: DetectorParity::default(),
3747 }],
3748 Vec::new(),
3749 Vec::new(),
3750 );
3751 let mut bloq = Bloq::new();
3752 bloq.add_template(template);
3753
3754 assert!(matches!(
3755 bloq.validate().unwrap_err(),
3756 BloqValidationError::UnknownTemplateLoopState {
3757 state: LoopStateId(0),
3758 ..
3759 }
3760 ));
3761 }
3762
3763 #[test]
3764 fn repeat_state_initial_cannot_reference_measurement_from_its_body() {
3765 let measured = ivec2(0, 0);
3766 let mut circuit = CoordCircuit::new();
3767 circuit.register_measurement_id(0, measured);
3768 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
3769 basis: PauliBasis::Z,
3770 qubits: vec![measured],
3771 measurements: vec![0],
3772 flip_probability: 0.0,
3773 }]));
3774 circuit
3775 .body_mut(circuit.entry_body())
3776 .unwrap()
3777 .ops_mut()
3778 .push(Op::Repeat {
3779 body,
3780 repetitions: 2,
3781 });
3782 let mut template = BloqTemplate::new(circuit);
3783 template.repeat_states.push(TemplateRepeatState {
3784 body,
3785 state: LoopStateId(0),
3786 initial: DetectorParity::from_measurements([0]),
3787 next: DetectorParity::from_measurements([0]),
3788 });
3789 let mut bloq = Bloq::new();
3790 bloq.add_template(template);
3791
3792 assert!(matches!(
3793 bloq.validate().unwrap_err(),
3794 BloqValidationError::UnknownTemplateMeasurement { measurement: 0, .. }
3795 ));
3796 }
3797
3798 #[test]
3799 fn zero_count_repeat_skips_state_event_liveness() {
3800 let (mut circuit, body) = empty_repeat_circuit();
3801 let Op::Repeat { repetitions, .. } =
3802 &mut circuit.body_mut(circuit.entry_body()).unwrap().ops_mut()[0]
3803 else {
3804 panic!("fixture entry op is a repeat");
3805 };
3806 *repetitions = 0;
3807 let state = LoopStateId(0);
3808 let self_reference = DetectorParity::from_terms([DetectorTerm::LoopState(state)]);
3809 let template = BloqTemplate::with_parts(
3810 circuit,
3811 vec![TemplateDetector {
3812 scope: TemplateDetectorScope::RepeatBody { body },
3813 parity: self_reference.clone(),
3814 coords: None,
3815 }],
3816 vec![TemplateRepeatState {
3817 body,
3818 state,
3819 initial: self_reference.clone(),
3820 next: self_reference,
3821 }],
3822 Vec::new(),
3823 Vec::new(),
3824 );
3825 let mut bloq = Bloq::new();
3826 bloq.add_template(template);
3827
3828 bloq.validate()
3829 .expect("a zero-count repeat has no state-liveness events");
3830 }
3831
3832 #[test]
3833 fn zero_count_repeat_still_validates_measurement_references() {
3834 let (mut circuit, body) = empty_repeat_circuit();
3835 let Op::Repeat { repetitions, .. } =
3836 &mut circuit.body_mut(circuit.entry_body()).unwrap().ops_mut()[0]
3837 else {
3838 panic!("fixture entry op is a repeat");
3839 };
3840 *repetitions = 0;
3841 let template = BloqTemplate::with_parts(
3842 circuit,
3843 Vec::new(),
3844 vec![TemplateRepeatState {
3845 body,
3846 state: LoopStateId(0),
3847 initial: DetectorParity::from_measurements([99]),
3848 next: DetectorParity::default(),
3849 }],
3850 Vec::new(),
3851 Vec::new(),
3852 );
3853 let mut bloq = Bloq::new();
3854 bloq.add_template(template);
3855
3856 assert!(matches!(
3857 bloq.validate().unwrap_err(),
3858 BloqValidationError::UnknownTemplateMeasurement {
3859 measurement: 99,
3860 ..
3861 }
3862 ));
3863 }
3864
3865 #[test]
3866 fn repeat_state_initial_cannot_reference_a_peer_state() {
3867 let (circuit, body) = empty_repeat_circuit();
3868 let template = BloqTemplate::with_parts(
3869 circuit,
3870 Vec::new(),
3871 vec![
3872 TemplateRepeatState {
3873 body,
3874 state: LoopStateId(0),
3875 initial: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(1))]),
3876 next: DetectorParity::default(),
3877 },
3878 TemplateRepeatState {
3879 body,
3880 state: LoopStateId(1),
3881 initial: DetectorParity::default(),
3882 next: DetectorParity::default(),
3883 },
3884 ],
3885 Vec::new(),
3886 Vec::new(),
3887 );
3888 let mut bloq = Bloq::new();
3889 bloq.add_template(template);
3890
3891 assert!(matches!(
3892 bloq.validate().unwrap_err(),
3893 BloqValidationError::UnknownTemplateLoopState {
3894 state: LoopStateId(1),
3895 ..
3896 }
3897 ));
3898 }
3899
3900 #[test]
3901 fn repeat_state_recurrence_and_post_loop_refs_validate() {
3902 let (circuit, body) = empty_repeat_circuit();
3903 let state = LoopStateId(0);
3904 let parity = DetectorParity::from_terms([DetectorTerm::LoopState(state)]);
3905 let template = BloqTemplate::with_parts(
3906 circuit,
3907 vec![
3908 TemplateDetector {
3909 scope: TemplateDetectorScope::RepeatBody { body },
3910 parity: parity.clone(),
3911 coords: None,
3912 },
3913 TemplateDetector {
3914 scope: TemplateDetectorScope::TopLevel,
3915 parity: parity.clone(),
3916 coords: None,
3917 },
3918 ],
3919 vec![TemplateRepeatState {
3920 body,
3921 state,
3922 initial: DetectorParity::default(),
3923 next: parity.clone(),
3924 }],
3925 Vec::new(),
3926 vec![TemplateRestart { parity }],
3927 );
3928 let mut bloq = Bloq::new();
3929 bloq.add_template(template);
3930
3931 bloq.validate()
3932 .expect("current-state recurrence and post-loop refs are live");
3933 }
3934
3935 #[test]
3936 fn repeat_state_next_cannot_reference_a_later_loop() {
3937 let (circuit, first, second) = two_repeat_circuit();
3938 let template = BloqTemplate::with_parts(
3939 circuit,
3940 Vec::new(),
3941 vec![
3942 TemplateRepeatState {
3943 body: first,
3944 state: LoopStateId(0),
3945 initial: DetectorParity::default(),
3946 next: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(1))]),
3947 },
3948 TemplateRepeatState {
3949 body: second,
3950 state: LoopStateId(1),
3951 initial: DetectorParity::default(),
3952 next: DetectorParity::default(),
3953 },
3954 ],
3955 Vec::new(),
3956 Vec::new(),
3957 );
3958 let mut bloq = Bloq::new();
3959 bloq.add_template(template);
3960
3961 assert!(matches!(
3962 bloq.validate().unwrap_err(),
3963 BloqValidationError::UnknownTemplateLoopState {
3964 state: LoopStateId(1),
3965 ..
3966 }
3967 ));
3968 }
3969
3970 #[test]
3971 fn repeat_body_detector_cannot_reference_a_later_loop() {
3972 let (circuit, first, second) = two_repeat_circuit();
3973 let template = BloqTemplate::with_parts(
3974 circuit,
3975 vec![TemplateDetector {
3976 scope: TemplateDetectorScope::RepeatBody { body: first },
3977 parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(1))]),
3978 coords: None,
3979 }],
3980 vec![TemplateRepeatState {
3981 body: second,
3982 state: LoopStateId(1),
3983 initial: DetectorParity::default(),
3984 next: DetectorParity::default(),
3985 }],
3986 Vec::new(),
3987 Vec::new(),
3988 );
3989 let mut bloq = Bloq::new();
3990 bloq.add_template(template);
3991
3992 assert!(matches!(
3993 bloq.validate().unwrap_err(),
3994 BloqValidationError::UnknownTemplateLoopState {
3995 state: LoopStateId(1),
3996 ..
3997 }
3998 ));
3999 }
4000
4001 #[test]
4002 fn restart_cannot_reference_state_dropped_with_enclosing_loop() {
4003 let mut circuit = CoordCircuit::new();
4004 let inner = circuit.add_body(CircuitBody::new());
4005 let outer = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
4006 body: inner,
4007 repetitions: 2,
4008 }]));
4009 circuit
4010 .body_mut(circuit.entry_body())
4011 .unwrap()
4012 .ops_mut()
4013 .push(Op::Repeat {
4014 body: outer,
4015 repetitions: 2,
4016 });
4017 let template = BloqTemplate::with_parts(
4018 circuit,
4019 Vec::new(),
4020 vec![TemplateRepeatState {
4021 body: inner,
4022 state: LoopStateId(0),
4023 initial: DetectorParity::default(),
4024 next: DetectorParity::default(),
4025 }],
4026 Vec::new(),
4027 vec![TemplateRestart {
4028 parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
4029 }],
4030 );
4031 let mut bloq = Bloq::new();
4032 bloq.add_template(template);
4033
4034 assert!(matches!(
4035 bloq.validate().unwrap_err(),
4036 BloqValidationError::UnknownTemplateLoopState {
4037 state: LoopStateId(0),
4038 ..
4039 }
4040 ));
4041 }
4042
4043 #[test]
4044 fn validate_rejects_dangling_boundary_flow_measurement() {
4045 let q = ivec2(0, 0);
4046 let mut template_circuit = CoordCircuit::new();
4047 template_circuit.measure(PauliBasis::Z, [q]); let mut template = BloqTemplate::new(template_circuit);
4049 template.boundary_flows.push(
4050 bloq_circuit::Flow::new(
4051 bloq_circuit::PauliMap::empty(),
4052 bloq_circuit::PauliMap::empty(),
4053 )
4054 .with_measurements([1u32]),
4055 );
4056
4057 let mut graph = Bloq::new();
4058 let template_id = graph.add_template(template);
4059
4060 let error = graph.validate().unwrap_err();
4061 assert!(matches!(
4062 error,
4063 BloqValidationError::UnknownTemplateMeasurement {
4064 template,
4065 measurement: 1,
4066 } if template == template_id
4067 ));
4068 }
4069
4070 #[test]
4071 fn validate_rejects_future_accumulate_measurement() {
4072 let q = ivec2(0, 0);
4073 let mut template_circuit = CoordCircuit::new();
4074 template_circuit.measure(PauliBasis::Z, [q]);
4075 let mut graph = Bloq::new();
4076 let template = graph.add_template(BloqTemplate::new(template_circuit));
4077
4078 let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
4080 index: None,
4081 operators: Vec::new(),
4082 measurements: vec![instance_measurement(1, 0)],
4083 }));
4084
4085 let mut producer_node = node(ivec3(0, 0, 1));
4086 producer_node
4087 .expect_quantum_mut()
4088 .instances
4089 .push(TemplateInstance::new(TemplateInstanceId(1), template, q));
4090 let producer = graph.add_node(producer_node);
4091 let _ = accumulate;
4092
4093 let error = graph.validate().unwrap_err();
4094 assert!(matches!(
4095 error,
4096 BloqValidationError::FutureMeasurement {
4097 owner,
4098 measurement: 0,
4099 ..
4100 } if owner == producer
4101 ));
4102 graph.node_mut(accumulate).unwrap().kind = crate::BloqNodeKind::Classical(
4103 ClassicalNode::Observable {
4104 index: Some(0),
4105 measurements: vec![instance_measurement(1, 0)],
4106 operators: Vec::new(),
4107 }
4108 .into(),
4109 );
4110 assert!(matches!(
4111 graph.validate(),
4112 Err(BloqValidationError::FutureMeasurement { owner, .. }) if owner == producer
4113 ));
4114 graph.add_edge(producer, accumulate, BloqEdge::Order);
4115 graph.validate().unwrap();
4116 }
4117
4118 #[test]
4119 fn node_detector_requires_path_from_cross_node_measurement_owner() {
4120 let q = ivec2(0, 0);
4121 let mut circuit = CoordCircuit::new();
4122 circuit.measure(PauliBasis::Z, [q]);
4123 let mut graph = Bloq::new();
4124 let template = graph.add_template(BloqTemplate::new(circuit));
4125
4126 let mut producer_node = node(ivec3(0, 0, 1));
4127 producer_node
4128 .expect_quantum_mut()
4129 .instances
4130 .push(TemplateInstance::new(TemplateInstanceId(1), template, q));
4131 let producer = graph.add_node(producer_node);
4132 let mut consumer_node = node(ivec3(1, 0, 0));
4133 consumer_node
4134 .expect_quantum_mut()
4135 .detectors
4136 .push(NodeDetector {
4137 parity: DetectorParity::from_measurements([instance_measurement(1, 0)]),
4138 coords: None,
4139 });
4140 let consumer = graph.add_node(consumer_node);
4141
4142 assert_eq!(
4143 graph.validate().unwrap_err(),
4144 BloqValidationError::UnorderedNodeMeasurement {
4145 node: consumer,
4146 owner: producer,
4147 measurement: 0,
4148 }
4149 );
4150
4151 graph.add_edge(producer, consumer, BloqEdge::Order);
4152 graph.validate().unwrap();
4153 }
4154
4155 #[test]
4156 fn bundle_detector_requires_path_from_cross_node_measurement_owner() {
4157 use crate::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
4158 let mut circuit = CoordCircuit::new();
4159 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
4160 let mut program = Bloq::new();
4161 let template = program.add_template(BloqTemplate::new(circuit));
4162 let bundle = program.add_detector_bundle(DetectorBundle::new(
4163 vec![template],
4164 vec![BundleDetector {
4165 parity: DetectorParity::from_measurements([BundleMeasurement {
4166 owner: 0,
4167 measurement: 0,
4168 }]),
4169 coords: None,
4170 }],
4171 ));
4172 let mut producer = node(ivec3(0, 0, 0));
4173 producer
4174 .expect_quantum_mut()
4175 .instances
4176 .push(TemplateInstance::new(
4177 TemplateInstanceId(1),
4178 template,
4179 ivec2(0, 0),
4180 ));
4181 let producer = program.add_node(producer);
4182 let mut consumer = node(ivec3(1, 0, 0));
4183 consumer
4184 .expect_quantum_mut()
4185 .detector_bundles
4186 .push(DetectorBundleUse {
4187 bundle,
4188 instances: vec![TemplateInstanceId(1)],
4189 offset: ivec2(0, 0),
4190 });
4191 let consumer = program.add_node(consumer);
4192 assert!(
4193 matches!(program.validate(), Err(BloqValidationError::UnavailableDetectorBundleOwner { node, instance: TemplateInstanceId(1) }) if node == consumer)
4194 );
4195 program.add_edge(producer, consumer, BloqEdge::Order);
4196 program.validate().unwrap();
4197 }
4198
4199 #[test]
4200 fn detector_ancestor_search_matches_forward_reachability() {
4201 let mut graph = SubGraph::new();
4202 let nodes = (0..10)
4203 .map(|_| {
4204 graph.add_node(BloqNode::classical(ClassicalNode::Compute {
4205 expr: ClassicalExpr::Const(false),
4206 }))
4207 })
4208 .collect::<Vec<_>>();
4209 for (source, target) in [
4210 (0, 1),
4211 (1, 2),
4212 (0, 3),
4213 (3, 4),
4214 (4, 5),
4215 (2, 5),
4216 (6, 7),
4217 (7, 8),
4218 ] {
4219 graph.add_edge(nodes[source], nodes[target], BloqEdge::Order);
4220 }
4221 graph.remove_node(nodes[9]);
4222 for _ in 0..64 {
4223 let leaf = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
4224 index: None,
4225 operators: Vec::new(),
4226 measurements: Vec::new(),
4227 }));
4228 graph.add_edge(nodes[0], leaf, BloqEdge::Order);
4229 }
4230 let mut scratch = graph.path_scratch();
4231 for source in graph.node_ids() {
4232 for target in graph.node_ids() {
4233 assert_eq!(
4234 graph.has_path_backwards_with_scratch(source, target, &mut scratch),
4235 graph.has_path(source, target),
4236 "{source:?} -> {target:?}"
4237 );
4238 }
4239 }
4240 }
4241
4242 #[test]
4243 fn validate_checks_unused_template_repeat_state_body_refs() {
4244 let mut graph = Bloq::new();
4245 let mut circuit = CoordCircuit::new();
4246 let orphan = circuit.add_body(CircuitBody::new());
4247 let mut template = BloqTemplate::new(circuit);
4248 template.repeat_states.push(TemplateRepeatState {
4249 body: orphan,
4250 state: LoopStateId(0),
4251 initial: DetectorParity::default(),
4252 next: DetectorParity::default(),
4253 });
4254 graph.add_template(template);
4255 graph.add_node(node(ivec3(0, 0, 0)));
4256
4257 let error = graph.validate().unwrap_err();
4258 assert!(matches!(
4259 error,
4260 BloqValidationError::UnknownTemplateBody {
4261 body,
4262 ..
4263 } if body == orphan
4264 ));
4265 }
4266
4267 #[test]
4268 fn repeat_body_detector_on_non_repeat_body_is_rejected() {
4269 let circuit = CoordCircuit::new();
4275 let entry = circuit.entry_body();
4276 let template = BloqTemplate::with_parts(
4277 circuit,
4278 vec![TemplateDetector {
4279 scope: TemplateDetectorScope::RepeatBody { body: entry },
4280 parity: DetectorParity::default(),
4281 coords: None,
4282 }],
4283 Vec::new(),
4284 Vec::new(),
4285 Vec::new(),
4286 );
4287 let mut bloq = Bloq::new();
4288 bloq.add_template(template);
4289
4290 assert!(matches!(
4291 bloq.validate().unwrap_err(),
4292 BloqValidationError::UnknownTemplateBody { body, .. } if body == entry
4293 ));
4294 }
4295
4296 #[test]
4297 fn validate_rejects_empty_mpp_in_unused_template_without_panicking() {
4298 let q = ivec2(0, 0);
4299 let mut circuit = CoordCircuit::new();
4300 circuit
4301 .body_mut(circuit.entry_body())
4302 .unwrap()
4303 .ops_mut()
4304 .push(Op::MPP {
4305 products: vec![PauliMap::empty()],
4306 measurements: vec![0],
4307 });
4308 circuit.register_measurement_id(0, q);
4309 let mut bloq = Bloq::new();
4310 bloq.add_template(BloqTemplate::new(circuit));
4311
4312 assert!(matches!(
4313 bloq.validate().unwrap_err(),
4314 BloqValidationError::InvalidTemplateCircuit {
4315 source: NodeTemplateInstanceMergeError::EmptyPauliProduct,
4316 ..
4317 }
4318 ));
4319 }
4320
4321 #[test]
4322 fn validate_rejects_measure_output_count_mismatch_in_unused_template() {
4323 let q = ivec2(0, 0);
4324 let mut circuit = CoordCircuit::new();
4325 circuit.register_measurement_id(0, q);
4326 circuit
4327 .body_mut(circuit.entry_body())
4328 .unwrap()
4329 .ops_mut()
4330 .push(Op::Measure {
4331 basis: PauliBasis::Z,
4332 qubits: vec![q],
4333 measurements: vec![],
4334 flip_probability: 0.0,
4335 });
4336
4337 assert_eq!(
4338 invalid_template_circuit_source(circuit),
4339 NodeTemplateInstanceMergeError::MeasureOutputCountMismatch {
4340 qubits: 1,
4341 measurements: 0,
4342 }
4343 );
4344 }
4345
4346 #[test]
4347 fn validate_rejects_odd_two_qubit_target_count_in_unused_template() {
4348 let mut circuit = CoordCircuit::new();
4349 circuit
4350 .body_mut(circuit.entry_body())
4351 .unwrap()
4352 .ops_mut()
4353 .push(Op::Gate {
4354 gate: GateType::CX,
4355 qubits: vec![ivec2(0, 0)],
4356 });
4357
4358 assert_eq!(
4359 invalid_template_circuit_source(circuit),
4360 NodeTemplateInstanceMergeError::OddTwoQubitTargetCount {
4361 gate: GateType::CX,
4362 targets: 1,
4363 }
4364 );
4365 }
4366
4367 #[test]
4368 fn validate_rejects_unregistered_measure_and_mpp_outputs() {
4369 let q = ivec2(0, 0);
4370 let mut measure = CoordCircuit::new();
4371 measure
4372 .body_mut(measure.entry_body())
4373 .unwrap()
4374 .ops_mut()
4375 .push(Op::Measure {
4376 basis: PauliBasis::Z,
4377 qubits: vec![q],
4378 measurements: vec![7],
4379 flip_probability: 0.0,
4380 });
4381 assert_eq!(
4382 invalid_template_circuit_source(measure),
4383 NodeTemplateInstanceMergeError::UnregisteredMeasurementOutput(7)
4384 );
4385
4386 let mut mpp = CoordCircuit::new();
4387 mpp.body_mut(mpp.entry_body())
4388 .unwrap()
4389 .ops_mut()
4390 .push(Op::MPP {
4391 products: vec![[(q, Pauli::X)].into_iter().collect()],
4392 measurements: vec![9],
4393 });
4394 assert_eq!(
4395 invalid_template_circuit_source(mpp),
4396 NodeTemplateInstanceMergeError::UnregisteredMeasurementOutput(9)
4397 );
4398 }
4399
4400 #[test]
4401 fn validate_rejects_measurement_id_produced_by_multiple_ops() {
4402 let q = ivec2(0, 0);
4403 let mut circuit = CoordCircuit::new();
4404 circuit.register_measurement_id(0, q);
4405 circuit
4406 .body_mut(circuit.entry_body())
4407 .unwrap()
4408 .ops_mut()
4409 .extend([
4410 Op::Measure {
4411 basis: PauliBasis::Z,
4412 qubits: vec![q],
4413 measurements: vec![0],
4414 flip_probability: 0.0,
4415 },
4416 Op::MPP {
4417 products: vec![[(q, Pauli::X)].into_iter().collect()],
4418 measurements: vec![0],
4419 },
4420 ]);
4421
4422 assert_eq!(
4423 invalid_template_circuit_source(circuit),
4424 NodeTemplateInstanceMergeError::DuplicateMeasurementOutput(0)
4425 );
4426 }
4427
4428 #[test]
4429 fn validate_rejects_measure_and_mpp_output_coordinate_mismatches() {
4430 let produced = ivec2(0, 0);
4431 let registered = ivec2(1, 0);
4432 let mut measure = CoordCircuit::new();
4433 measure.register_measurement_id(0, registered);
4434 measure
4435 .body_mut(measure.entry_body())
4436 .unwrap()
4437 .ops_mut()
4438 .push(Op::Measure {
4439 basis: PauliBasis::Z,
4440 qubits: vec![produced],
4441 measurements: vec![0],
4442 flip_probability: 0.0,
4443 });
4444 assert_eq!(
4445 invalid_template_circuit_source(measure),
4446 NodeTemplateInstanceMergeError::MeasurementOutputCoordinateMismatch {
4447 measurement: 0,
4448 registered,
4449 produced,
4450 }
4451 );
4452
4453 let mut mpp = CoordCircuit::new();
4454 mpp.register_measurement_id(0, registered);
4455 mpp.body_mut(mpp.entry_body())
4456 .unwrap()
4457 .ops_mut()
4458 .push(Op::MPP {
4459 products: vec![[(produced, Pauli::X)].into_iter().collect()],
4460 measurements: vec![0],
4461 });
4462 assert_eq!(
4463 invalid_template_circuit_source(mpp),
4464 NodeTemplateInstanceMergeError::MeasurementOutputCoordinateMismatch {
4465 measurement: 0,
4466 registered,
4467 produced,
4468 }
4469 );
4470 }
4471
4472 #[test]
4473 fn validate_rejects_cyclic_repeat_body_in_unused_template() {
4474 let mut circuit = CoordCircuit::new();
4475 let entry = circuit.entry_body();
4476 circuit.body_mut(entry).unwrap().ops_mut().push(Op::Repeat {
4477 body: entry,
4478 repetitions: 2,
4479 });
4480 let mut bloq = Bloq::new();
4481 bloq.add_template(BloqTemplate::new(circuit));
4482
4483 assert!(matches!(
4484 bloq.validate().unwrap_err(),
4485 BloqValidationError::InvalidTemplateCircuit {
4486 source: NodeTemplateInstanceMergeError::CyclicBody(BodyId(0)),
4487 ..
4488 }
4489 ));
4490 }
4491
4492 #[test]
4493 fn discard_inside_rus_body_is_rejected() {
4494 let mut body = SubGraph::new();
4495 let producer = body.add_node(BloqNode::classical(ClassicalNode::Compute {
4496 expr: ClassicalExpr::Const(false),
4497 }));
4498 let discard = body.add_node(BloqNode::classical(ClassicalNode::Discard {
4499 condition: ClassicalExpr::In(0),
4500 }));
4501 body.add_edge(producer, discard, BloqEdge::value(0));
4502 let mut bloq = Bloq::new();
4503 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4504 body,
4505 restart_condition: ClassicalExpr::Const(false),
4506 restart_source: Some(producer.into()),
4507 }));
4508 assert!(matches!(
4509 bloq.validate().unwrap_err(),
4510 BloqValidationError::DiscardInsideRepeatUntilSuccess { .. }
4511 ));
4512 }
4513
4514 #[test]
4515 fn nested_rus_is_rejected() {
4516 let mut inner_body = SubGraph::new();
4517 let inner_restart = inner_body.add_node(BloqNode::classical(ClassicalNode::Compute {
4518 expr: ClassicalExpr::Const(false),
4519 }));
4520 let inner = BloqNode::region(RegionNode::RepeatUntilSuccess {
4521 body: inner_body,
4522 restart_condition: ClassicalExpr::Const(false),
4523 restart_source: Some(inner_restart.into()),
4524 });
4525
4526 let mut outer_body = SubGraph::new();
4527 let outer_restart = outer_body.add_node(BloqNode::classical(ClassicalNode::Compute {
4528 expr: ClassicalExpr::Const(false),
4529 }));
4530 outer_body.add_node(inner);
4531 let mut bloq = Bloq::new();
4532 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4533 body: outer_body,
4534 restart_condition: ClassicalExpr::Const(false),
4535 restart_source: Some(outer_restart.into()),
4536 }));
4537
4538 assert!(matches!(
4539 bloq.validate().unwrap_err(),
4540 BloqValidationError::NestedRepeatUntilSuccess { .. }
4541 ));
4542 }
4543
4544 #[test]
4545 fn retry_wrapped_include_validates() {
4546 let mut bloq = Bloq::new();
4548 let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
4549 let mut producer = node(ivec3(0, 0, 0));
4550 producer
4551 .expect_quantum_mut()
4552 .instances
4553 .push(TemplateInstance::new(
4554 TemplateInstanceId(0),
4555 template,
4556 ivec2(0, 0),
4557 ));
4558 bloq.add_node(producer);
4559
4560 let mut body = SubGraph::new();
4561 body.add_node(BloqNode::classical(ClassicalNode::Observable {
4562 index: None,
4563 measurements: Vec::new(),
4564 operators: vec![InstanceBoundaryOperator {
4565 instance: TemplateInstanceId(0),
4566 face: BoundaryFace::Output,
4567 operator: bloq_circuit::PauliMap::empty(),
4568 }],
4569 }));
4570 body.add_node(BloqNode::classical(ClassicalNode::Observable {
4571 index: None,
4572 operators: Vec::new(),
4573 measurements: vec![],
4574 }));
4575 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4576 restart_source: None,
4577 restart_condition: ClassicalExpr::Const(false),
4578 body,
4579 }));
4580 bloq.validate().unwrap();
4581 }
4582
4583 #[test]
4584 fn boundary_payload_with_unknown_instance_is_rejected_at_every_level() {
4585 for nested in [false, true] {
4586 let operators = vec![InstanceBoundaryOperator {
4587 instance: TemplateInstanceId(99),
4588 face: BoundaryFace::Input,
4589 operator: PauliMap::empty(),
4590 }];
4591 for classical in [
4592 ClassicalNode::Observable {
4593 index: None,
4594 measurements: Vec::new(),
4595 operators: operators.clone(),
4596 },
4597 ClassicalNode::Observable {
4598 index: Some(0),
4599 measurements: Vec::new(),
4600 operators,
4601 },
4602 ] {
4603 let include = BloqNode::classical(classical);
4604 let mut bloq = Bloq::new();
4605 if nested {
4606 let mut body = SubGraph::new();
4607 body.add_node(include);
4608 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4609 restart_source: None,
4610 restart_condition: ClassicalExpr::Const(false),
4611 body,
4612 }));
4613 } else {
4614 bloq.add_node(include);
4615 }
4616
4617 assert!(matches!(
4618 bloq.validate().unwrap_err(),
4619 BloqValidationError::UnknownTemplateInstance {
4620 instance: TemplateInstanceId(99),
4621 ..
4622 }
4623 ));
4624 }
4625 }
4626 }
4627
4628 #[test]
4629 fn template_restart_with_unknown_measurement_is_rejected() {
4630 let q = ivec2(0, 0);
4631 let mut template_circuit = CoordCircuit::new();
4632 template_circuit.measure(PauliBasis::Z, [q]); let mut template = BloqTemplate::new(template_circuit);
4634 template.restarts.push(TemplateRestart {
4635 parity: DetectorParity::from_measurements([1u32]),
4636 });
4637
4638 let mut graph = Bloq::new();
4639 let template_id = graph.add_template(template);
4640
4641 let error = graph.validate().unwrap_err();
4642 assert!(matches!(
4643 error,
4644 BloqValidationError::UnknownTemplateMeasurement {
4645 template,
4646 measurement: 1,
4647 } if template == template_id
4648 ));
4649 }
4650
4651 #[test]
4652 fn rus_region_with_restart_condition_and_template_restarts_validates() {
4653 let q = ivec2(0, 0);
4655 let mut template_circuit = CoordCircuit::new();
4656 template_circuit.measure(PauliBasis::Z, [q]); let mut template = BloqTemplate::new(template_circuit);
4658 template.restarts.push(TemplateRestart {
4659 parity: DetectorParity::from_measurements([0u32]),
4660 });
4661
4662 let mut graph = Bloq::new();
4663 let template_id = graph.add_template(template);
4664
4665 let mut body = SubGraph::new();
4666 let mut inst_node = node(ivec3(0, 0, 0));
4667 inst_node
4668 .expect_quantum_mut()
4669 .instances
4670 .push(TemplateInstance::new(TemplateInstanceId(0), template_id, q));
4671 let producer = body.add_node(inst_node);
4672 let accumulate = body.add_node(BloqNode::classical(ClassicalNode::Observable {
4673 index: None,
4674 operators: Vec::new(),
4675 measurements: vec![instance_measurement(0, 0)],
4676 }));
4677 body.add_edge(producer, accumulate, BloqEdge::Order);
4678
4679 let selector = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
4680 index: None,
4681 operators: Vec::new(),
4682 measurements: vec![],
4683 }));
4684 let region = graph.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4685 body,
4686 restart_condition: ClassicalExpr::In(0),
4687 restart_source: Some(accumulate.into()),
4688 }));
4689 graph.add_edge(selector, region, BloqEdge::value(0));
4690
4691 graph.validate().unwrap();
4692 }
4693
4694 fn rus_bloq_with_node_restart(measurement: u32) -> Bloq {
4696 let q = ivec2(0, 0);
4697 let mut template_circuit = CoordCircuit::new();
4698 template_circuit.measure(PauliBasis::Z, [q]); let mut graph = Bloq::new();
4700 let template_id = graph.add_template(BloqTemplate::new(template_circuit));
4701
4702 let mut body = SubGraph::new();
4703 let mut inst_node = node(ivec3(0, 0, 0));
4704 inst_node
4705 .expect_quantum_mut()
4706 .instances
4707 .push(TemplateInstance::new(TemplateInstanceId(0), template_id, q));
4708 inst_node.expect_quantum_mut().restarts.push(NodeRestart {
4709 parity: DetectorParity::from_measurements([InstanceMeasurement {
4710 instance: TemplateInstanceId(0),
4711 measurement,
4712 }]),
4713 });
4714 body.add_node(inst_node);
4715 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Observable {
4716 index: None,
4717 operators: Vec::new(),
4718 measurements: vec![],
4719 }));
4720 graph.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4721 body,
4722 restart_condition: ClassicalExpr::Const(false),
4723 restart_source: Some(restart_source.into()),
4724 }));
4725 graph
4726 }
4727
4728 #[test]
4729 fn node_restart_inside_rus_validates() {
4730 rus_bloq_with_node_restart(0).validate().unwrap();
4731 }
4732
4733 #[test]
4734 fn node_restart_with_unknown_measurement_is_rejected() {
4735 assert!(matches!(
4736 rus_bloq_with_node_restart(1) .validate()
4738 .unwrap_err(),
4739 BloqValidationError::UnknownInstanceMeasurement { measurement: 1, .. }
4740 ));
4741 }
4742
4743 #[test]
4744 fn node_restart_outside_rus_is_rejected() {
4745 let mut graph = Bloq::new();
4746 let mut restart_node = node(ivec3(0, 0, 0));
4747 restart_node
4748 .expect_quantum_mut()
4749 .restarts
4750 .push(NodeRestart {
4751 parity: DetectorParity::default(),
4752 });
4753 graph.add_node(restart_node);
4754 assert!(matches!(
4755 graph.validate().unwrap_err(),
4756 BloqValidationError::RestartOutsideRepeatUntilSuccess { .. }
4757 ));
4758 }
4759
4760 #[test]
4761 fn restart_template_instantiated_outside_rus_is_rejected() {
4762 let q = ivec2(0, 0);
4763 let mut template_circuit = CoordCircuit::new();
4764 template_circuit.measure(PauliBasis::Z, [q]);
4765 let mut template = BloqTemplate::new(template_circuit);
4766 template.restarts.push(TemplateRestart {
4767 parity: DetectorParity::from_measurements([0u32]),
4768 });
4769
4770 let mut graph = Bloq::new();
4771 let template_id = graph.add_template(template);
4772 let mut inst_node = node(ivec3(0, 0, 0));
4773 inst_node
4774 .expect_quantum_mut()
4775 .instances
4776 .push(TemplateInstance::new(TemplateInstanceId(0), template_id, q));
4777 graph.add_node(inst_node);
4778 assert!(matches!(
4779 graph.validate().unwrap_err(),
4780 BloqValidationError::RestartOutsideRepeatUntilSuccess { .. }
4781 ));
4782 }
4783
4784 #[test]
4785 fn indexed_observable_exposes_corrected_and_flip_values() {
4786 let mut bloq = Bloq::new();
4787 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(3)));
4788 for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
4789 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
4790 expr: ClassicalExpr::In(0),
4791 }));
4792 bloq.add_edge(
4793 observable,
4794 consumer,
4795 BloqEdge::Value {
4796 slot: 0,
4797 role: crate::ValueRole::Data,
4798 output,
4799 },
4800 );
4801 }
4802 bloq.validate().unwrap();
4803 }
4804
4805 #[test]
4806 fn flip_requires_an_indexed_observable() {
4807 for producer in [
4808 ClassicalNode::observable_fragment(Vec::new(), Vec::new()),
4809 ClassicalNode::Compute {
4810 expr: ClassicalExpr::Const(false),
4811 },
4812 ] {
4813 let mut bloq = Bloq::new();
4814 let source = bloq.add_node(BloqNode::classical(producer));
4815 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
4816 expr: ClassicalExpr::In(0),
4817 }));
4818 bloq.add_edge(
4819 source,
4820 consumer,
4821 BloqEdge::Value {
4822 slot: 0,
4823 role: crate::ValueRole::Data,
4824 output: ObservableOutput::Flip,
4825 },
4826 );
4827 assert!(
4828 matches!(bloq.validate(), Err(BloqValidationError::InvalidValueProducer { producer, .. }) if producer == source)
4829 );
4830 bloq.top_mut().set_value_output(Some(ValueRef {
4831 node: source,
4832 output: ObservableOutput::Flip,
4833 }));
4834 bloq.remove_node(consumer);
4835 assert!(matches!(
4836 bloq.validate(),
4837 Err(BloqValidationError::InvalidValueOutput { .. })
4838 ));
4839 }
4840 }
4841
4842 #[test]
4843 fn value_edge_into_quantum_node_is_rejected() {
4844 let mut bloq = Bloq::new();
4845 let quantum = bloq.add_node(node(ivec3(0, 0, 0)));
4846 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
4847 expr: ClassicalExpr::Const(true),
4848 }));
4849 bloq.add_edge(producer, quantum, BloqEdge::value(0));
4850 assert!(matches!(
4851 bloq.validate().unwrap_err(),
4852 BloqValidationError::UnusedQuantumValueInput { node, slot: 0 }
4853 if node == quantum
4854 ));
4855 }
4856
4857 #[test]
4858 fn duplicate_observable_index_is_rejected() {
4859 let mut bloq = Bloq::new();
4860 bloq.add_node(BloqNode::classical(ClassicalNode::observable(5)));
4861 bloq.add_node(BloqNode::classical(ClassicalNode::observable(5)));
4862 assert!(matches!(
4863 bloq.validate().unwrap_err(),
4864 BloqValidationError::DuplicateObservableIndex { index: 5, .. }
4865 ));
4866 }
4867
4868 #[test]
4869 fn duplicate_observable_index_across_region_scopes_is_rejected() {
4870 let mut body = SubGraph::new();
4871 body.add_node(BloqNode::classical(ClassicalNode::observable(5)));
4872 let mut bloq = Bloq::new();
4873 bloq.add_node(BloqNode::classical(ClassicalNode::observable(5)));
4874 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4875 restart_source: None,
4876 restart_condition: ClassicalExpr::Const(false),
4877 body,
4878 }));
4879 assert!(matches!(
4880 bloq.validate().unwrap_err(),
4881 BloqValidationError::DuplicateObservableIndex { index: 5, .. }
4882 ));
4883 }
4884
4885 fn bloq_with_top_level_ref_into_rus_body(
4887 instance: TemplateInstanceId,
4888 measurement: u32,
4889 ) -> Bloq {
4890 let q = ivec2(0, 0);
4891 let mut template_circuit = CoordCircuit::new();
4892 template_circuit.measure(PauliBasis::Z, [q]); let mut bloq = Bloq::new();
4894 let template_id = bloq.add_template(BloqTemplate::new(template_circuit));
4895
4896 let mut body = SubGraph::new();
4897 let mut inst_node = node(ivec3(0, 0, 0));
4898 inst_node
4899 .expect_quantum_mut()
4900 .instances
4901 .push(TemplateInstance::new(TemplateInstanceId(0), template_id, q));
4902 body.add_node(inst_node);
4903 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Observable {
4904 index: None,
4905 operators: Vec::new(),
4906 measurements: vec![],
4907 }));
4908 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
4909 body,
4910 restart_condition: ClassicalExpr::Const(false),
4911 restart_source: Some(restart_source.into()),
4912 }));
4913
4914 let mut neighbor = node(ivec3(0, 0, 1));
4915 neighbor
4916 .expect_quantum_mut()
4917 .detectors
4918 .push(crate::NodeDetector {
4919 parity: DetectorParity::from_measurements([InstanceMeasurement {
4920 instance,
4921 measurement,
4922 }]),
4923 coords: None,
4924 });
4925 let neighbor = bloq.add_node(neighbor);
4926 bloq.add_edge(region, neighbor, BloqEdge::Order);
4927 bloq
4928 }
4929
4930 #[test]
4931 fn top_level_detector_referencing_rus_body_instance_validates() {
4932 bloq_with_top_level_ref_into_rus_body(TemplateInstanceId(0), 0)
4933 .validate()
4934 .unwrap();
4935 }
4936
4937 #[test]
4940 fn emission_plans_match_the_validating_path() {
4941 let bloq = bloq_with_top_level_ref_into_rus_body(TemplateInstanceId(0), 0);
4942 let options = InstantiationOptions::default();
4943 let validated = bloq
4944 .validate_with_plans(&options)
4945 .expect("program is valid");
4946 let plans_only = bloq.emission_plans(&options).expect("plans build");
4947
4948 assert_eq!(validated.len(), plans_only.len());
4949 assert!(!plans_only.is_empty(), "fixture has quantum nodes to plan");
4950 for ((path, node), plan) in validated.iter() {
4951 let other = plans_only
4955 .get(path, node)
4956 .unwrap_or_else(|| panic!("checkless path is missing {path:?}/{node:?}"));
4957 assert_eq!(plan.measurements, other.measurements);
4958 assert_eq!(plan.bodies, other.bodies);
4959 assert_eq!(
4960 plan.circuit.num_measurements(),
4961 other.circuit.num_measurements()
4962 );
4963 assert_eq!(plan.circuit.body_count(), other.circuit.body_count());
4964 }
4965 }
4966
4967 #[test]
4968 fn top_level_detector_referencing_unknown_instance_is_rejected() {
4969 assert!(matches!(
4970 bloq_with_top_level_ref_into_rus_body(TemplateInstanceId(7), 0)
4971 .validate()
4972 .unwrap_err(),
4973 BloqValidationError::UnknownTemplateInstance { .. }
4974 ));
4975 }
4976
4977 #[test]
4978 fn top_level_detector_with_out_of_range_body_measurement_is_rejected() {
4979 assert!(matches!(
4980 bloq_with_top_level_ref_into_rus_body(TemplateInstanceId(0), 1)
4981 .validate()
4982 .unwrap_err(),
4983 BloqValidationError::UnknownInstanceMeasurement { measurement: 1, .. }
4984 ));
4985 }
4986
4987 #[test]
4988 fn duplicate_instance_id_across_region_boundary_is_rejected() {
4989 let q = ivec2(0, 0);
4990 let mut template_circuit = CoordCircuit::new();
4991 template_circuit.measure(PauliBasis::Z, [q]);
4992 let mut bloq = Bloq::new();
4993 let template_id = bloq.add_template(BloqTemplate::new(template_circuit));
4994 let instance = |offset| TemplateInstance::new(TemplateInstanceId(5), template_id, offset);
4995
4996 let mut top = node(ivec3(0, 0, 0));
4997 top.expect_quantum_mut().instances.push(instance(q));
4998 bloq.add_node(top);
4999
5000 let mut body = SubGraph::new();
5001 let mut inner = node(ivec3(1, 0, 0));
5002 inner
5003 .expect_quantum_mut()
5004 .instances
5005 .push(instance(ivec2(2, 0)));
5006 body.add_node(inner);
5007 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
5008 restart_source: None,
5009 restart_condition: ClassicalExpr::Const(false),
5010 body,
5011 }));
5012
5013 assert!(matches!(
5014 bloq.validate().unwrap_err(),
5015 BloqValidationError::DuplicateTemplateInstance {
5016 instance: TemplateInstanceId(5)
5017 }
5018 ));
5019 }
5020
5021 #[test]
5022 fn duplicated_in_leaf_is_rejected() {
5023 let mut bloq = Bloq::new();
5025 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5026 expr: ClassicalExpr::Const(false),
5027 }));
5028 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5029 expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(0)])),
5030 }));
5031 bloq.add_edge(producer, compute, BloqEdge::value(0));
5032 assert!(matches!(
5033 bloq.validate().unwrap_err(),
5034 BloqValidationError::DuplicateExprInput { node, slot: 0 } if node == compute
5035 ));
5036 }
5037
5038 #[test]
5039 fn value_edge_from_quantum_node_is_rejected() {
5040 let mut bloq = Bloq::new();
5041 let quantum = bloq.add_node(node(ivec3(0, 0, 0)));
5042 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5043 expr: ClassicalExpr::In(0),
5044 }));
5045 bloq.add_edge(quantum, compute, BloqEdge::value(0));
5046 assert!(matches!(
5047 bloq.validate().unwrap_err(),
5048 BloqValidationError::InvalidValueProducer { producer, .. } if producer == quantum
5049 ));
5050 }
5051
5052 #[test]
5053 fn value_edge_from_discard_is_rejected() {
5054 let mut bloq = Bloq::new();
5055 let discard = bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
5056 condition: ClassicalExpr::Const(false),
5057 }));
5058 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5059 expr: ClassicalExpr::In(0),
5060 }));
5061 bloq.add_edge(discard, compute, BloqEdge::value(0));
5062 assert!(matches!(
5063 bloq.validate().unwrap_err(),
5064 BloqValidationError::InvalidValueProducer { producer, .. } if producer == discard
5065 ));
5066 }
5067
5068 #[test]
5069 fn composition_requires_an_observable_consumer() {
5070 for compose in [false, true] {
5071 let mut bloq = Bloq::new();
5072 let fragment = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
5073 Vec::new(),
5074 Vec::new(),
5075 )));
5076 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
5077 bloq.add_edge(
5078 fragment,
5079 observable,
5080 if compose {
5081 BloqEdge::compose(0)
5082 } else {
5083 BloqEdge::value(0)
5084 },
5085 );
5086 bloq.validate().unwrap();
5087 }
5088 let mut bloq = Bloq::new();
5089 let fragment = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
5090 Vec::new(),
5091 Vec::new(),
5092 )));
5093 let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5094 expr: ClassicalExpr::In(0),
5095 }));
5096 bloq.add_edge(fragment, consumer, BloqEdge::compose(0));
5097 assert!(
5098 matches!(bloq.validate(), Err(BloqValidationError::InvalidValueProducer { node, .. }) if node == consumer)
5099 );
5100 }
5101
5102 #[test]
5103 fn shared_readout_recipe_roundtrips_and_rejects_invalid_uses() {
5104 let program = Bloq::from_text(
5105 "BLOQIR 1
5106template t0 {
5107 circuit {
5108 H (0,0)
5109 }
5110}
5111graph {
5112 n0 quantum {
5113 instance i0 t0 @ (0,0)
5114 }
5115 n1 compute 0
5116 n2 compute 1
5117 n3 observable fragment operators i0 output Z(0,0) when v0
5118 n4 observable fragment operators i0 output X(0,0)
5119 n5 observable 0
5120 n6 compute in0
5121 n7 observable fragment
5122 n8 observable fragment
5123 n9 observable 1
5124 n10 compute in0
5125 n11 observable 2
5126 n12 compute in0
5127 n13 observable fragment when v9
5128 n0 -> n3 order
5129 n0 -> n4 order
5130 n1 -> n3 value 0
5131 n1 -> n5 value 0
5132 n5 -> n6 value 0
5133 n2 -> n7 value 0
5134 n6 -> n7 value 1 readout
5135 n2 -> n7 value 2 feedback 7
5136 n3 -> n7 compose 3
5137 n4 -> n7 compose 4
5138 n7 -> n8 compose 0
5139 n1 -> n8 value 1
5140 n8 -> n9 compose 0
5141 n9 -> n10 value 0
5142 n1 -> n13 value 9
5143 n8 -> n13 compose 0
5144 n13 -> n11 compose 0
5145 n11 -> n12 value 0
5146}",
5147 )
5148 .unwrap();
5149 program.validate().unwrap();
5150 let text = Bloq::from_text(&program.to_text()).unwrap();
5151 let binary = Bloq::from_binary(&program.to_binary()).unwrap();
5152 assert_eq!(program.to_binary(), text.to_binary());
5153 assert_eq!(program.to_binary(), binary.to_binary());
5154 let resolved = program
5155 .resolve_classical(
5156 crate::BloqNodeId(10),
5157 crate::ClassicalAssignment::Uniform(false),
5158 )
5159 .unwrap();
5160 assert_eq!(resolved.decoder_observables, [0, 1].into_iter().collect());
5161 let pinned = program
5162 .pin_membership(&std::collections::BTreeMap::new())
5163 .unwrap();
5164 pinned.validate().unwrap();
5165 assert_eq!(pinned.value_inputs(crate::BloqNodeId(13)).count(), 0);
5166 assert_eq!(pinned[crate::BloqNodeId(13)].activation, None);
5167 let mut optimized = program.clone();
5168 optimized.optimize().expect("acyclic test program");
5169 optimized.validate().unwrap();
5170 for node in [crate::BloqNodeId(9), crate::BloqNodeId(11)] {
5171 let original = program
5172 .resolve_classical(node, crate::ClassicalAssignment::Uniform(false))
5173 .unwrap();
5174 for edited in [&pinned, &optimized] {
5175 assert_eq!(
5176 edited
5177 .resolve_classical(node, crate::ClassicalAssignment::Uniform(false))
5178 .unwrap(),
5179 original
5180 );
5181 }
5182 }
5183
5184 let mut bad = program.clone();
5185 let compute = bad.add_node(BloqNode::classical(ClassicalNode::Compute {
5186 expr: ClassicalExpr::In(0),
5187 }));
5188 bad.add_edge(crate::BloqNodeId(8), compute, BloqEdge::compose(0));
5189 assert!(
5190 matches!(bad.validate(), Err(BloqValidationError::InvalidValueProducer { node, .. }) if node == compute)
5191 );
5192 let mut bad = program.clone();
5193 bad.add_edge(
5194 crate::BloqNodeId(8),
5195 crate::BloqNodeId(9),
5196 BloqEdge::Compose {
5197 slot: 1,
5198 role: crate::ValueRole::ReadoutFold,
5199 },
5200 );
5201 assert!(matches!(
5202 bad.validate(),
5203 Err(BloqValidationError::InvalidObservableFold { .. })
5204 ));
5205 let mut bad = program;
5206 bad.add_edge(
5207 crate::BloqNodeId(8),
5208 crate::BloqNodeId(7),
5209 BloqEdge::compose(9),
5210 );
5211 assert!(matches!(
5212 bad.validate(),
5213 Err(BloqValidationError::CyclicGraph)
5214 ));
5215 }
5216
5217 #[test]
5218 fn observable_folds_require_a_bit_source_and_observable_target() {
5219 for role in [
5220 crate::ValueRole::FeedbackFold { action: 0 },
5221 crate::ValueRole::ReadoutFold,
5222 ] {
5223 let mut bloq = Bloq::new();
5224 let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5225 expr: ClassicalExpr::Const(false),
5226 }));
5227 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
5228 expr: ClassicalExpr::In(0),
5229 }));
5230 bloq.add_edge(
5231 producer,
5232 compute,
5233 BloqEdge::Value {
5234 slot: 0,
5235 output: ObservableOutput::Corrected,
5236 role: role.clone(),
5237 },
5238 );
5239
5240 assert!(matches!(
5241 bloq.validate().unwrap_err(),
5242 BloqValidationError::InvalidObservableFold { node, .. } if node == compute
5243 ));
5244
5245 let mut bloq = Bloq::new();
5246 let include = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
5247 index: None,
5248 measurements: Vec::new(),
5249 operators: Vec::new(),
5250 }));
5251 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
5252 bloq.add_edge(include, observable, BloqEdge::Compose { slot: 0, role });
5253
5254 assert!(matches!(
5255 bloq.validate().unwrap_err(),
5256 BloqValidationError::InvalidObservableFold { producer, .. } if producer == include
5257 ));
5258 }
5259 }
5260
5261 #[test]
5262 fn duplicate_output_frame_stamp_is_rejected() {
5263 let mut bloq = Bloq::new();
5264 for _ in 0..2 {
5265 bloq.add_node(
5266 BloqNode::classical(ClassicalNode::Compute {
5267 expr: ClassicalExpr::Const(false),
5268 })
5269 .with_provenance(crate::NodeProvenance::OutputFrame {
5270 port: ivec3(0, 0, 1),
5271 basis: crate::Basis::X,
5272 }),
5273 );
5274 }
5275 assert!(matches!(
5276 bloq.validate().unwrap_err(),
5277 BloqValidationError::DuplicateOutputFrameStamp { port } if port == ivec3(0, 0, 1)
5278 ));
5279 }
5280
5281 #[test]
5282 fn rus_explicit_restart_source_binds_despite_stray_open_producer() {
5283 let mut body = SubGraph::new();
5284 let predicate = body.add_node(BloqNode::classical(ClassicalNode::Compute {
5285 expr: ClassicalExpr::Const(false),
5286 }));
5287 let _stray = body.add_node(BloqNode::classical(ClassicalNode::Compute {
5288 expr: ClassicalExpr::Const(false),
5289 }));
5290 let mut bloq = Bloq::new();
5291 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
5292 body,
5293 restart_condition: ClassicalExpr::In(0),
5294 restart_source: Some(predicate.into()),
5295 }));
5296 bloq.validate().unwrap();
5297 }
5298
5299 #[test]
5300 fn rus_restart_source_naming_missing_body_node_is_rejected() {
5301 let mut bloq = Bloq::new();
5302 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
5303 body: SubGraph::new(),
5304 restart_condition: ClassicalExpr::In(0),
5305 restart_source: Some(crate::BloqNodeId(9).into()),
5306 }));
5307 assert!(matches!(
5308 bloq.validate().unwrap_err(),
5309 BloqValidationError::InvalidRestartSource {
5310 restart_source: ValueRef {
5311 node: crate::BloqNodeId(9),
5312 ..
5313 },
5314 ..
5315 }
5316 ));
5317 }
5318
5319 #[test]
5320 fn rus_restart_source_naming_non_producer_is_rejected() {
5321 let mut body = SubGraph::new();
5322 let binding = body.add_node(BloqNode::classical(ClassicalNode::Discard {
5323 condition: ClassicalExpr::Const(false),
5324 }));
5325 let mut bloq = Bloq::new();
5326 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
5327 body,
5328 restart_condition: ClassicalExpr::In(0),
5329 restart_source: Some(binding.into()),
5330 }));
5331 assert!(matches!(
5332 bloq.validate().unwrap_err(),
5333 BloqValidationError::InvalidRestartSource { .. }
5334 ));
5335 }
5336
5337 fn single_measure_program() -> (Bloq, crate::BloqNodeId) {
5340 let q = ivec2(0, 0);
5341 let mut template_circuit = CoordCircuit::new();
5342 template_circuit.measure(PauliBasis::Z, [q]);
5343 let mut bloq = Bloq::new();
5344 let template = bloq.add_template(BloqTemplate::new(template_circuit));
5345 let mut quantum = node(ivec3(0, 0, 0));
5346 quantum
5347 .expect_quantum_mut()
5348 .instances
5349 .push(TemplateInstance::new(TemplateInstanceId(0), template, q));
5350 let id = bloq.add_node(quantum);
5351 (bloq, id)
5352 }
5353
5354 #[test]
5355 fn validate_with_plans_returns_per_node_emission_plan() {
5356 let (bloq, quantum) = single_measure_program();
5357 let plans = bloq
5358 .validate_with_plans(&InstantiationOptions::default())
5359 .expect("the program is well-formed");
5360
5361 let stored = plans
5362 .get(&LevelPath::default(), quantum)
5363 .expect("a quantum node has a plan");
5364 assert_eq!(
5365 stored.measurements,
5366 [(instance_measurement(0, 0), 0)].into_iter().collect()
5367 );
5368 assert_eq!(
5369 stored.bodies,
5370 [((TemplateInstanceId(0), BodyId(0)), BodyId(0))]
5371 .into_iter()
5372 .collect()
5373 );
5374 assert_eq!(
5375 stored.grouped_measurements(),
5376 vec![(0, vec![instance_measurement(0, 0)])]
5377 );
5378 assert_eq!(stored.circuit.entry_body(), BodyId(0));
5379 assert_eq!(
5380 stored.circuit.body(BodyId(0)).unwrap().ops(),
5381 &[Op::Measure {
5382 basis: PauliBasis::Z,
5383 qubits: vec![ivec2(0, 0)],
5384 measurements: vec![0],
5385 flip_probability: 0.0,
5386 }]
5387 );
5388 }
5389
5390 #[test]
5391 fn validate_with_plans_agrees_with_validate_on_failure() {
5392 let (mut bloq, quantum) = single_measure_program();
5395 let accumulate = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
5396 index: None,
5397 operators: Vec::new(),
5398 measurements: vec![InstanceMeasurement {
5399 instance: TemplateInstanceId(0),
5400 measurement: 1, }],
5402 }));
5403 bloq.add_edge(quantum, accumulate, BloqEdge::Order);
5404
5405 let via_validate = bloq.validate();
5406 let via_plans = bloq
5407 .validate_with_plans(&InstantiationOptions::default())
5408 .map(|_| ());
5409 assert_eq!(via_validate, via_plans);
5410 assert!(matches!(
5411 via_validate.unwrap_err(),
5412 BloqValidationError::UnknownInstanceMeasurement {
5413 instance: TemplateInstanceId(0),
5414 measurement: 1,
5415 ..
5416 }
5417 ));
5418 }
5419
5420 #[test]
5421 fn relative_merge_cache_reuses_proofs_without_changing_measurement_aliases() {
5422 use bloq_utils::boolean::BooleanLimits;
5423
5424 let mut circuit = CoordCircuit::new();
5425 for _ in 0..128 {
5426 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
5427 circuit.tick();
5428 }
5429 let mut bloq = Bloq::new();
5430 let template = bloq.add_template(BloqTemplate::new(circuit));
5431 for index in 0..32 {
5432 let mut quantum = node(ivec3(0, 0, index));
5433 quantum
5434 .expect_quantum_mut()
5435 .instances
5436 .extend((0..2).map(|member| {
5437 TemplateInstance::new(
5438 TemplateInstanceId(2 * index as u32 + member),
5439 template,
5440 ivec2(index, 0),
5441 )
5442 }));
5443 bloq.add_node(quantum);
5444 }
5445 let options = InstantiationOptions::default().with_boolean_limits(BooleanLimits {
5446 max_steps: 8_000,
5447 ..BooleanLimits::default()
5448 });
5449 bloq.validate_with_options(&options)
5450 .expect("one merge proof plus each placement fits the work budget");
5451 assert!(
5452 bloq.validate_with_plans(&options)
5453 .unwrap_err()
5454 .is_resource_limited(),
5455 "materializing every plan still charges its circuit work"
5456 );
5457
5458 let plans = bloq
5459 .validate_with_plans(&InstantiationOptions::default())
5460 .expect("retained plans perform the same successful merges");
5461 assert_eq!(plans.iter().count(), 32);
5462 for (_, plan) in plans.iter() {
5463 let groups = plan.grouped_measurements();
5464 assert_eq!(groups.len(), 128);
5465 assert!(groups.iter().all(|(_, sources)| sources.len() == 2));
5466 }
5467 }
5468
5469 #[test]
5470 fn relative_merge_cache_distinguishes_placement_and_preserves_extrema() {
5471 let mut bloq = Bloq::new();
5472 let mut circuit = CoordCircuit::new();
5473 circuit.do_gate(GateType::H, [ivec2(0, 0)]).unwrap();
5474 let template = bloq.add_template(BloqTemplate::new(circuit));
5475 for (index, offsets) in [[i32::MIN, i32::MAX], [i32::MAX, i32::MIN], [0, 1], [10, 11]]
5476 .into_iter()
5477 .enumerate()
5478 {
5479 let mut quantum = node(ivec3(0, 0, index as i32));
5480 quantum
5481 .expect_quantum_mut()
5482 .instances
5483 .extend(offsets.into_iter().enumerate().map(|(member, offset)| {
5484 TemplateInstance::new(
5485 TemplateInstanceId((2 * index + member) as u32),
5486 template,
5487 ivec2(offset, 0),
5488 )
5489 }));
5490 bloq.add_node(quantum);
5491 }
5492 assert_eq!(bloq.validate(), Ok(()));
5493 assert_eq!(
5494 bloq.validate_with_plans(&InstantiationOptions::default())
5495 .map(|_| ()),
5496 Ok(())
5497 );
5498
5499 let mut quantum = node(ivec3(0, 0, 4));
5500 quantum.expect_quantum_mut().instances.extend(
5501 (8..10).map(|id| TemplateInstance::new(TemplateInstanceId(id), template, ivec2(10, 0))),
5502 );
5503 bloq.add_node(quantum);
5504 let cached = bloq.validate();
5505 assert_eq!(
5506 cached,
5507 bloq.validate_with_plans(&InstantiationOptions::default())
5508 .map(|_| ())
5509 );
5510 assert!(matches!(
5511 cached,
5512 Err(BloqValidationError::InvalidInstanceMergeStructure {
5513 source: NodeTemplateInstanceMergeError::QubitConflict(_),
5514 ..
5515 })
5516 ));
5517 }
5518
5519 #[test]
5520 fn relative_merge_cache_checks_unused_absolute_coordinates() {
5521 for (coordinate, last_valid, overflow) in [
5522 (ivec2(1, 0), ivec2(i32::MAX - 1, 0), ivec2(i32::MAX, 0)),
5523 (ivec2(0, -1), ivec2(0, i32::MIN + 1), ivec2(0, i32::MIN)),
5524 ] {
5525 let mut circuit = CoordCircuit::new();
5526 circuit.register_measurement_id(0, coordinate);
5527 let mut bloq = Bloq::new();
5528 let template = bloq.add_template(BloqTemplate::new(circuit));
5529 for (index, offset) in [ivec2(0, 0), last_valid, overflow].into_iter().enumerate() {
5530 let mut quantum = node(ivec3(0, 0, index as i32));
5531 quantum
5532 .expect_quantum_mut()
5533 .instances
5534 .push(TemplateInstance::new(
5535 TemplateInstanceId(index as u32),
5536 template,
5537 offset,
5538 ));
5539 bloq.add_node(quantum);
5540 if index < 2 {
5541 assert_eq!(bloq.validate(), Ok(()));
5542 }
5543 }
5544 let cached = bloq.validate();
5545 assert_eq!(
5546 cached,
5547 bloq.validate_with_plans(&InstantiationOptions::default())
5548 .map(|_| ())
5549 );
5550 assert!(matches!(
5551 cached,
5552 Err(BloqValidationError::InvalidInstanceMergeStructure {
5553 source: NodeTemplateInstanceMergeError::CoordinateOverflow(_),
5554 ..
5555 })
5556 ));
5557 }
5558 }
5559
5560 #[test]
5561 fn relative_merge_cache_requires_zero_repeat_measurement_remapping() {
5562 let mut circuit = CoordCircuit::new();
5563 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::ConditionalPauli(vec![
5564 ConditionalCorrection {
5565 pauli: PauliBasis::X,
5566 control: 0,
5567 target: ivec2(0, 0),
5568 },
5569 ])]));
5570 circuit
5571 .body_mut(circuit.entry_body())
5572 .unwrap()
5573 .ops_mut()
5574 .push(Op::Repeat {
5575 body,
5576 repetitions: 0,
5577 });
5578 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
5579 let mut bloq = Bloq::new();
5580 let template = bloq.add_template(BloqTemplate::new(circuit));
5581 assert_eq!(
5582 bloq.validate(),
5583 Ok(()),
5584 "source preflight skips the zero repeat"
5585 );
5586 let mut quantum = node(ivec3(0, 0, 0));
5587 quantum
5588 .expect_quantum_mut()
5589 .instances
5590 .push(TemplateInstance::new(
5591 TemplateInstanceId(0),
5592 template,
5593 ivec2(0, 0),
5594 ));
5595 bloq.add_node(quantum);
5596 let cached = bloq.validate();
5597 assert_eq!(
5598 cached,
5599 bloq.validate_with_plans(&InstantiationOptions::default())
5600 .map(|_| ())
5601 );
5602 assert!(matches!(
5603 cached,
5604 Err(BloqValidationError::InvalidInstanceMergeStructure {
5605 source: NodeTemplateInstanceMergeError::ControlReferencesUnknownMeasurement(0),
5606 ..
5607 })
5608 ));
5609 }
5610}