Skip to main content

bloq_ir/
validation.rs

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/// A well-formedness violation or resource exhaustion from [`Bloq::validate`].
16///
17/// Well-formedness messages begin with their normative rule id. Resource
18/// failures are explicitly identified and can be queried without parsing text.
19#[derive(Debug, Error, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum BloqValidationError {
22    /// A detector bundle definition or use is malformed.
23    #[error("WF-6: {0}")]
24    DetectorBundle(#[from] crate::DetectorBundleError),
25    /// A bundle use reads a bound owner after the consumer or without a path.
26    #[error("WF-7: node {node:?} cannot read bundle owner instance {instance:?}")]
27    UnavailableDetectorBundleOwner {
28        /// Consumer node.
29        node: BloqNodeId,
30        /// Bound record owner.
31        instance: TemplateInstanceId,
32    },
33    /// A bundle parity contains an unresolvable template loop state.
34    #[error("WF-6: detector bundle {bundle:?} references loop state {state:?}")]
35    DetectorBundleLoopStateUnsupported {
36        /// Bundle definition.
37        bundle: crate::DetectorBundleId,
38        /// Unresolvable state.
39        state: LoopStateId,
40    },
41    /// A bundle signature names a template missing from the pool.
42    #[error("WF-4: detector bundle {bundle:?} references unknown template {template:?}")]
43    UnknownDetectorBundleTemplate {
44        /// Bundle definition.
45        bundle: crate::DetectorBundleId,
46        /// Missing template.
47        template: TemplateId,
48    },
49    /// Boolean validation exceeded its resource budget.
50    #[error("resource limit: {0}")]
51    BooleanResource(#[from] bloq_utils::boolean::BooleanResourceError),
52    /// A node reads a measurement from a later node.
53    #[error("WF-7: node {node:?} references measurement {measurement} from later node {owner:?}")]
54    FutureMeasurement {
55        /// Consumer node.
56        node: BloqNodeId,
57        /// Measurement owner.
58        owner: BloqNodeId,
59        /// Owner-local measurement id.
60        measurement: u32,
61    },
62    /// A node reads a measurement from an unrelated node.
63    #[error(
64        "WF-7: node {node:?} references measurement {measurement} from unrelated node {owner:?}"
65    )]
66    UnorderedNodeMeasurement {
67        /// Consumer node.
68        node: BloqNodeId,
69        /// Measurement owner.
70        owner: BloqNodeId,
71        /// Owner-local measurement id.
72        measurement: u32,
73    },
74    /// A node references an absent template.
75    #[error("WF-4: node {node:?} references unknown template {template:?}")]
76    UnknownTemplate {
77        /// Referencing node.
78        node: BloqNodeId,
79        /// Missing template.
80        template: TemplateId,
81    },
82    /// Pipe padding references an absent template.
83    #[error("WF-4: pipe padding references unknown template {template:?}")]
84    UnknownPipePaddingTemplate {
85        /// Missing template.
86        template: TemplateId,
87    },
88    /// A one-round padding template contains a repeat.
89    #[error("WF-4: one-round padding template {template:?} contains a top-level REPEAT")]
90    PipePaddingOneRoundLooped {
91        /// Invalid template.
92        template: TemplateId,
93    },
94    /// A looped padding template lacks exactly one repeat.
95    #[error("WF-4: looped padding template {template:?} must contain exactly one top-level REPEAT")]
96    PipePaddingLoopedMalformed {
97        /// Invalid template.
98        template: TemplateId,
99    },
100    /// A template-instance id is duplicated.
101    #[error("WF-5: template instance id {instance:?} is used more than once")]
102    DuplicateTemplateInstance {
103        /// Duplicated instance.
104        instance: TemplateInstanceId,
105    },
106    /// A node references an absent instance.
107    #[error("WF-6: node {node:?} references unknown template instance {instance:?}")]
108    UnknownTemplateInstance {
109        /// Referencing node.
110        node: BloqNodeId,
111        /// Missing instance.
112        instance: TemplateInstanceId,
113    },
114    /// A logical input references an absent instance.
115    #[error(
116        "WF-6: logical input ({},{},{}) references unknown template instance {instance:?}",
117        port.x, port.y, port.z
118    )]
119    LogicalInputUnknownInstance {
120        /// Source port.
121        port: IVec3,
122        /// Missing instance.
123        instance: TemplateInstanceId,
124    },
125    /// A logical output references a missing owner instance.
126    #[error("WF-6: logical output at {port:?} references unknown instance {instance:?}")]
127    LogicalOutputUnknownInstance {
128        /// Source port.
129        port: IVec3,
130        /// Missing instance.
131        instance: TemplateInstanceId,
132    },
133    /// A node parity references an unavailable instance measurement.
134    #[error(
135        "WF-6: node {node:?} references unavailable measurement {measurement} in instance {instance:?}"
136    )]
137    UnknownInstanceMeasurement {
138        /// Referencing node.
139        node: BloqNodeId,
140        /// Referenced instance.
141        instance: TemplateInstanceId,
142        /// Missing measurement id.
143        measurement: u32,
144    },
145    /// A template side table references an unreachable body.
146    #[error("WF-6: template {template:?} references unreachable repeat body {body:?}")]
147    UnknownTemplateBody {
148        /// Referencing template.
149        template: TemplateId,
150        /// Missing body.
151        body: BodyId,
152    },
153    /// A template side table references an unavailable measurement.
154    #[error("WF-6: template {template:?} references unavailable measurement {measurement}")]
155    UnknownTemplateMeasurement {
156        /// Referencing template.
157        template: TemplateId,
158        /// Missing measurement.
159        measurement: u32,
160    },
161    /// A template side table references an unavailable loop state.
162    #[error("WF-6: template {template:?} references unavailable loop state {state:?}")]
163    UnknownTemplateLoopState {
164        /// Referencing template.
165        template: TemplateId,
166        /// Missing loop state.
167        state: LoopStateId,
168    },
169    /// A node parity contains a template-local loop state.
170    #[error("WF-6: node {node:?} parity references loop state {state:?}")]
171    NodeLoopStateUnsupported {
172        /// Referencing node.
173        node: BloqNodeId,
174        /// Unsupported loop state.
175        state: LoopStateId,
176    },
177    /// A template declares one loop-state id twice.
178    #[error("WF-6: template {template:?} declares loop state {state:?} more than once")]
179    DuplicateTemplateLoopState {
180        /// Owning template.
181        template: TemplateId,
182        /// Duplicated state.
183        state: LoopStateId,
184    },
185    /// A node's template instances cannot be merged.
186    #[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        /// Invalid quantum node.
193        node: BloqNodeId,
194        /// Merge failure.
195        source: NodeTemplateInstanceMergeError,
196    },
197    /// A template circuit is structurally invalid.
198    #[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        /// Invalid template.
205        template: TemplateId,
206        /// Circuit failure.
207        source: NodeTemplateInstanceMergeError,
208    },
209    /// A consumer has two producers on one value slot.
210    #[error("WF-2: node {node:?} has multiple value producers on input slot {slot}")]
211    DuplicateValueSlot {
212        /// Consumer node.
213        node: BloqNodeId,
214        /// Duplicated slot.
215        slot: u32,
216    },
217    /// A quantum face fans out.
218    #[error("WF-3: node {node:?} has multiple quantum outputs for one face")]
219    QuantumEdgeFanout {
220        /// Fan-out node.
221        node: BloqNodeId,
222    },
223    /// A region predicate slot has no producer.
224    #[error("WF-10: region {node:?} has no value input for predicate slot {slot}")]
225    MissingRegionSelectorInput {
226        /// Region node.
227        node: BloqNodeId,
228        /// Missing slot.
229        slot: u32,
230    },
231    /// The graph is cyclic.
232    #[error("WF-1: graph contains a cycle")]
233    CyclicGraph,
234    /// A classical expression slot has no producer.
235    #[error("WF-11: classical node {node:?} has no value input for slot {slot}")]
236    MissingClassicalInput {
237        /// Consumer node.
238        node: BloqNodeId,
239        /// Missing slot.
240        slot: u32,
241    },
242    /// A value edge feeds an unread classical slot.
243    #[error("WF-11: classical node {node:?} does not read value input slot {slot}")]
244    UnusedClassicalInput {
245        /// Consumer node.
246        node: BloqNodeId,
247        /// Unused slot.
248        slot: u32,
249    },
250    /// A discard occurs inside a retry body.
251    #[error("WF-8: Discard node {node:?} is inside a RepeatUntilSuccess body")]
252    DiscardInsideRepeatUntilSuccess {
253        /// Invalid discard.
254        node: BloqNodeId,
255    },
256    /// A retry region is nested.
257    #[error("WF-8: RepeatUntilSuccess node {node:?} is nested")]
258    NestedRepeatUntilSuccess {
259        /// Nested region.
260        node: BloqNodeId,
261    },
262    /// An observable index is duplicated in the program.
263    #[error("WF-13: Observable node {node:?} reuses index {index}")]
264    DuplicateObservableIndex {
265        /// Duplicate observable node.
266        node: BloqNodeId,
267        /// Duplicate index.
268        index: u32,
269    },
270    /// Restart syndromes occur outside a retry body.
271    #[error("WF-8: node {node:?} has restart syndromes outside RepeatUntilSuccess")]
272    RestartOutsideRepeatUntilSuccess {
273        /// Invalid quantum node.
274        node: BloqNodeId,
275    },
276    /// An output-frame stamp is not on a top-level compute.
277    #[error(
278        "WF-15: output frame ({},{},{}) references non-top-level-Compute node {node:?}",
279        port.x, port.y, port.z
280    )]
281    OutputFrameNotCompute {
282        /// Output port.
283        port: IVec3,
284        /// Invalid frame node.
285        node: BloqNodeId,
286    },
287    /// An output lacks one frame basis stamp.
288    #[error(
289        "WF-15: output frame ({},{},{}) needs one X and one Z stamp",
290        port.x, port.y, port.z
291    )]
292    OutputFrameIncomplete {
293        /// Incomplete output port.
294        port: IVec3,
295    },
296    /// A quantum node receives a value edge.
297    #[error("WF-12: quantum node {node:?} has value input slot {slot}")]
298    UnusedQuantumValueInput {
299        /// Quantum node.
300        node: BloqNodeId,
301        /// Invalid input slot.
302        slot: u32,
303    },
304    /// A node's classical activation is invalid.
305    ///
306    /// Unlike its siblings this variant serves several rules — a separate
307    /// activation slot is WF-11, whole-node activation of a quantum node is
308    /// WF-12, and the activation's edge role is WF-16 — so it carries the one
309    /// it cites rather than hard-coding a prefix.
310    #[error("{rule}: invalid classical activation on node {node:?}: {reason}")]
311    InvalidActivation {
312        /// Activated node.
313        node: BloqNodeId,
314        /// Normative rule violated.
315        rule: &'static str,
316        /// Contract violation.
317        reason: &'static str,
318    },
319    /// A quantum seam guard is invalid.
320    #[error("WF-3: invalid quantum seam guard {guard:?} before node {node:?}")]
321    InvalidSeamGuard {
322        /// Guarded target.
323        node: BloqNodeId,
324        /// Invalid guard producer.
325        guard: crate::ValueRef,
326    },
327    /// A selected node may reference a deselected instance.
328    #[error("WF-7: active node {node:?} can reference deselected instance {instance:?}")]
329    ConditionalReferenceUnavailable {
330        /// Referencing node.
331        node: BloqNodeId,
332        /// Conditionally unavailable instance.
333        instance: TemplateInstanceId,
334    },
335    /// A classical expression reads one slot more than once.
336    #[error("WF-11: node {node:?} reads input slot {slot} more than once")]
337    DuplicateExprInput {
338        /// Consumer node.
339        node: BloqNodeId,
340        /// Repeated slot.
341        slot: u32,
342    },
343    /// A value edge comes from a node that produces no value.
344    #[error("WF-16: node {node:?} has a value edge from non-value producer {producer:?}")]
345    InvalidValueProducer {
346        /// Consumer node.
347        node: BloqNodeId,
348        /// Invalid producer.
349        producer: BloqNodeId,
350    },
351    /// An observable-fold edge has invalid endpoint kinds.
352    #[error(
353        "WF-16: observable-fold edge {producer:?} -> {node:?} must carry a bit leaf into an Observable"
354    )]
355    InvalidObservableFold {
356        /// Consumer node.
357        node: BloqNodeId,
358        /// Invalid producer.
359        producer: BloqNodeId,
360    },
361    /// An output has two frame stamps for one basis.
362    #[error(
363        "WF-15: output frame ({},{},{}) has duplicate basis stamps",
364        port.x, port.y, port.z
365    )]
366    DuplicateOutputFrameStamp {
367        /// Output port.
368        port: IVec3,
369    },
370    /// A retry region's explicit restart source is invalid.
371    #[error(
372        "WF-10: RepeatUntilSuccess region {node:?} has invalid restart source {restart_source:?}"
373    )]
374    InvalidRestartSource {
375        /// Retry region.
376        node: BloqNodeId,
377        /// Invalid body-local producer.
378        restart_source: crate::ValueRef,
379    },
380    /// A declared bit result is invalid.
381    #[error("WF-17: declared result {node:?} must name a live bit producer")]
382    InvalidValueOutput {
383        /// Invalid result node.
384        node: BloqNodeId,
385    },
386    /// A declared boundary output is invalid.
387    #[error("WF-17: declared boundary output {node:?} must name a live Observable or region")]
388    InvalidBoundaryOutput {
389        /// Invalid boundary node.
390        node: BloqNodeId,
391    },
392    /// A boundary output is declared twice.
393    #[error("WF-17: boundary output {node:?} is declared more than once")]
394    DuplicateBoundaryOutput {
395        /// Duplicated boundary node.
396        node: BloqNodeId,
397    },
398    /// A quantum timeline violates its projection contract.
399    #[error("WF-20: quantum node {node:?} has an invalid source-layer timeline")]
400    InvalidQuantumTimeline {
401        /// Invalid quantum node.
402        node: BloqNodeId,
403    },
404}
405
406impl BloqValidationError {
407    /// Whether validation stopped at a resource allowance. Such a failure is
408    /// not evidence that the program violates the IR's well-formedness rules.
409    #[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/// Quantum emission stages keyed by scoped node address.
421///
422/// Static stages retain an exact emission plan. Conditional stages are marked
423/// for selected materialization. Only [`Bloq::validate_with_plans`] certifies
424/// their full Boolean domain; [`Bloq::emission_plans`] does not.
425#[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    /// A static stage's emission plan. Conditional stages use [`Self::has_membership`].
437    #[must_use]
438    pub fn get(&self, path: &LevelPath, node: BloqNodeId) -> Option<&NodeEmissionPlan> {
439        self.plans.get(path)?.get(&node)
440    }
441
442    /// Whether this conditional stage requires selected materialization.
443    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    /// Every static `((path, node), plan)` entry, in unspecified order.
450    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    /// The number of static and conditional quantum stages.
457    #[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    /// Whether the program has no quantum-node plans.
464    #[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
492/// Checks unique value-input slots, linear quantum faces, and valid value-edge sources.
493// Spec rules WF-2/WF-3 (edge cardinality) and WF-16 (producer kind).
494fn 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            // WF-16: fold roles carry runtime bits, never structural recipes.
553            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    /// Validates IR well-formedness, including nested region bodies.
599    ///
600    /// Same-layer qubit overlap depends on source `BlockGraph` soft corridors
601    /// and is checked by `bloq_compile` lowering.
602    ///
603    /// WF-9 checks every reachable selected merge. [`Self::validate_with_plans`]
604    /// additionally returns static plans and certified conditional stages.
605    /// This entry point releases each merged circuit after checking its node,
606    /// bounding temporary circuit storage by the largest quantum stage. Within
607    /// one noiseless validation, successful unguarded merges are reused at the
608    /// same relative placements; absolute coordinates are still checked.
609    ///
610    /// # Errors
611    ///
612    /// Returns [`BloqValidationError`] for any violated IR well-formedness or
613    /// bounded-resource rule.
614    pub fn validate(&self) -> Result<(), BloqValidationError> {
615        self.validate_with_options(&InstantiationOptions::default())
616    }
617
618    /// Validate with explicit noise and resource limits, releasing emission
619    /// plans after each node instead of retaining them.
620    ///
621    /// # Errors
622    ///
623    /// Returns [`BloqValidationError`] for invalid IR, merge, noise, or
624    /// exhausted resources.
625    pub fn validate_with_options(
626        &self,
627        options: &InstantiationOptions<'_>,
628    ) -> Result<(), BloqValidationError> {
629        self.validate_inner(options, &mut None)
630    }
631
632    /// [`Self::validate`], additionally returning static [`NodeEmissionPlan`]s
633    /// and certified conditional stages, keyed by scoped address (see
634    /// [`ValidatedPlans`]). Plans are instantiated with `options`.
635    ///
636    /// Applies the same well-formedness rules as [`Self::validate`], charging
637    /// work for every materialized plan. SEM-MERGE applies noise once to the
638    /// *post*-merge circuit, and the returned plans carry the caller's `options`.
639    /// Conditional membership uses pairwise Boolean overlap checks.
640    /// A malformed [`NoiseModel`](crate::circuit::NoiseModel) also changes the outcome: its
641    /// probabilities are only checked while materializing noise, so a bad model
642    /// surfaces here (and only under noisy `options`) as
643    /// [`InvalidInstanceMergeStructure`](BloqValidationError::InvalidInstanceMergeStructure)
644    /// at the first quantum node.
645    ///
646    /// # Errors
647    ///
648    /// Returns [`BloqValidationError`] under the same conditions as
649    /// [`Self::validate_with_options`].
650    ///
651    /// # Panics
652    ///
653    /// Panics only if the internal plan collector is unexpectedly absent.
654    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    /// Materialize static quantum stages without a full well-formedness audit.
664    /// Guarded stages are marked for the consumer to select and materialize;
665    /// no Boolean proof of their entire choice domain is performed here.
666    ///
667    /// Required static merges and their noise application still fail here.
668    /// A consumer must report failures while materializing selected guarded
669    /// stages. Call [`Self::validate_with_plans`] explicitly for the complete
670    /// all-choice IR audit as well as plans.
671    ///
672    /// # Errors
673    ///
674    /// [`InvalidInstanceMergeStructure`](BloqValidationError::InvalidInstanceMergeStructure)
675    /// if a static quantum node's instances cannot merge (SEM-MERGE).
676    ///
677    /// # Caution
678    ///
679    /// This is not a substitute for [`Self::validate`]. Malformed IR outside
680    /// the materialized stages may not be reported by this method.
681    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    /// Shared checks; only consumers requesting emission retain WF-9 plans.
714    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        // Charge source containers before whole-program maps reserve their entries.
726        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        // Templates are shared, so validate the pool once.
741        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        // A definition is shared by every use. Check its record coordinates
747        // once; uses below validate only their bindings and activation.
748        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        // Memory-round insertion requires an unrolled template and one with a
805        // single top-level REPEAT.
806        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        // Cross-region references use program-global instance ids (WF-5), while
835        // read-after-measure order remains graph-local.
836        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        // U10 requires one X/Z stamp pair on top-level Compute nodes.
859        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        // Observable indices share one global emission space (§4.2/U17a).
905        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                // Skip per-node scans when no template can restart.
921                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        // WF-7: external metadata is unconditional, so its cut owner must
968        // exist in every reachable selection too.
969        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                    // RUS executes its body at least once on every completed shot.
1004                    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
1195/// Collects program-global instance ids and rejects duplicates (WF-5).
1196fn 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    /// Nested in a `RepeatUntilSuccess` body.
1221    in_rus: bool,
1222    /// Whether any shared template carries restart syndromes.
1223    pool_has_restarts: bool,
1224}
1225
1226/// A merge proof depends on ordered templates and relative placement, while
1227/// ownership, side tables, guards, and logical cuts remain per-node checks.
1228/// This cache belongs to one validation of one immutable template pool.
1229#[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        // Subtraction must represent opposite i32 extrema without overflow.
1250        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                // Includes unused registry entries and stored, unreachable bodies,
1274                // matching the full instance translator's coordinate preflight.
1275                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        // Template preflight alone does not prove measurement remapping inside
1291        // zero-count REPEAT bodies. Cache only successful full SEM-MERGE proofs.
1292        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
1304/// Declared outputs remain valid even when the parent does not consume them.
1305fn 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/// Validates one graph level, then recurses into region bodies. `path` is the
1327/// level's [`LevelPath`], threaded down so each quantum node's plan is keyed by
1328/// its program-unique scoped address (matching `Bloq::levels`/`Bloq::walk`).
1329#[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    // Hand-built Value edges may create cycles (IR spec §3).
1349    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; // classical / region nodes carry no template instances
1374        };
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            // Complete observables retain the same record and boundary-owner
1416            // checks as independently shared readout fragments.
1417            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; // classical / region nodes carry no detectors/repeat states
1449        };
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        // WF-9: the instance merge must succeed. Reuse successful relative-shape
1503        // proofs only when no noisy or retained emission plan is requested.
1504        // Retained plans let consumers emit without re-merging (IR-02).
1505        // Noise is applied post-merge, so a malformed NoiseModel surfaces here as
1506        // an InvalidInstanceMergeStructure (see `validate_with_plans`).
1507        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    // RUS failures restart rather than discard, including in nested bodies (§6).
1551    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        // Region predicates share Compute's linear-input contract (WF-11).
1572        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        // A supplied RUS restart_source must produce a body-local value (WF-10).
1588        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        // Restart predicates may use incoming Value edges or a body-local
1598        // restart_source because edges cannot cross regions (§6/U17).
1599        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    // Compute/Discard expressions must read each fed Value slot exactly once.
1637    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
1665/// WF-11: collect inputs without recursing through expression nesting. Only
1666/// affine expressions prohibit duplicates; every constant/operand costs work.
1667fn 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 stored input before the circuit-analysis cache may allocate or
1719    // walk it. Reused caches still obey the caller's declared input allowance.
1720    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    // Side tables may name only executed measurement sites.
1817    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        // Preserve first-reference order for the same diagnostic as the walk.
1887        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            // Initial parities see the surrounding frame before this loop's
1927            // own declarations become available.
1928            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            // Body detectors and next parities also see completed nested
1946            // declarations. Those nested states die when this frame exits.
1947            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
1992/// Every measurement term of `parity` must name a site `available` accepts.
1993fn 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
2011/// The repeat-event form: a parity evaluated at an event may only read
2012/// measurement sites ordered strictly before it.
2013fn 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        // Canonical parities and compiler observable recipes group an instance's refs.
2109        if cached
2110            .as_ref()
2111            .is_none_or(|(instance, _, _)| *instance != measurement.instance)
2112        {
2113            // Cross-region refs use global instances; only local refs enforce
2114            // read order.
2115            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    /// [`BloqValidationError`]'s own contract: a well-formedness message leads
2175    /// with its normative rule id, so a consumer can attribute a failure
2176    /// without consulting the spec's error tables. These two variants are the
2177    /// ones that once reported no id at all.
2178    #[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        // A residual seam predicate is live even without a Value consumer.
2277        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        // Whole-use activation must imply its record owner's selection.
2393        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        // Nonexhaustive membership still needs a pinned path for timing edits.
2434        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        // An unconditional readout would read a missing record on the false arm.
2484        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        // Local records and declarative bindings on a complete observable
2499        // each retain the selected-owner requirement.
2500        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    /// Builds the unrolled/looped template pair required by pipe padding.
2603    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), // no such template in the pool
2730                ivec2(0, 0),
2731            ));
2732        body.add_node(bad);
2733        // Const lets validation reach the nested body.
2734        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), // slot 1 has no incoming edge
2912            ])),
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                // Exercise both metadata channels independently.
3068                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]); // only measurement id 0 exists
4048        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        // Observable fragment sorts before its producer without an Order edge.
4079        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        // IR-01: reachable bodies now come from `preflight_circuit`'s visited
4270        // set minus the entry, replacing the former single-source merge. A
4271        // detector scoped to a body that is never a REPEAT target — here the
4272        // entry body, which preflight visits but deliberately excludes — must
4273        // still fail template-body validation.
4274        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        // Cross-level observable bindings resolve program-global instances.
4547        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]); // only measurement id 0 exists
4633        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        // Hand-built RUS with template restart and matching observable fragment.
4654        let q = ivec2(0, 0);
4655        let mut template_circuit = CoordCircuit::new();
4656        template_circuit.measure(PauliBasis::Z, [q]); // measurement 0
4657        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    /// Builds a RUS with a NodeRestart over one measurement.
4695    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]); // measurement 0
4699        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) // only measurement id 0 exists
4737                .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    /// Builds a RUS-body instance referenced by a top-level detector.
4886    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]); // measurement 0
4893        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    /// The checkless plan path a trusted consumer emits from must agree with
4938    /// the validating one, including for nodes nested inside a region body.
4939    #[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            // `NodeEmissionPlan` is not `PartialEq` (its circuit is not), so
4952            // compare the id maps the backends resolve through plus the merged
4953            // circuit's shape.
4954            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        // Backend folds each producer once, so duplicate linear leaves miscompile.
5024        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    /// A small program with one measuring quantum node. Returns the program and
5338    /// that node's id.
5339    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        // An observable fragment naming a measurement id the instance never produces fails
5393        // validation; both entry points must reject it identically.
5394        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, // only measurement id 0 exists
5401            }],
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}