Skip to main content

bloq_ir/
detslice.rs

1//! Program-level detector slices.
2//!
3//! This is Layer 2 of the detector-slice viewer: flatten the [`Bloq`], build straight-line
4//! tapes along the static executed path, resolve detector and logical-observable
5//! contributions into tape space, run the reverse sparse frame tracker, then
6//! scatter slices back to node timelines.
7//!
8//! This module is program-tape slicing over the whole [`Bloq`];
9//! `bloq_circuit::detslice` is the reverse sparse frame tracker it drives per
10//! straight-line tape.
11
12use std::sync::Arc;
13
14use bloq_circuit::{
15    DetectorCoords, DetectorParity, DetsliceError, Op, Pauli, PauliBasis, PauliMap,
16    RegionBreakKind, RegionTerm, SliceDetector, SliceRegionSeed, detector_slices_with_seeds,
17};
18use glam::IVec2;
19use rustc_hash::{FxHashMap, FxHashSet};
20use thiserror::Error;
21
22use crate::instantiation::InstantiationOptions;
23use crate::{
24    Bloq, BloqNodeId, BloqNodeKind, BloqValidationError, BodySelector, BoundaryFace, ClassicalExpr,
25    ClassicalNode, CycleDetected, FlattenError, InstanceMeasurement, LevelPath, NodeDetectorParity,
26    SubGraph, TemplateInstanceId, ValidatedPlans,
27};
28
29// ==============================================================================
30// Moment bucketing (shared with the editor timeline)
31// ==============================================================================
32
33/// The kind of a viewer moment: a bucket ops fall into within one tick segment.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum MomentKind {
36    /// Reset operation.
37    Reset,
38    /// Single-qubit rotation.
39    Rotation,
40    /// Two-qubit interaction.
41    Interaction,
42    /// Measurement operation.
43    Measurement,
44}
45
46/// Which moment bucket an op belongs to, or `None` if it is timeline-invisible.
47///
48/// Mirrors `bloq_editor::program_view::op_kind` with two deliberate deltas so
49/// the tracker sees exactly the ops that carry stabilizer flow:
50///
51/// - [`Op::MPP`] joins the [`Measurement`](MomentKind::Measurement) bucket. A
52///   product measurement births a region, and the editor consumes this same
53///   bucketing function so its visible MPP moments cannot drift from the tape.
54/// - [`Op::ConditionalPauli`] has no visible moment kind. Measurement-controlled
55///   corrections are folded into an adjacent visible moment by
56///   [`moment_segments`] so the reverse tracker sees them; value-controlled
57///   corrections remain transparent.
58fn op_moment_kind(op: &Op) -> Option<MomentKind> {
59    match op {
60        Op::Gate { gate, .. } if gate.is_reset() => Some(MomentKind::Reset),
61        Op::Gate { gate, .. } if gate.is_two_qubit_gate() => Some(MomentKind::Interaction),
62        Op::Gate { .. } => Some(MomentKind::Rotation),
63        Op::Measure { .. } | Op::MPP { .. } => Some(MomentKind::Measurement),
64        // Ticks segment the stream; ConditionalPauli is injected into an
65        // adjacent visible bucket by `bucket_tick_segment`; Repeat is not
66        // expected on a flattened tape (see `moment_segments`).
67        Op::Tick
68        | Op::ConditionalPauli(_)
69        | Op::Repeat { .. }
70        | Op::Depolarize1 { .. }
71        | Op::Depolarize2 { .. }
72        | Op::PauliError { .. } => None,
73    }
74}
75
76/// One viewer moment: a bucket of visible same-kind ops plus any invisible
77/// measurement-controlled feedforward assigned to that boundary, in op order.
78#[derive(Debug, Clone, PartialEq)]
79pub struct MomentSegment {
80    /// Common operation kind.
81    pub kind: MomentKind,
82    /// Operations in source order.
83    pub ops: Vec<Op>,
84}
85
86/// Incremental viewer-moment segmentation for structural circuit walkers.
87///
88/// Measurement-controlled corrections from an all-invisible chunk are carried
89/// into the next chunk, so callers may split at repeat boundaries without
90/// changing the moment order. Call [`Self::finish`] after the final chunk when
91/// the returned segments will be sent to the reverse tracker.
92#[derive(Debug, Default)]
93pub struct MomentSegmenter {
94    carried_corrections: Vec<Op>,
95}
96
97impl MomentSegmenter {
98    /// Segment one straight-line chunk while retaining trailing invisible
99    /// feedforward for the next chunk.
100    pub fn extend(&mut self, ops: &[Op]) -> Vec<MomentSegment> {
101        self.extend_owned(ops.to_vec())
102    }
103
104    /// Owned half of [`Self::extend`], used when the caller already has an op
105    /// buffer and can move it into the moment buckets.
106    fn extend_owned(&mut self, ops: Vec<Op>) -> Vec<MomentSegment> {
107        let mut out = Vec::new();
108        let mut segment = Vec::new();
109        for op in ops {
110            if matches!(op, Op::Tick) {
111                bucket_tick_segment(
112                    std::mem::take(&mut segment),
113                    &mut self.carried_corrections,
114                    &mut out,
115                );
116            } else {
117                segment.push(op);
118            }
119        }
120        bucket_tick_segment(segment, &mut self.carried_corrections, &mut out);
121        out
122    }
123
124    /// Attach trailing invisible feedforward to the last visible segment.
125    /// With no visible segment there is nothing to display or track.
126    pub fn finish(mut self, out: &mut [MomentSegment]) {
127        if let Some(last) = out.last_mut() {
128            last.ops.append(&mut self.carried_corrections);
129        }
130    }
131}
132
133/// Split a straight-line op stream into viewer moments, first at [`Op::Tick`].
134///
135/// Every tick segment uses consecutive same-kind runs in source order: even
136/// without feedforward, sequential operations may reuse a qubit (SEM-MERGE).
137/// Invisible corrections stay at their operation boundary. Empty runs are
138/// dropped. The editor timeline consumes this same implementation.
139///
140/// Expects a flattened (straight-line) stream: an [`Op::Repeat`] would be
141/// silently dropped by `op_moment_kind`, corrupting the segmentation, so
142/// callers flatten first (Layer 2 always does).
143pub fn moment_segments(ops: &[Op]) -> Vec<MomentSegment> {
144    moment_segments_owned(ops.to_vec())
145}
146
147fn moment_segments_owned(ops: Vec<Op>) -> Vec<MomentSegment> {
148    let mut segmenter = MomentSegmenter::default();
149    let mut out = segmenter.extend_owned(ops);
150    segmenter.finish(&mut out);
151    out
152}
153
154/// Preserve source order without creating a visible correction-only moment.
155/// Corrections before the first visible op prefix its run; later corrections
156/// stay at the end of the current run, and an all-invisible tick carries them
157/// to the next visible run.
158fn bucket_tick_segment(
159    segment: Vec<Op>,
160    carried_corrections: &mut Vec<Op>,
161    out: &mut Vec<MomentSegment>,
162) {
163    let mut runs = Vec::<MomentSegment>::new();
164    let mut pending = std::mem::take(carried_corrections);
165
166    for op in segment {
167        let op = match op {
168            Op::ConditionalPauli(corrections) => {
169                if !corrections.is_empty() {
170                    let correction = Op::ConditionalPauli(corrections);
171                    if let Some(run) = runs.last_mut() {
172                        run.ops.push(correction);
173                    } else {
174                        pending.push(correction);
175                    }
176                }
177                continue;
178            }
179            op => op,
180        };
181
182        let Some(kind) = op_moment_kind(&op) else {
183            continue;
184        };
185        if runs.last().is_none_or(|run| run.kind != kind) {
186            runs.push(MomentSegment {
187                kind,
188                ops: std::mem::take(&mut pending),
189            });
190        }
191        runs.last_mut()
192            .expect("a visible op creates a source-order run")
193            .ops
194            .push(op);
195    }
196
197    if runs.is_empty() {
198        carried_corrections.append(&mut pending);
199    } else {
200        debug_assert!(
201            pending.is_empty(),
202            "the final visible run must absorb pending corrections"
203        );
204        out.extend(runs);
205    }
206}
207
208// ==============================================================================
209// Public output types
210// ==============================================================================
211
212/// A tape-unique node reference.
213///
214/// [`BloqNodeId`]s are level-local (they
215/// restart at 0 in every region body [`SubGraph`]), so a bare id cannot key
216/// across levels; the `path` disambiguates by naming the region bodies descended
217/// into (empty for the top level).
218#[derive(Debug, Clone, PartialEq, Eq, Hash)]
219pub struct NodeRef {
220    /// Region bodies descended into to reach this node, top level first. Empty
221    /// for a top-level node.
222    pub path: LevelPath,
223    /// The node's id within its own level.
224    pub node: BloqNodeId,
225}
226
227impl NodeRef {
228    /// A top-level node's reference (empty path).
229    pub fn top_level(node: BloqNodeId) -> Self {
230        Self {
231            path: LevelPath::default(),
232            node,
233        }
234    }
235
236    /// Whether this refers to a top-level node.
237    pub fn is_top_level(&self) -> bool {
238        self.path.is_top_level()
239    }
240}
241
242/// Detector and logical-observable region choices for a pinned program.
243#[derive(Debug, Clone, Default, PartialEq, Eq)]
244pub struct ProgramSliceOptions {
245    detectors_only: bool,
246    physical_observables: bool,
247}
248
249impl ProgramSliceOptions {
250    /// Show each observable's physical Pauli support without its runtime sign
251    /// folds (`FeedbackFold` and `ReadoutFold`). Plain data expressions still
252    /// have to be affine; this is not a Boolean-to-parity approximation.
253    pub fn set_physical_observables(&mut self, enabled: bool) {
254        self.physical_observables = enabled;
255    }
256
257    /// Omit logical-observable regions while retaining the same quantum tapes.
258    pub fn set_detectors_only(&mut self, enabled: bool) {
259        self.detectors_only = enabled;
260    }
261}
262
263/// Stable identity of a detector or logical-observable region.
264#[derive(Debug, Clone, PartialEq, Eq, Hash)]
265pub enum ProgramRegionId {
266    /// A detector from one quantum node's side table.
267    Detector {
268        /// Node that owns the detector table.
269        owner: NodeRef,
270        /// Detector index within that table.
271        detector: u32,
272    },
273    /// A logical observable, matching Stim's `L{index}` region identity.
274    Observable {
275        /// Logical-observable index.
276        index: u32,
277    },
278}
279
280/// Identity and coordinates stored once for every program region.
281#[derive(Debug, Clone, PartialEq)]
282pub struct ProgramRegion {
283    /// Stable region identity.
284    pub id: ProgramRegionId,
285    /// Optional detector coordinates.
286    pub coords: Option<Arc<DetectorCoords>>,
287}
288
289/// One detector or logical-observable region at one moment.
290#[derive(Debug, Clone, PartialEq)]
291pub struct RegionView {
292    /// Index into [`ProgramSlices::regions`].
293    pub region: u32,
294    /// The region's Pauli terms, sorted by qubit.
295    pub terms: Vec<RegionTerm>,
296}
297
298/// Per-moment regions for one node.
299///
300/// `0[m]` is the regions alive at the end of the
301/// node's `m`-th moment. Length equals the node's [`moment_segments`] count, so
302/// an empty inner vec means "moment exists, no live regions".
303#[derive(Debug, Clone, Default, PartialEq)]
304pub struct NodeSlices(pub Vec<Vec<RegionView>>);
305
306/// A gauge anticommutation located back to the node timeline it falls in.
307///
308/// A break means the supplied region is not preserved by the tracked circuit.
309/// Consumers visualizing a non-Clifford program through a Clifford gate proxy
310/// must recompute the program's observables for that same proxy first.
311#[derive(Debug, Clone, PartialEq)]
312pub struct ProgramRegionBreak {
313    /// The node whose moment the break lands in (the broken segment's node).
314    pub node: NodeRef,
315    /// Node-local moment index within `node`.
316    pub moment: usize,
317    /// The detector or logical observable whose region broke.
318    pub region: ProgramRegionId,
319    /// Qubit where anticommutation occurred.
320    pub qubit: IVec2,
321    /// Kind of region break.
322    pub kind: RegionBreakKind,
323}
324
325/// The whole program's detector-slice data, keyed by node.
326#[derive(Debug, Clone, Default)]
327pub struct ProgramSlices {
328    /// Region identity and detector coordinates, stored once rather than copied
329    /// into every moment where the region is alive.
330    pub regions: Vec<ProgramRegion>,
331    /// Every quantum node (top-level and region-body) with its per-moment
332    /// regions.
333    pub per_node: FxHashMap<NodeRef, NodeSlices>,
334    /// Gauge breaks, located back to their node timeline.
335    pub breaks: Vec<ProgramRegionBreak>,
336    /// How many detector or observable regions were dropped because a
337    /// contribution reached an instance outside its own tape.
338    pub skipped_cross_tape: usize,
339}
340
341/// Why [`program_detector_slices`] could not build the tape.
342#[derive(Debug, Error)]
343#[non_exhaustive]
344pub enum ProgramSliceError {
345    /// Detector-slice region ids exhausted their `u32` index space.
346    #[error("detector-slice region id space overflowed")]
347    RegionIdOverflow,
348    /// A detector bundle use is malformed.
349    #[error("{0}")]
350    DetectorBundle(#[from] crate::DetectorBundleError),
351    /// Conditional membership must be pinned before slicing.
352    #[error("detector slices require pinned component membership")]
353    MembershipSelectionRequired,
354    /// A quantum emission plan could not be materialized.
355    #[error("quantum plan materialization failed: {0}")]
356    Materialization(#[from] BloqValidationError),
357    /// A requested region refers to a measurement no emitted stage provides.
358    #[error("region {region:?} references unavailable measurement {measurement:?}")]
359    UnknownMeasurement {
360        /// Region containing the reference.
361        region: ProgramRegionId,
362        /// Unavailable measurement.
363        measurement: InstanceMeasurement,
364    },
365    /// A requested region refers to an instance absent from the program.
366    #[error("region {region:?} references unavailable instance {instance:?}")]
367    UnknownInstance {
368        /// Region containing the reference.
369        region: ProgramRegionId,
370        /// Unavailable instance.
371        instance: TemplateInstanceId,
372    },
373    /// Repeat flattening failed.
374    #[error("flattening the program failed: {0}")]
375    Flatten(#[from] FlattenError),
376    /// A graph level is cyclic.
377    #[error("a graph level has no topological emit order (cycle)")]
378    Cycle(#[from] CycleDetected),
379    /// An observable expression is nonlinear.
380    #[error("node {node:?} folds a nonlinear observable expression (SEM-FOLD)")]
381    NonLinearExpr {
382        /// Node containing the nonlinear expression.
383        node: NodeRef,
384    },
385    /// Stabilizer tracking failed.
386    #[error("detector-slice tracking failed: {0}")]
387    Detslice(#[from] DetsliceError),
388    /// A gauge break could not be assigned to a moment.
389    #[error("region {region:?} produced a gauge break outside every circuit moment")]
390    BreakOutsideTimeline {
391        /// Region that broke outside the timeline.
392        region: ProgramRegionId,
393    },
394}
395
396fn checked_region_base(base: usize, additional: usize) -> Result<u32, ProgramSliceError> {
397    let count = base
398        .checked_add(additional)
399        .ok_or(ProgramSliceError::RegionIdOverflow)?;
400    u32::try_from(count).map_err(|_| ProgramSliceError::RegionIdOverflow)?;
401    u32::try_from(base).map_err(|_| ProgramSliceError::RegionIdOverflow)
402}
403
404// ==============================================================================
405// Driver
406// ==============================================================================
407
408/// Compute detector-slice regions for every quantum node of `program`.
409///
410/// Clones and flattens `program` (repeats unrolled — regions genuinely differ
411/// per iteration), then builds the executed-path tape and runs Layer 1 over it,
412/// spawning each non-executed region arm as its own tape. See the module docs
413/// for the tape model.
414///
415/// # Errors
416///
417/// See [`ProgramSliceError`]: plan materialization, flattening, a cyclic level, an
418/// unmergeable node's instances, the tracker rejecting the (post-flatten) op
419/// stream, or a boundary-seeded break that cannot be placed because the tape
420/// has no moment, or a nonlinear observable record fold (SEM-FOLD).
421pub fn program_detector_slices(program: &Bloq) -> Result<ProgramSlices, ProgramSliceError> {
422    program_detector_slices_with_options(program, &ProgramSliceOptions::default())
423}
424
425/// Compute detector-slice regions using explicit region choices.
426///
427/// [`ProgramSliceOptions::set_detectors_only`] omits logical-observable regions
428/// while retaining the same quantum tapes and detector regions.
429/// [`ProgramSliceOptions::set_physical_observables`] keeps logical regions but
430/// omits their known runtime sign folds. The default includes those folds and
431/// requires their complete record expressions to be affine.
432///
433/// # Errors
434///
435/// See [`ProgramSliceError`].
436pub fn program_detector_slices_with_options(
437    program: &Bloq,
438    options: &ProgramSliceOptions,
439) -> Result<ProgramSlices, ProgramSliceError> {
440    if program.has_conditional_membership() {
441        return Err(ProgramSliceError::MembershipSelectionRequired);
442    }
443    let mut program = program.clone();
444    program.flatten()?;
445    // SEM-MERGE/IR-02: build exactly the quantum plans tracked below. Full IR
446    // validation is an explicit audit, separate from this viewer operation.
447    let plans = program.emission_plans(&InstantiationOptions::default())?;
448    let known_measurements: FxHashSet<_> = plans
449        .iter()
450        .flat_map(|(_, plan)| plan.measurements.keys().copied())
451        .collect();
452    let known_instances: FxHashMap<_, _> = program
453        .levels()
454        .flat_map(|(_, level)| {
455            level.quantum_nodes().flat_map(|(_, quantum)| {
456                quantum
457                    .instances
458                    .iter()
459                    .map(|instance| (instance.id, instance.template_id))
460            })
461        })
462        .collect();
463    for (_, level) in program.levels() {
464        for (_, quantum) in level.quantum_nodes() {
465            program.check_detector_bundle_bindings(quantum, |instance| {
466                known_instances.get(&instance).copied()
467            })?;
468        }
469    }
470
471    let mut out = ProgramSlices::default();
472    let mut tape = TapeBuilder::default();
473    add_level_to_tape(
474        &program,
475        program.top(),
476        &LevelPath::default(),
477        &plans,
478        options,
479        &mut tape,
480    )?;
481    tape.finish(&known_measurements, &known_instances, &mut out)?;
482    Ok(out)
483}
484
485fn add_level_to_tape(
486    program: &Bloq,
487    level: &SubGraph,
488    path: &LevelPath,
489    plans: &ValidatedPlans,
490    options: &ProgramSliceOptions,
491    tape: &mut TapeBuilder,
492) -> Result<(), ProgramSliceError> {
493    for id in level.deterministic_emit_order()? {
494        match &level[id].kind {
495            BloqNodeKind::Quantum(_) => tape.add_node(program, path, id, &level[id], plans)?,
496            BloqNodeKind::Region(region) => {
497                for (selector, subgraph) in region.bodies() {
498                    add_level_to_tape(
499                        program,
500                        subgraph,
501                        &path.child(id, selector),
502                        plans,
503                        options,
504                        tape,
505                    )?;
506                }
507            }
508            BloqNodeKind::Classical(classical) => {
509                if !options.detectors_only
510                    && let ClassicalNode::Observable {
511                        index: Some(index), ..
512                    } = classical.as_ref()
513                {
514                    tape.stage_observable(level, path, id, *index, options)?;
515                }
516            }
517        }
518    }
519    Ok(())
520}
521
522fn evaluate_expression(
523    level: &SubGraph,
524    path: &LevelPath,
525    node: BloqNodeId,
526    expr: &ClassicalExpr,
527) -> Option<bool> {
528    evaluate_expression_cached(level, path, node, expr, &mut FxHashMap::default())
529}
530
531fn evaluate_expression_cached(
532    level: &SubGraph,
533    path: &LevelPath,
534    node: BloqNodeId,
535    expr: &ClassicalExpr,
536    cache: &mut FxHashMap<NodeRef, Option<bool>>,
537) -> Option<bool> {
538    expr.eval(&mut |slot| {
539        let producer = level
540            .value_inputs(node)
541            .find(|input| input.slot == slot)?
542            .producer;
543        evaluate_producer(level, path, producer, cache)
544    })
545}
546
547fn evaluate_producer(
548    level: &SubGraph,
549    path: &LevelPath,
550    producer: BloqNodeId,
551    cache: &mut FxHashMap<NodeRef, Option<bool>>,
552) -> Option<bool> {
553    let node_ref = NodeRef {
554        path: path.clone(),
555        node: producer,
556    };
557    if let Some(value) = cache.get(&node_ref) {
558        return *value;
559    }
560    cache.insert(node_ref.clone(), None);
561    if let Some(slot) = level[producer].activation
562        && evaluate_expression_cached(level, path, producer, &ClassicalExpr::In(slot), cache)
563            == Some(false)
564    {
565        cache.insert(node_ref, Some(false));
566        return Some(false);
567    }
568    let value = match level[producer].try_classical()? {
569        ClassicalNode::Compute { expr } => {
570            evaluate_expression_cached(level, path, producer, expr, cache)
571        }
572        ClassicalNode::Observable { .. } | ClassicalNode::Discard { .. } => None,
573    };
574    cache.insert(node_ref, value);
575    value
576}
577
578// ==============================================================================
579// Tape construction
580// ==============================================================================
581
582/// A positioned Pauli contribution to a logical observable.
583#[derive(Clone)]
584struct PendingPauliSeed {
585    instance: TemplateInstanceId,
586    face: BoundaryFace,
587    operator: PauliMap,
588}
589
590/// A detector or logical observable staged for resolution once the tape's
591/// whole measurement and instance-boundary maps are built.
592struct PendingRegion {
593    id: ProgramRegionId,
594    coords: Option<DetectorCoords>,
595    /// The parity's measurement terms, in instance space.
596    terms: Vec<InstanceMeasurement>,
597    /// Positioned Pauli-target contributions. Empty for detectors.
598    seeds: Vec<PendingPauliSeed>,
599}
600
601/// Accumulates one level's tape: the concatenated moment segments, the
602/// per-node segment ranges, shared instance maps, and regions staged for
603/// resolution.
604#[derive(Default)]
605struct TapeBuilder {
606    /// One entry per tape segment: the moment ops (measurement ids already
607    /// rewritten to tape-global space).
608    segments: Vec<Vec<Op>>,
609    /// For each tape segment, the node it belongs to and its node-local moment
610    /// index — so a break's segment maps back to a node timeline.
611    segment_owner: Vec<(NodeRef, usize)>,
612    /// Each quantum node's `(NodeRef, tape start, moment count)`. Present even
613    /// for a node with zero moments, so every node appears in `per_node`.
614    node_ranges: Vec<(NodeRef, usize, usize)>,
615    /// Instance-space measurement → tape-global id, spanning every node in the
616    /// tape (a cross-node parity resolves against all of them).
617    instance_to_global: FxHashMap<InstanceMeasurement, u32>,
618    /// Template instance → `(input boundary, output boundary)` in segment space.
619    instance_boundaries: FxHashMap<TemplateInstanceId, (usize, usize)>,
620    /// Detector and observable regions staged for resolution after the maps are
621    /// complete.
622    pending: Vec<PendingRegion>,
623    /// Next tape-global measurement id to hand out.
624    next_global: u32,
625}
626
627impl TapeBuilder {
628    /// Add one quantum node's ops and detector side table to the tape.
629    fn add_node(
630        &mut self,
631        program: &Bloq,
632        path: &LevelPath,
633        id: BloqNodeId,
634        node: &crate::BloqNode,
635        plans: &ValidatedPlans,
636    ) -> Result<(), ProgramSliceError> {
637        let node_ref = NodeRef {
638            path: path.clone(),
639            node: id,
640        };
641        let plan = plans
642            .get(path, id)
643            .expect("materialization returns one emission plan per quantum node (IR-02)");
644        let ops = plan
645            .circuit
646            .body(plan.circuit.entry_body())
647            .map(bloq_circuit::CircuitBody::ops)
648            .unwrap_or_default();
649
650        // Assign a tape-global id to each distinct node-local measurement id, in
651        // op order, and rewrite the ops to that space.
652        let mut local_to_global: FxHashMap<u32, u32> = FxHashMap::default();
653        for op in ops {
654            for &local in op_measurement_ids(op) {
655                local_to_global.entry(local).or_insert_with(|| {
656                    let global = self.next_global;
657                    self.next_global += 1;
658                    global
659                });
660            }
661        }
662        let rewritten: Vec<Op> = ops
663            .iter()
664            .map(|op| rewrite_measurement_ids(op, &local_to_global))
665            .collect();
666
667        // Extend the tape's shared instance→global map with this node's
668        // instances. Each instance belongs to exactly one node, so no key
669        // collides across the tape.
670        for (&instance_measurement, &local) in &plan.measurements {
671            if let Some(&global) = local_to_global.get(&local) {
672                self.instance_to_global.insert(instance_measurement, global);
673            }
674        }
675
676        // Bucket into moments and record the node's tape range.
677        let moments = moment_segments_owned(rewritten);
678        let start = self.segments.len();
679        for (moment, segment) in moments.into_iter().enumerate() {
680            self.segment_owner.push((node_ref.clone(), moment));
681            self.segments.push(segment.ops);
682        }
683        let len = self.segments.len() - start;
684        self.node_ranges.push((node_ref.clone(), start, len));
685
686        let quantum = node.expect_quantum();
687        for instance in &quantum.instances {
688            self.instance_boundaries
689                .insert(instance.id, (start, start + len));
690        }
691        self.stage_detectors(program, &node_ref, quantum)?;
692        Ok(())
693    }
694
695    /// Stage this node's template and node detectors for later resolution,
696    /// assigning each a stable index into the node's detector table (template
697    /// detectors first — instance then table order — then node detectors, as the
698    /// Stim emitter's `node_annotations` orders them).
699    fn stage_detectors(
700        &mut self,
701        program: &Bloq,
702        node_ref: &NodeRef,
703        quantum: &crate::QuantumNode,
704    ) -> Result<(), ProgramSliceError> {
705        let template_count = quantum
706            .instances
707            .iter()
708            .try_fold(0usize, |count, instance| {
709                let rows = program
710                    .templates()
711                    .get(instance.template_id)
712                    .expect("emission plan resolved template")
713                    .detectors
714                    .len();
715                count
716                    .checked_add(rows)
717                    .ok_or(crate::DetectorBundleError::CountOverflow)
718            })?;
719        let count = template_count
720            .checked_add(program.node_detector_count(quantum)?)
721            .ok_or(crate::DetectorBundleError::CountOverflow)?;
722        u32::try_from(count).map_err(|_| crate::DetectorBundleError::CountOverflow)?;
723        let mut index = 0u32;
724        for instance in &quantum.instances {
725            let template = program
726                .templates()
727                .get(instance.template_id)
728                .expect("emission plan already resolved every instance template");
729            for detector in &template.detectors {
730                // Template-local measurement ids name this instance.
731                let terms = detector
732                    .parity
733                    .measurements()
734                    .map(|measurement| InstanceMeasurement {
735                        instance: instance.id,
736                        measurement,
737                    })
738                    .collect();
739                self.pending.push(PendingRegion {
740                    id: ProgramRegionId::Detector {
741                        owner: node_ref.clone(),
742                        detector: index,
743                    },
744                    coords: detector.coords.as_ref().map(|coords| {
745                        bloq_circuit::translate_detector_coords(coords, instance.offset)
746                    }),
747                    terms,
748                    seeds: Vec::new(),
749                });
750                index += 1;
751            }
752        }
753        for detector in program.node_detectors(quantum)? {
754            self.pending.push(PendingRegion {
755                id: ProgramRegionId::Detector {
756                    owner: node_ref.clone(),
757                    detector: index,
758                },
759                // Node detector coords are already in layout space.
760                coords: detector
761                    .coords()
762                    .map(|coords| DetectorCoords::from_slice(&coords.collect::<Vec<_>>())),
763                terms: detector.measurements().collect(),
764                seeds: Vec::new(),
765            });
766            index += 1;
767        }
768        Ok(())
769    }
770
771    /// Stage one logical observable: its record-backed XOR parity plus positioned
772    /// boundary operators, including content exported by selected region bodies.
773    fn stage_observable(
774        &mut self,
775        level: &SubGraph,
776        path: &LevelPath,
777        node: BloqNodeId,
778        index: u32,
779        options: &ProgramSliceOptions,
780    ) -> Result<(), ProgramSliceError> {
781        let content = ObservableCollector {
782            cache: FxHashMap::default(),
783            physical_observables: options.physical_observables,
784        }
785        .inputs(level, path, node)?;
786
787        self.pending.push(PendingRegion {
788            id: ProgramRegionId::Observable { index },
789            coords: None,
790            terms: content.parity.measurements().collect(),
791            seeds: content.seeds,
792        });
793        Ok(())
794    }
795
796    /// Resolve staged regions against the completed tape maps, run Layer 1, and
797    /// scatter slices and breaks into `out`.
798    fn finish(
799        self,
800        known_measurements: &FxHashSet<InstanceMeasurement>,
801        known_instances: &FxHashMap<TemplateInstanceId, crate::TemplateId>,
802        out: &mut ProgramSlices,
803    ) -> Result<(), ProgramSliceError> {
804        // Resolve record parities and positioned Pauli seeds into tape space;
805        // drop any region reaching an instance outside this tape.
806        let (detectors, seeds, meta) =
807            {
808                let mut detectors = Vec::with_capacity(self.pending.len());
809                let mut seeds = Vec::new();
810                let mut meta: Vec<ProgramRegion> = Vec::with_capacity(self.pending.len());
811                for pending in self.pending {
812                    let PendingRegion {
813                        id: region_id,
814                        coords,
815                        terms,
816                        seeds: pending_seeds,
817                    } = pending;
818                    let parity = DetectorParity::from_measurements(terms);
819                    let mut measurements = Vec::with_capacity(parity.terms().len());
820                    let mut cross_tape = false;
821                    for term in parity.measurements() {
822                        match self.instance_to_global.get(&term) {
823                            Some(&global) => measurements.push(global),
824                            None => {
825                                if !known_measurements.contains(&term) {
826                                    return Err(ProgramSliceError::UnknownMeasurement {
827                                        region: region_id,
828                                        measurement: term,
829                                    });
830                                }
831                                cross_tape = true;
832                                break;
833                            }
834                        }
835                    }
836                    let pending_seeds = canonicalize_pauli_seeds(pending_seeds);
837                    let mut resolved_seeds = Vec::with_capacity(pending_seeds.len());
838                    for seed in pending_seeds {
839                        let Some(&(input, output)) = self.instance_boundaries.get(&seed.instance)
840                        else {
841                            if !known_instances.contains_key(&seed.instance) {
842                                return Err(ProgramSliceError::UnknownInstance {
843                                    region: region_id,
844                                    instance: seed.instance,
845                                });
846                            }
847                            cross_tape = true;
848                            break;
849                        };
850                        resolved_seeds.push((
851                            match seed.face {
852                                BoundaryFace::Input => input,
853                                BoundaryFace::Output => output,
854                            },
855                            pauli_region_terms(&seed.operator),
856                        ));
857                    }
858                    if cross_tape {
859                        out.skipped_cross_tape += 1;
860                        continue;
861                    }
862                    let id = u32::try_from(meta.len())
863                        .map_err(|_| ProgramSliceError::RegionIdOverflow)?;
864                    detectors.push(SliceDetector { id, measurements });
865                    seeds.extend(resolved_seeds.into_iter().map(|(boundary, terms)| {
866                        SliceRegionSeed {
867                            id,
868                            boundary,
869                            terms,
870                        }
871                    }));
872                    meta.push(ProgramRegion {
873                        id: region_id,
874                        coords: coords.map(Arc::new),
875                    });
876                }
877                (detectors, seeds, meta)
878            };
879
880        let region_base = checked_region_base(out.regions.len(), meta.len())?;
881        let segment_refs: Vec<&[Op]> = self.segments.iter().map(Vec::as_slice).collect();
882        let mut slices = detector_slices_with_seeds(&segment_refs, &detectors, &seeds)?;
883
884        // Every node appears, with an inner vec per moment (empty if no regions).
885        for (node_ref, start, len) in &self.node_ranges {
886            let mut node_slices = vec![Vec::new(); *len];
887            for (moment, slots) in node_slices.iter_mut().enumerate() {
888                for (detector_id, terms) in std::mem::take(&mut slices.slices[start + moment]) {
889                    slots.push(RegionView {
890                        region: region_base + detector_id,
891                        terms,
892                    });
893                }
894            }
895            out.per_node
896                .insert(node_ref.clone(), NodeSlices(node_slices));
897        }
898
899        for brk in slices.breaks {
900            let meta = &meta[brk.detector as usize];
901            let Some((node, moment)) = self.segment_owner.get(brk.segment).cloned() else {
902                return Err(ProgramSliceError::BreakOutsideTimeline {
903                    region: meta.id.clone(),
904                });
905            };
906            out.breaks.push(ProgramRegionBreak {
907                node,
908                moment,
909                region: meta.id.clone(),
910                qubit: brk.qubit,
911                kind: brk.kind,
912            });
913        }
914        out.regions.extend(meta);
915        Ok(())
916    }
917}
918
919/// XOR boundary operators before instance lookup. This lets a selected branch's
920/// delta cancel the base arm's symbolic faces even when that base instance lives
921/// on the isolated, non-selected tape.
922fn canonicalize_pauli_seeds(seeds: Vec<PendingPauliSeed>) -> Vec<PendingPauliSeed> {
923    let mut groups: FxHashMap<(TemplateInstanceId, BoundaryFace), PauliMap> = FxHashMap::default();
924    for seed in seeds {
925        groups
926            .entry((seed.instance, seed.face))
927            .and_modify(|operator| *operator = &*operator ^ &seed.operator)
928            .or_insert(seed.operator);
929    }
930    let mut seeds: Vec<PendingPauliSeed> = groups
931        .into_iter()
932        .filter_map(|((instance, face), operator)| {
933            (!operator.is_empty()).then_some(PendingPauliSeed {
934                instance,
935                face,
936                operator,
937            })
938        })
939        .collect();
940    seeds.sort_unstable_by_key(|seed| {
941        (
942            seed.instance.0,
943            match seed.face {
944                BoundaryFace::Input => 0,
945                BoundaryFace::Output => 1,
946            },
947        )
948    });
949    seeds
950}
951
952/// Canonical content is cached before parents merge it, so shared producers
953/// retain XOR multiplicity without expanding a DAG into a tree.
954#[derive(Clone, Default)]
955struct ObservableContent {
956    parity: NodeDetectorParity,
957    seeds: Vec<PendingPauliSeed>,
958}
959
960impl ObservableContent {
961    fn xor_assign(&mut self, other: Self) {
962        self.parity.xor_assign(&other.parity);
963        self.seeds.extend(other.seeds);
964        self.seeds = canonicalize_pauli_seeds(std::mem::take(&mut self.seeds));
965    }
966}
967
968#[derive(Clone, Copy, PartialEq, Eq, Hash)]
969enum ContentKind {
970    Value,
971    Boundary,
972}
973
974struct ObservableCollector {
975    physical_observables: bool,
976    cache: FxHashMap<(NodeRef, ContentKind), ObservableContent>,
977}
978
979impl ObservableCollector {
980    /// Skip a recipe when its pinned dataflow proves its activation false.
981    fn inactive(&self, level: &SubGraph, path: &LevelPath, node: BloqNodeId) -> bool {
982        level[node].activation.is_some_and(|slot| {
983            evaluate_expression(level, path, node, &ClassicalExpr::In(slot)) == Some(false)
984        })
985    }
986
987    fn inputs(
988        &mut self,
989        level: &SubGraph,
990        path: &LevelPath,
991        node: BloqNodeId,
992    ) -> Result<ObservableContent, ProgramSliceError> {
993        let mut content = ObservableContent::default();
994        if self.inactive(level, path, node) {
995            return Ok(content);
996        }
997        if let Some(ClassicalNode::Observable {
998            measurements,
999            operators,
1000            ..
1001        }) = level[node].try_classical()
1002        {
1003            content.parity = NodeDetectorParity::from_measurements(measurements.iter().copied());
1004            content.seeds = canonicalize_pauli_seeds(
1005                operators
1006                    .iter()
1007                    .map(|operator| PendingPauliSeed {
1008                        instance: operator.instance,
1009                        face: operator.face,
1010                        operator: operator.operator.clone(),
1011                    })
1012                    .collect(),
1013            );
1014        }
1015        for input in level.data_inputs(node) {
1016            // Runtime folds change the known sign of this physical equation;
1017            // they are not extra measurement support for its decoder query.
1018            if self.physical_observables && *input.role != crate::ValueRole::Data {
1019                continue;
1020            }
1021            if input.output != Some(crate::ObservableOutput::Flip) {
1022                content.xor_assign(self.producer(
1023                    level,
1024                    path,
1025                    input.producer,
1026                    ContentKind::Value,
1027                )?);
1028            }
1029            if input.output.is_none() {
1030                content.xor_assign(self.producer(
1031                    level,
1032                    path,
1033                    input.producer,
1034                    ContentKind::Boundary,
1035                )?);
1036            }
1037        }
1038        Ok(content)
1039    }
1040
1041    fn producer(
1042        &mut self,
1043        level: &SubGraph,
1044        path: &LevelPath,
1045        producer: BloqNodeId,
1046        kind: ContentKind,
1047    ) -> Result<ObservableContent, ProgramSliceError> {
1048        let node_ref = NodeRef {
1049            path: path.clone(),
1050            node: producer,
1051        };
1052        let key = (node_ref.clone(), kind);
1053        if let Some(content) = self.cache.get(&key) {
1054            return Ok(content.clone());
1055        }
1056        let mut content = ObservableContent::default();
1057        if self.inactive(level, path, producer) {
1058            self.cache.insert(key, content.clone());
1059            return Ok(content);
1060        }
1061        match (level[producer].try_classical(), &level[producer].kind) {
1062            (Some(ClassicalNode::Observable { .. }), _) => {
1063                content = self.inputs(level, path, producer)?;
1064                match kind {
1065                    ContentKind::Value => content.seeds.clear(),
1066                    ContentKind::Boundary => content.parity = NodeDetectorParity::default(),
1067                }
1068            }
1069            (Some(ClassicalNode::Compute { expr }), _) if kind == ContentKind::Value => {
1070                // WF-11 makes each linear input occur once. Nonlinear inputs
1071                // cannot be interpreted as a record parity (SEM-FOLD).
1072                if !expr.is_linear() {
1073                    return Err(ProgramSliceError::NonLinearExpr { node: node_ref });
1074                }
1075                content = self.inputs(level, path, producer)?;
1076            }
1077            (_, BloqNodeKind::Region(region)) => {
1078                let selected = BodySelector::Body;
1079                let body = region
1080                    .bodies()
1081                    .find_map(|(selector, body)| (selector == selected).then_some(body))
1082                    .expect("regions have one body");
1083                let child_path = path.child(producer, selected);
1084                match kind {
1085                    ContentKind::Boundary => {
1086                        for &output in body.boundary_outputs() {
1087                            content.xor_assign(self.producer(body, &child_path, output, kind)?);
1088                        }
1089                    }
1090                    ContentKind::Value => {
1091                        if let Some(output) = body.value_output()
1092                            && output.output != crate::ObservableOutput::Flip
1093                        {
1094                            content = self.producer(body, &child_path, output.node, kind)?;
1095                        }
1096                    }
1097                }
1098            }
1099            // Other nodes do not produce this channel.
1100            (_, BloqNodeKind::Classical(_) | BloqNodeKind::Quantum(_)) => {}
1101        }
1102        self.cache.insert(key, content.clone());
1103        Ok(content)
1104    }
1105}
1106
1107fn pauli_region_terms(operator: &bloq_circuit::PauliMap) -> Vec<RegionTerm> {
1108    operator
1109        .iter()
1110        .filter_map(|(&qubit, &pauli)| {
1111            let pauli = match pauli {
1112                Pauli::X => PauliBasis::X,
1113                Pauli::Y => PauliBasis::Y,
1114                Pauli::Z => PauliBasis::Z,
1115                Pauli::I => return None,
1116            };
1117            Some(RegionTerm { qubit, pauli })
1118        })
1119        .collect()
1120}
1121
1122/// The measurement ids an op writes (for id remapping). Only `Measure`/`MPP`
1123/// produce records; other ops write none.
1124fn op_measurement_ids(op: &Op) -> &[u32] {
1125    match op {
1126        Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => measurements,
1127        Op::Gate { .. }
1128        | Op::Tick
1129        | Op::Repeat { .. }
1130        | Op::ConditionalPauli(_)
1131        | Op::Depolarize1 { .. }
1132        | Op::Depolarize2 { .. }
1133        | Op::PauliError { .. } => &[],
1134    }
1135}
1136
1137/// Clone `op`, remapping its measurement record ids through `map`.
1138/// Measurement-controlled feedforward uses the same tape-global id space;
1139/// node-scoped `Value` controls pass through unchanged.
1140fn rewrite_measurement_ids(op: &Op, map: &FxHashMap<u32, u32>) -> Op {
1141    let remap = |ids: &[u32]| -> Vec<u32> {
1142        ids.iter()
1143            .map(|id| {
1144                *map.get(id)
1145                    .expect("every op measurement id was assigned a global id")
1146            })
1147            .collect()
1148    };
1149    match op {
1150        Op::Measure {
1151            basis,
1152            qubits,
1153            measurements,
1154            flip_probability,
1155        } => Op::Measure {
1156            basis: *basis,
1157            qubits: qubits.clone(),
1158            measurements: remap(measurements),
1159            flip_probability: *flip_probability,
1160        },
1161        Op::MPP {
1162            products,
1163            measurements,
1164        } => Op::MPP {
1165            products: products.clone(),
1166            measurements: remap(measurements),
1167        },
1168        Op::ConditionalPauli(corrections) => Op::ConditionalPauli(
1169            corrections
1170                .iter()
1171                .copied()
1172                .map(|mut correction| {
1173                    correction.control = *map
1174                        .get(&correction.control)
1175                        .expect("every measurement control names a produced record");
1176                    correction
1177                })
1178                .collect(),
1179        ),
1180        other => other.clone(),
1181    }
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186    #[test]
1187    fn region_indices_reject_overflow_before_scattering() {
1188        let maximum = u32::MAX as usize;
1189        assert_eq!(super::checked_region_base(maximum, 0).unwrap(), u32::MAX);
1190        assert!(matches!(
1191            super::checked_region_base(maximum, 1),
1192            Err(super::ProgramSliceError::RegionIdOverflow)
1193        ));
1194        assert!(matches!(
1195            super::checked_region_base(usize::MAX, 1),
1196            Err(super::ProgramSliceError::RegionIdOverflow)
1197        ));
1198    }
1199    use bloq_circuit::{
1200        CircuitBody, ConditionalCorrection, CoordCircuit, DetectorTerm, GateType, LoopStateId,
1201    };
1202    use glam::ivec2;
1203
1204    use super::*;
1205    use crate::test_fixture::instance_measurement;
1206    use crate::{
1207        BloqEdge, BloqNode, BloqTemplate, InstanceBoundaryOperator, RegionNode, TemplateDetector,
1208        TemplateDetectorScope, TemplateInstance, TemplateRepeatState,
1209    };
1210
1211    /// A one-measurement `M Z` template at `q`.
1212    fn z_measure_template(q: IVec2) -> BloqTemplate {
1213        let mut circuit = CoordCircuit::new();
1214        circuit.measure(PauliBasis::Z, [q]);
1215        BloqTemplate::new(circuit)
1216    }
1217
1218    fn single_instance(id: u32, template: crate::TemplateId) -> Vec<TemplateInstance> {
1219        vec![TemplateInstance::new(
1220            TemplateInstanceId(id),
1221            template,
1222            ivec2(0, 0),
1223        )]
1224    }
1225
1226    #[test]
1227    fn pinned_membership_slices_only_the_selected_observable_records() {
1228        let program = Bloq::from_text(
1229            "BLOQIR 1
1230 template t0 {
1231  circuit {
1232   Z (0,0)
1233   M (0,0):m0
1234  }
1235 }
1236 graph {
1237  n0 quantum {
1238   instance i0 t0 @ (0,0)
1239  }
1240  n1 observable fragment measurements i0:m0
1241  n2 compute in0 from selector choice
1242  n3 compute !in0
1243  n4 quantum {
1244   instance i1 t0 @ (1,0)
1245   instance i2 t0 @ (2,0)
1246   guard 0 i1
1247   guard 1 i2
1248  }
1249  n5 observable fragment measurements i1:m0 when v0
1250  n6 observable fragment measurements i2:m0 when v0
1251  n7 observable 0
1252  n0 -> n1 order
1253  n1 -> n2 value 0
1254  n2 -> n3 value 0
1255  n2 -> n4 value 0
1256  n3 -> n4 value 1
1257  n2 -> n5 value 0
1258  n3 -> n6 value 0
1259  n4 -> n5 order
1260  n4 -> n6 order
1261  n5 -> n7 compose 0
1262  n6 -> n7 compose 1
1263 }",
1264        )
1265        .unwrap();
1266        assert!(matches!(
1267            program_detector_slices(&program),
1268            Err(ProgramSliceError::MembershipSelectionRequired)
1269        ));
1270        for choice in [false, true] {
1271            let pinned = program
1272                .pin_membership(&[("choice".into(), choice)].into())
1273                .unwrap();
1274            let slices = program_detector_slices(&pinned).unwrap();
1275            let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(4))];
1276            assert_eq!(timeline.0.len(), 2);
1277            assert_eq!(timeline.0[0].len(), 1);
1278            assert_eq!(
1279                timeline.0[0][0].terms,
1280                vec![RegionTerm {
1281                    qubit: ivec2(if choice { 1 } else { 2 }, 0),
1282                    pauli: PauliBasis::Z
1283                }]
1284            );
1285            assert_eq!(
1286                slices.regions[timeline.0[0][0].region as usize].id,
1287                ProgramRegionId::Observable { index: 0 }
1288            );
1289        }
1290    }
1291
1292    #[test]
1293    fn physical_observable_support_excludes_runtime_sign_folds() {
1294        for role in ["feedback 0", "readout"] {
1295            for expression in ["in0 & in1", "in0 ^ in1"] {
1296                let program = Bloq::from_text(&format!(
1297                    "BLOQIR 1
1298 template t0 {{
1299   circuit {{
1300     RX (0,0) (1,0) (2,0)
1301     TICK
1302     M (0,0):m0 (1,0):m1 (2,0):m2
1303   }}
1304 }}
1305 graph {{
1306   n0 quantum {{
1307     instance i0 t0 @ (0,0)
1308   }}
1309   n1 observable fragment measurements i0:m0
1310   n2 observable fragment measurements i0:m1
1311   n3 observable fragment measurements i0:m2
1312   n4 compute {expression}
1313   n5 observable 0
1314   n0 -> n1 order
1315   n0 -> n2 order
1316   n0 -> n3 order
1317   n2 -> n4 value 0
1318   n3 -> n4 value 1
1319   n1 -> n5 compose 0
1320   n4 -> n5 value 1 {role}
1321 }}"
1322                ))
1323                .unwrap();
1324                let mut options = ProgramSliceOptions::default();
1325                options.set_physical_observables(true);
1326                let slices = program_detector_slices_with_options(&program, &options).unwrap();
1327                let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(0))];
1328                assert_eq!(timeline.0[0].len(), 1);
1329                assert_eq!(
1330                    timeline.0[0][0].terms,
1331                    vec![RegionTerm {
1332                        qubit: ivec2(0, 0),
1333                        pauli: PauliBasis::Z,
1334                    }]
1335                );
1336                if expression.contains('&') {
1337                    assert!(matches!(
1338                        program_detector_slices(&program),
1339                        Err(ProgramSliceError::NonLinearExpr { .. })
1340                    ));
1341                } else {
1342                    let full = program_detector_slices(&program).unwrap();
1343                    assert_eq!(
1344                        full.per_node[&NodeRef::top_level(BloqNodeId(0))].0[0][0]
1345                            .terms
1346                            .len(),
1347                        3
1348                    );
1349                }
1350            }
1351        }
1352    }
1353
1354    #[test]
1355    fn nested_recipe_preserves_physical_roles_and_boundary_bindings() {
1356        let source = "BLOQIR 1
1357template t0 {
1358  circuit {
1359    RX (0,0) (1,0)
1360    TICK
1361    M (0,0):m0 (1,0):m1
1362  }
1363}
1364graph {
1365  n0 quantum {
1366    instance i0 t0 @ (0,0)
1367  }
1368  n1 observable fragment measurements i0:m0
1369  n2 observable fragment measurements i0:m1
1370  n3 observable 0
1371  n4 compute in0
1372  n5 observable fragment operators i0 output Z(0,0)
1373  n6 observable fragment
1374  n7 observable fragment
1375  n8 observable 1
1376  n9 compute in0
1377  n10 observable 2
1378  n11 compute in0
1379  n12 compute 0
1380  n13 observable fragment operators i0 output X(1,0) when v0
1381  n0 -> n1 order
1382  n0 -> n2 order
1383  n0 -> n5 order
1384  n0 -> n13 order
1385  n12 -> n13 value 0
1386  n2 -> n3 compose 0
1387  n3 -> n4 value 0
1388  n1 -> n6 compose 0
1389  n4 -> n6 value 1 readout
1390  n1 -> n6 value 2 feedback 7
1391  n5 -> n6 compose 3
1392  n13 -> n6 compose 4
1393  n6 -> n7 compose 0
1394  __OBS_EDGES__
1395  n8 -> n9 value 0
1396  n10 -> n11 value 0
1397}";
1398        let recipe = Bloq::from_text(
1399            &source.replace("__OBS_EDGES__", "n7 -> n8 compose 0\n  n7 -> n10 compose 0"),
1400        )
1401        .unwrap();
1402        let flat = Bloq::from_text(&source.replace(
1403            "__OBS_EDGES__",
1404            "n1 -> n8 compose 0\n  n4 -> n8 value 1 readout\n  n1 -> n8 value 2 feedback 7\n  n5 -> n8 compose 3\n  n13 -> n8 compose 4\n  n1 -> n10 compose 0\n  n4 -> n10 value 1 readout\n  n1 -> n10 value 2 feedback 7\n  n5 -> n10 compose 3\n  n13 -> n10 compose 4",
1405        ))
1406        .unwrap();
1407        recipe.validate().unwrap();
1408        flat.validate().unwrap();
1409        for node in [BloqNodeId(8), BloqNodeId(10)] {
1410            for physical_observables in [false, true] {
1411                let collect = |program: &Bloq| {
1412                    ObservableCollector {
1413                        physical_observables,
1414                        cache: FxHashMap::default(),
1415                    }
1416                    .inputs(program.top(), &LevelPath::default(), node)
1417                    .unwrap()
1418                };
1419                let shared = collect(&recipe);
1420                let direct = collect(&flat);
1421                assert_eq!(shared.parity, direct.parity);
1422                assert_eq!(shared.seeds.len(), direct.seeds.len());
1423                assert_eq!(shared.seeds.len(), 1);
1424                assert_eq!(shared.seeds[0].instance, direct.seeds[0].instance);
1425                assert_eq!(shared.seeds[0].face, direct.seeds[0].face);
1426                assert_eq!(shared.seeds[0].operator, direct.seeds[0].operator);
1427                if physical_observables {
1428                    assert_eq!(
1429                        shared.parity,
1430                        NodeDetectorParity::from_measurements([instance_measurement(0, 0)])
1431                    );
1432                }
1433            }
1434        }
1435        for physical_observables in [false, true] {
1436            let mut options = ProgramSliceOptions::default();
1437            options.set_physical_observables(physical_observables);
1438            let shared = program_detector_slices_with_options(&recipe, &options).unwrap();
1439            let direct = program_detector_slices_with_options(&flat, &options).unwrap();
1440            assert_eq!(shared.regions, direct.regions);
1441            assert_eq!(shared.per_node, direct.per_node);
1442            assert_eq!(shared.breaks, direct.breaks);
1443            assert_eq!(shared.skipped_cross_tape, direct.skipped_cross_tape);
1444        }
1445    }
1446
1447    #[test]
1448    fn nonlinear_observable_record_folds_are_rejected() {
1449        for expression in ["in0 & 0", "in0 | 1", "in0 & in1", "select(in0, 0, 1)"] {
1450            let second = if expression.contains("in1") {
1451                "n1 -> n2 value 1"
1452            } else {
1453                ""
1454            };
1455            let mut program = Bloq::from_text(&format!(
1456                "BLOQIR 1
1457 template t0 {{
1458   circuit {{
1459     RX (0,0)
1460     TICK
1461     M (0,0):m0
1462   }}
1463 }}
1464 graph {{
1465   n0 quantum {{
1466     instance i0 t0 @ (0,0)
1467   }}
1468   n1 observable fragment measurements i0:m0
1469   n2 compute {expression}
1470   n3 observable 0
1471   n0 -> n1 order
1472   n1 -> n2 value 0
1473   {second}
1474   n2 -> n3 value 0
1475 }}
1476"
1477            ))
1478            .unwrap();
1479            program
1480                .node_mut(BloqNodeId(0))
1481                .unwrap()
1482                .expect_quantum_mut()
1483                .detectors
1484                .push(crate::NodeDetector {
1485                    parity: NodeDetectorParity::from_measurements([instance_measurement(0, 0)]),
1486                    coords: None,
1487                });
1488            program.validate().unwrap();
1489            assert!(
1490                matches!(program_detector_slices(&program), Err(ProgramSliceError::NonLinearExpr { node }) if node == NodeRef::top_level(BloqNodeId(2)))
1491            );
1492            let mut options = ProgramSliceOptions::default();
1493            options.set_physical_observables(true);
1494            assert!(matches!(
1495                program_detector_slices_with_options(&program, &options),
1496                Err(ProgramSliceError::NonLinearExpr { .. })
1497            ));
1498            options.set_detectors_only(true);
1499            let slices = program_detector_slices_with_options(&program, &options).unwrap();
1500            assert_eq!(
1501                slices
1502                    .regions
1503                    .iter()
1504                    .map(|region| &region.id)
1505                    .collect::<Vec<_>>(),
1506                [&ProgramRegionId::Detector {
1507                    owner: NodeRef::top_level(BloqNodeId(0)),
1508                    detector: 0
1509                }]
1510            );
1511        }
1512    }
1513
1514    #[test]
1515    fn upstream_observable_bits_carry_records_without_boundary_operators() {
1516        let program = Bloq::from_text(
1517            "BLOQIR 1
1518 template t0 {
1519   circuit {
1520     RX (0,0)
1521     R (1,0)
1522     TICK
1523     M (1,0):m0
1524   }
1525 }
1526 graph {
1527   n0 quantum {
1528     instance i0 t0 @ (0,0)
1529   }
1530   n1 observable fragment operators i0 output X(0,0)
1531   n2 observable 0
1532   n3 compute in0
1533   n4 observable 1
1534   n5 observable fragment measurements i0:m0
1535   n0 -> n5 order
1536   n1 -> n2 compose 0
1537   n5 -> n2 compose 1
1538   n2 -> n3 value 0
1539   n3 -> n4 value 0
1540 }
1541",
1542        )
1543        .unwrap();
1544        let slices = program_detector_slices(&program).unwrap();
1545        let mut raw_record = false;
1546        let mut direct_boundary = false;
1547        for view in slices
1548            .per_node
1549            .values()
1550            .flat_map(|node| node.0.iter().flatten())
1551        {
1552            match slices.regions[view.region as usize].id {
1553                ProgramRegionId::Observable { index: 0 } => {
1554                    direct_boundary |= view.terms.iter().any(|term| term.qubit == ivec2(0, 0))
1555                }
1556                ProgramRegionId::Observable { index: 1 } => {
1557                    assert!(view.terms.iter().all(|term| term.qubit != ivec2(0, 0)));
1558                    raw_record |= view.terms.iter().any(|term| term.qubit == ivec2(1, 0));
1559                }
1560                _ => {}
1561            }
1562        }
1563        assert!(raw_record && direct_boundary);
1564        assert!(slices.breaks.is_empty());
1565    }
1566
1567    #[test]
1568    fn complete_observable_owns_direct_records_and_boundary_but_consumers_read_only_records() {
1569        let mut program = Bloq::from_text(
1570            "BLOQIR 1
1571 template t0 {
1572   circuit {
1573     RX (0,0)
1574     R (1,0)
1575     TICK
1576     M (1,0):m0
1577   }
1578 }
1579 graph {
1580   n0 quantum {
1581     instance i0 t0 @ (0,0)
1582   }
1583   n1 observable 0
1584   n2 observable fragment
1585   n3 observable 1
1586   n4 observable 2
1587   n0 -> n1 order
1588   n1 -> n2 compose 0
1589   n1 -> n3 value 0
1590   n2 -> n4 value 0
1591 }
1592",
1593        )
1594        .unwrap();
1595        program.node_mut(BloqNodeId(1)).unwrap().kind = BloqNodeKind::Classical(
1596            ClassicalNode::Observable {
1597                index: Some(0),
1598                measurements: vec![instance_measurement(0, 0)],
1599                operators: vec![InstanceBoundaryOperator {
1600                    instance: TemplateInstanceId(0),
1601                    face: BoundaryFace::Output,
1602                    operator: PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::X)]),
1603                }],
1604            }
1605            .into(),
1606        );
1607        program.validate().unwrap();
1608        let mut collector = ObservableCollector {
1609            physical_observables: false,
1610            cache: FxHashMap::default(),
1611        };
1612        let direct = collector
1613            .inputs(program.top(), &LevelPath::default(), BloqNodeId(1))
1614            .unwrap();
1615        assert_eq!(
1616            direct.parity,
1617            NodeDetectorParity::from_measurements([instance_measurement(0, 0)])
1618        );
1619        assert_eq!(direct.seeds.len(), 1);
1620        for node in [BloqNodeId(3), BloqNodeId(4)] {
1621            let consumer = collector
1622                .inputs(program.top(), &LevelPath::default(), node)
1623                .unwrap();
1624            assert_eq!(consumer.parity, direct.parity);
1625            assert!(consumer.seeds.is_empty());
1626        }
1627        let slices = program_detector_slices(&program).unwrap();
1628        let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(0))];
1629        let mut seen = FxHashSet::default();
1630        for view in timeline.0.iter().flatten() {
1631            let ProgramRegionId::Observable { index } = slices.regions[view.region as usize].id
1632            else {
1633                continue;
1634            };
1635            seen.insert(index);
1636            if index != 0 {
1637                assert!(view.terms.iter().all(|term| term.qubit != ivec2(0, 0)));
1638            }
1639        }
1640        assert_eq!(seen, FxHashSet::from_iter([0, 1, 2]));
1641        assert!(slices.breaks.is_empty());
1642    }
1643
1644    #[test]
1645    fn shared_observable_dags_cancel_records_and_positioned_operators() {
1646        let mut program = Bloq::from_text(
1647            "BLOQIR 1
1648 template t0 {
1649   circuit {
1650     RX (0,0)
1651     R (1,0)
1652     TICK
1653     M (1,0):m0
1654   }
1655 }
1656 graph {
1657   n0 quantum {
1658     instance i0 t0 @ (0,0)
1659   }
1660   n1 rus 0 {
1661     body {
1662       n0 observable fragment operators i0 output X(0,0)
1663       n1 observable fragment measurements i0:m0
1664     }
1665   }
1666   n2 observable 0
1667   n0 -> n1 order
1668   n1 -> n2 compose 0
1669 }
1670",
1671        )
1672        .unwrap();
1673        let expected = program_detector_slices(&program).unwrap();
1674        let mut previous = BloqNodeId(1);
1675        let mut predicate = program.add_node(BloqNode::classical(ClassicalNode::Compute {
1676            expr: ClassicalExpr::Const(true),
1677        }));
1678        for _ in 0..30 {
1679            for previous in [&mut previous, &mut predicate] {
1680                let next = program.add_node(BloqNode::classical(ClassicalNode::Compute {
1681                    expr: ClassicalExpr::Xor(Box::new([
1682                        ClassicalExpr::In(0),
1683                        ClassicalExpr::In(1),
1684                    ])),
1685                }));
1686                program.add_edge(*previous, next, BloqEdge::value(0));
1687                program.add_edge(*previous, next, BloqEdge::value(1));
1688                *previous = next;
1689            }
1690        }
1691        let BloqNodeKind::Region(RegionNode::RepeatUntilSuccess {
1692            restart_condition: condition,
1693            ..
1694        }) = &mut program.top_mut().node_mut(BloqNodeId(1)).unwrap().kind
1695        else {
1696            unreachable!()
1697        };
1698        *condition = ClassicalExpr::Or(Box::new([
1699            ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
1700            ClassicalExpr::Const(false),
1701        ]));
1702        program.add_edge(predicate, BloqNodeId(1), BloqEdge::value(0));
1703        program.add_edge(previous, BloqNodeId(2), BloqEdge::value(1));
1704        let actual = program_detector_slices(&program).unwrap();
1705        assert_eq!(actual.regions, expected.regions);
1706        assert_eq!(actual.per_node, expected.per_node);
1707        assert_eq!(actual.breaks, expected.breaks);
1708    }
1709
1710    #[test]
1711    fn post_flatten_validation_is_wf_equivalent_for_looped_detector() {
1712        let q = ivec2(0, 0);
1713        let mut circuit = CoordCircuit::new();
1714        let seed = circuit.measure(PauliBasis::Z, [q])[0];
1715        let repeated = circuit.reserve_measurement_id(q);
1716        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1717            basis: PauliBasis::Z,
1718            qubits: vec![q],
1719            measurements: vec![repeated],
1720            flip_probability: 0.0,
1721        }]));
1722        circuit
1723            .body_mut(circuit.entry_body())
1724            .expect("entry body")
1725            .ops_mut()
1726            .push(Op::Repeat {
1727                body,
1728                repetitions: 3,
1729            });
1730        let state = LoopStateId(0);
1731        let template = BloqTemplate::with_parts(
1732            circuit,
1733            vec![TemplateDetector {
1734                scope: TemplateDetectorScope::RepeatBody { body },
1735                parity: DetectorParity::from_terms([
1736                    DetectorTerm::Measurement(repeated),
1737                    DetectorTerm::LoopState(state),
1738                ]),
1739                coords: None,
1740            }],
1741            vec![TemplateRepeatState {
1742                body,
1743                state,
1744                initial: DetectorParity::from_measurements([seed]),
1745                next: DetectorParity::from_measurements([repeated]),
1746            }],
1747            Vec::new(),
1748            Vec::new(),
1749        );
1750        let mut program = Bloq::new();
1751        let template = program.add_template(template);
1752        let mut node = BloqNode::from_members(vec![]);
1753        node.expect_quantum_mut().instances = single_instance(0, template);
1754        program.add_node(node);
1755
1756        let before = program.validate();
1757        let mut flattened = program.clone();
1758        flattened.flatten().expect("representative loop flattens");
1759
1760        assert_eq!(before, Ok(()));
1761        assert_eq!(flattened.validate(), before);
1762    }
1763
1764    #[test]
1765    fn malformed_instance_reference_fails_resolution_instead_of_skipping() {
1766        let q = ivec2(0, 0);
1767        let mut bloq = Bloq::new();
1768        let template = bloq.add_template(z_measure_template(q));
1769        let mut node = BloqNode::from_members(vec![]);
1770        node.expect_quantum_mut().instances = single_instance(0, template);
1771        node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1772            parity: DetectorParity::from_measurements([instance_measurement(7, 0)]),
1773            coords: None,
1774        }];
1775        bloq.add_node(node);
1776
1777        assert!(matches!(
1778            program_detector_slices(&bloq),
1779            Err(ProgramSliceError::UnknownMeasurement {
1780                measurement: InstanceMeasurement {
1781                    instance: TemplateInstanceId(7),
1782                    ..
1783                },
1784                ..
1785            })
1786        ));
1787    }
1788
1789    #[test]
1790    fn unknown_observable_instance_fails_resolution() {
1791        let program = Bloq::from_text(
1792            "BLOQIR 1
1793template t0 {
1794 circuit {
1795  R (0,0)
1796 }
1797}
1798graph {
1799 n0 quantum {
1800  instance i0 t0 @ (0,0)
1801 }
1802 n1 observable fragment operators i7 output X(0,0)
1803 n2 observable 0
1804 n1 -> n2 compose 0
1805}",
1806        )
1807        .unwrap();
1808        assert!(matches!(
1809            program_detector_slices(&program),
1810            Err(ProgramSliceError::UnknownInstance {
1811                instance: TemplateInstanceId(7),
1812                ..
1813            })
1814        ));
1815    }
1816
1817    #[test]
1818    fn slicing_does_not_audit_unrelated_logical_cut_metadata() {
1819        let mut program = Bloq::new();
1820        program.set_logical_inputs(vec![crate::LogicalInput {
1821            port: glam::IVec3::ZERO,
1822            instance: TemplateInstanceId(7),
1823            x: PauliMap::empty(),
1824            z: PauliMap::empty(),
1825        }]);
1826        assert!(matches!(
1827            program.validate(),
1828            Err(BloqValidationError::LogicalInputUnknownInstance { .. })
1829        ));
1830        let slices = program_detector_slices(&program).unwrap();
1831        assert!(slices.regions.is_empty());
1832        assert!(slices.per_node.is_empty());
1833    }
1834
1835    #[test]
1836    fn cross_node_detector_region_spans_both_node_timelines() {
1837        let q = ivec2(0, 0);
1838        // Child keeps the qubit alive through a Pauli `Z` (a tracker no-op) so
1839        // the region survives into the child's first moment before its measure.
1840        let mut child_circuit = CoordCircuit::new();
1841        child_circuit.do_gate(GateType::Z, [q]).unwrap();
1842        child_circuit.measure(PauliBasis::Z, [q]);
1843
1844        let mut bloq = Bloq::new();
1845        let parent_tpl = bloq.add_template(z_measure_template(q));
1846        let child_tpl = bloq.add_template(BloqTemplate::new(child_circuit));
1847
1848        let mut parent = BloqNode::from_members(vec![]);
1849        parent.expect_quantum_mut().instances = single_instance(0, parent_tpl);
1850        let mut child = BloqNode::from_members(vec![]);
1851        child.expect_quantum_mut().instances = single_instance(1, child_tpl);
1852        child.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1853            parity: DetectorParity::from_measurements([
1854                instance_measurement(0, 0),
1855                instance_measurement(1, 0),
1856            ]),
1857            coords: Some(DetectorCoords::from_slice(&[5.0, 6.0])),
1858        }];
1859
1860        let parent_id = bloq.add_node(parent);
1861        let child_id = bloq.add_node(child);
1862        bloq.add_edge(parent_id, child_id, BloqEdge::quantum(vec![]));
1863
1864        let slices = program_detector_slices(&bloq).unwrap();
1865        assert_eq!(slices.skipped_cross_tape, 0);
1866        assert!(slices.breaks.is_empty());
1867
1868        let parent_ref = NodeRef::top_level(parent_id);
1869        let child_ref = NodeRef::top_level(child_id);
1870        let parent_slices = &slices.per_node[&parent_ref];
1871        let child_slices = &slices.per_node[&child_ref];
1872        assert_eq!(parent_slices.0.len(), 1, "parent: one measurement moment");
1873        assert_eq!(child_slices.0.len(), 2, "child: rotation then measurement");
1874
1875        let expected = vec![RegionTerm {
1876            qubit: q,
1877            pauli: PauliBasis::Z,
1878        }];
1879        // Region alive at the end of the parent's measurement and the child's
1880        // first (pre-measure) moment; gone after the child measures.
1881        assert_eq!(parent_slices.0[0].len(), 1);
1882        assert_eq!(child_slices.0[0].len(), 1);
1883        assert!(child_slices.0[1].is_empty());
1884
1885        let region = &parent_slices.0[0][0];
1886        assert_eq!(
1887            slices.regions[region.region as usize].id,
1888            ProgramRegionId::Detector {
1889                owner: child_ref,
1890                detector: 0,
1891            },
1892            "child's side table owns it"
1893        );
1894        assert_eq!(
1895            slices.regions[region.region as usize]
1896                .coords
1897                .as_ref()
1898                .map(|coords| coords.as_slice()),
1899            Some(&[5.0, 6.0][..])
1900        );
1901        assert_eq!(region.terms, expected);
1902        assert_eq!(child_slices.0[0][0].terms, expected);
1903    }
1904
1905    #[test]
1906    fn measurement_controlled_pauli_cancels_program_detector_dependency() {
1907        let parent_q = ivec2(9, 0);
1908        let q0 = ivec2(0, 0);
1909        let q1 = ivec2(1, 0);
1910
1911        let mut child_circuit = CoordCircuit::new();
1912        child_circuit.do_gate(GateType::RX, [q0]).unwrap();
1913        child_circuit.do_gate(GateType::RZ, [q1]).unwrap();
1914        child_circuit.tick();
1915        let m0 = child_circuit.measure(PauliBasis::Z, [q0])[0];
1916        child_circuit
1917            .body_mut(child_circuit.entry_body())
1918            .unwrap()
1919            .ops_mut()
1920            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
1921                pauli: PauliBasis::X,
1922                control: m0,
1923                target: q1,
1924            }]));
1925        let m1 = child_circuit.measure(PauliBasis::Z, [q1])[0];
1926
1927        let mut bloq = Bloq::new();
1928        let parent_template = bloq.add_template(z_measure_template(parent_q));
1929        let child_template = bloq.add_template(BloqTemplate::new(child_circuit));
1930        let mut parent = BloqNode::from_members(vec![]);
1931        parent.expect_quantum_mut().instances = single_instance(0, parent_template);
1932        let mut child = BloqNode::from_members(vec![]);
1933        child.expect_quantum_mut().instances = single_instance(1, child_template);
1934        child.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1935            parity: DetectorParity::from_measurements([
1936                instance_measurement(1, m0),
1937                instance_measurement(1, m1),
1938            ]),
1939            coords: None,
1940        }];
1941        let parent = bloq.add_node(parent);
1942        let child = bloq.add_node(child);
1943        bloq.add_edge(parent, child, BloqEdge::quantum(vec![]));
1944
1945        let slices = program_detector_slices(&bloq).unwrap();
1946        assert!(slices.breaks.is_empty());
1947        assert!(
1948            slices.per_node[&NodeRef::top_level(parent)]
1949                .0
1950                .iter()
1951                .all(Vec::is_empty),
1952            "the child-local control must not retain the parent's tape-global id"
1953        );
1954        let child_slices = &slices.per_node[&NodeRef::top_level(child)];
1955        assert_eq!(
1956            child_slices.0.len(),
1957            2,
1958            "feedforward adds no visible moment"
1959        );
1960        assert_eq!(
1961            child_slices.0[0],
1962            vec![RegionView {
1963                region: 0,
1964                terms: vec![RegionTerm {
1965                    qubit: q1,
1966                    pauli: PauliBasis::Z,
1967                }],
1968            }]
1969        );
1970        assert!(child_slices.0[1].is_empty());
1971    }
1972
1973    #[test]
1974    fn same_tick_measurement_keeps_its_place_before_a_rotation() {
1975        let q = ivec2(0, 0);
1976        let mut circuit = CoordCircuit::new();
1977        circuit.do_gate(GateType::RZ, [q]).unwrap();
1978        let measurement = circuit.measure(PauliBasis::Z, [q])[0];
1979        circuit.do_gate(GateType::H, [q]).unwrap();
1980        let ops = circuit.body(circuit.entry_body()).unwrap().ops();
1981        assert_eq!(
1982            moment_segments(ops)
1983                .into_iter()
1984                .flat_map(|segment| segment.ops)
1985                .collect::<Vec<_>>(),
1986            ops
1987        );
1988        let mut bloq = Bloq::new();
1989        let template = bloq.add_template(BloqTemplate::new(circuit));
1990        let mut node = BloqNode::from_members(Vec::new());
1991        node.expect_quantum_mut().instances = single_instance(0, template);
1992        node.expect_quantum_mut()
1993            .detectors
1994            .push(crate::NodeDetector {
1995                parity: DetectorParity::from_measurements([instance_measurement(0, measurement)]),
1996                coords: None,
1997            });
1998        bloq.add_node(node);
1999
2000        let slices = program_detector_slices(&bloq).unwrap();
2001
2002        assert!(slices.breaks.is_empty(), "RZ; M is deterministic before H");
2003    }
2004
2005    #[test]
2006    fn measurement_controlled_pauli_stays_before_a_following_reset() {
2007        let control = ivec2(0, 0);
2008        let target = ivec2(1, 0);
2009        let spectator = ivec2(2, 0);
2010        let mut circuit = CoordCircuit::new();
2011        circuit.do_gate(GateType::RX, [control]).unwrap();
2012        let m0 = circuit.measure(PauliBasis::Z, [control])[0];
2013        circuit.tick();
2014        circuit.measure(PauliBasis::Z, [spectator]);
2015        circuit
2016            .body_mut(circuit.entry_body())
2017            .unwrap()
2018            .ops_mut()
2019            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2020                pauli: PauliBasis::X,
2021                control: m0,
2022                target,
2023            }]));
2024        circuit.do_gate(GateType::RZ, [target]).unwrap();
2025        circuit.tick();
2026        let final_measurement = circuit.measure(PauliBasis::Z, [target])[0];
2027
2028        let ops = circuit
2029            .body(circuit.entry_body())
2030            .expect("entry body")
2031            .ops();
2032        let moments = moment_segments(ops);
2033        assert!(matches!(
2034            moments[2].ops.as_slice(),
2035            [Op::Measure { .. }, Op::ConditionalPauli(_)]
2036        ));
2037        assert!(matches!(
2038            moments[3].ops.as_slice(),
2039            [Op::Gate {
2040                gate: GateType::RZ,
2041                ..
2042            }]
2043        ));
2044
2045        let mut bloq = Bloq::new();
2046        let template = bloq.add_template(BloqTemplate::new(circuit));
2047        let mut node = BloqNode::from_members(vec![]);
2048        node.expect_quantum_mut().instances = single_instance(0, template);
2049        node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2050            parity: DetectorParity::from_measurements([instance_measurement(0, final_measurement)]),
2051            coords: None,
2052        }];
2053        let node = bloq.add_node(node);
2054
2055        let slices = program_detector_slices(&bloq).unwrap();
2056        assert!(slices.breaks.is_empty());
2057        let timeline = &slices.per_node[&NodeRef::top_level(node)].0;
2058        assert_eq!(timeline.len(), 5, "feedforward adds no visible moment");
2059        assert!(timeline[..3].iter().all(Vec::is_empty));
2060        assert_eq!(
2061            timeline[3][0].terms,
2062            vec![RegionTerm {
2063                qubit: target,
2064                pauli: PauliBasis::Z,
2065            }],
2066            "the reset erases the earlier correction before it can depend on m0"
2067        );
2068    }
2069
2070    #[test]
2071    fn same_tick_measurement_control_stays_before_a_following_reset() {
2072        let control = ivec2(0, 0);
2073        let target = ivec2(1, 0);
2074        let mut circuit = CoordCircuit::new();
2075        circuit.do_gate(GateType::RX, [control]).unwrap();
2076        circuit.tick();
2077        let m0 = circuit.measure(PauliBasis::Z, [control])[0];
2078        circuit
2079            .body_mut(circuit.entry_body())
2080            .unwrap()
2081            .ops_mut()
2082            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2083                pauli: PauliBasis::X,
2084                control: m0,
2085                target,
2086            }]));
2087        circuit.do_gate(GateType::RZ, [target]).unwrap();
2088        circuit.tick();
2089        let final_measurement = circuit.measure(PauliBasis::Z, [target])[0];
2090
2091        let ops = circuit
2092            .body(circuit.entry_body())
2093            .expect("entry body")
2094            .ops();
2095        let moments = moment_segments(ops);
2096        assert!(matches!(
2097            moments[1].ops.as_slice(),
2098            [Op::Measure { .. }, Op::ConditionalPauli(_)]
2099        ));
2100        assert!(matches!(
2101            moments[2].ops.as_slice(),
2102            [Op::Gate {
2103                gate: GateType::RZ,
2104                ..
2105            }]
2106        ));
2107
2108        let mut bloq = Bloq::new();
2109        let template = bloq.add_template(BloqTemplate::new(circuit));
2110        let mut node = BloqNode::from_members(vec![]);
2111        node.expect_quantum_mut().instances = single_instance(0, template);
2112        node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2113            parity: DetectorParity::from_measurements([instance_measurement(0, final_measurement)]),
2114            coords: None,
2115        }];
2116        let node = bloq.add_node(node);
2117
2118        let slices = program_detector_slices(&bloq).unwrap();
2119        assert!(slices.breaks.is_empty());
2120        let timeline = &slices.per_node[&NodeRef::top_level(node)].0;
2121        assert_eq!(timeline.len(), 4, "feedforward adds no visible moment");
2122        assert!(timeline[..2].iter().all(Vec::is_empty));
2123        assert_eq!(
2124            timeline[2][0].terms,
2125            vec![RegionTerm {
2126                qubit: target,
2127                pauli: PauliBasis::Z,
2128            }],
2129            "the reset erases the correction before it can depend on m0"
2130        );
2131    }
2132
2133    #[test]
2134    fn record_backed_observable_tracks_as_logical_region() {
2135        let q = ivec2(0, 0);
2136        let mut circuit = CoordCircuit::new();
2137        circuit.do_gate(GateType::Z, [q]).unwrap();
2138        circuit.measure(PauliBasis::Z, [q]);
2139
2140        let mut bloq = Bloq::new();
2141        let template = bloq.add_template(BloqTemplate::new(circuit));
2142        let mut node = BloqNode::from_members(vec![]);
2143        node.expect_quantum_mut().instances = single_instance(0, template);
2144        let quantum = bloq.add_node(node);
2145        let accumulate = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2146            index: None,
2147            operators: Vec::new(),
2148            measurements: vec![instance_measurement(0, 0)],
2149        }));
2150        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(5)));
2151        bloq.add_edge(quantum, accumulate, BloqEdge::Order);
2152        bloq.add_edge(accumulate, observable, BloqEdge::compose(0));
2153
2154        let slices = program_detector_slices(&bloq).unwrap();
2155        let timeline = &slices.per_node[&NodeRef::top_level(quantum)];
2156
2157        assert_eq!(timeline.0.len(), 2);
2158        assert_eq!(timeline.0[0].len(), 1);
2159        assert_eq!(
2160            timeline.0[0][0],
2161            RegionView {
2162                region: 0,
2163                terms: vec![RegionTerm {
2164                    qubit: q,
2165                    pauli: PauliBasis::Z,
2166                }],
2167            }
2168        );
2169        assert!(timeline.0[1].is_empty());
2170        assert!(slices.breaks.is_empty());
2171    }
2172
2173    #[test]
2174    fn boundary_observable_seeds_input_and_output_at_node_faces() {
2175        let q = ivec2(0, 0);
2176        let mut circuit = CoordCircuit::new();
2177        circuit.do_gate(GateType::H, [q]).unwrap();
2178
2179        let mut bloq = Bloq::new();
2180        let template = bloq.add_template(BloqTemplate::new(circuit));
2181        let mut node = BloqNode::from_members(vec![]);
2182        node.expect_quantum_mut().instances = single_instance(0, template);
2183        let quantum = bloq.add_node(node);
2184        let include = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2185            index: None,
2186            measurements: Vec::new(),
2187            operators: vec![
2188                InstanceBoundaryOperator {
2189                    instance: TemplateInstanceId(0),
2190                    face: BoundaryFace::Input,
2191                    operator: PauliMap::from_unique_entries([(q, Pauli::Z)]),
2192                },
2193                InstanceBoundaryOperator {
2194                    instance: TemplateInstanceId(0),
2195                    face: BoundaryFace::Output,
2196                    operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2197                },
2198            ],
2199        }));
2200        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(8)));
2201        bloq.add_edge(include, observable, BloqEdge::compose(0));
2202
2203        let slices = program_detector_slices(&bloq).unwrap();
2204        let timeline = &slices.per_node[&NodeRef::top_level(quantum)];
2205
2206        assert_eq!(timeline.0.len(), 1);
2207        assert_eq!(timeline.0[0].len(), 1);
2208        assert_eq!(
2209            slices.regions[timeline.0[0][0].region as usize].id,
2210            ProgramRegionId::Observable { index: 8 }
2211        );
2212        assert_eq!(
2213            timeline.0[0][0].terms,
2214            vec![RegionTerm {
2215                qubit: q,
2216                pauli: PauliBasis::X,
2217            }]
2218        );
2219        assert!(
2220            slices.breaks.is_empty(),
2221            "input Z cancels output X propagated backward through H"
2222        );
2223    }
2224
2225    #[test]
2226    fn boundary_seed_on_zero_moment_instance_returns_typed_error() {
2227        let q = ivec2(0, 0);
2228        let mut bloq = Bloq::new();
2229        let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2230        let mut node = BloqNode::from_members(vec![]);
2231        node.expect_quantum_mut().instances = single_instance(0, template);
2232        bloq.add_node(node);
2233        let include = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2234            index: None,
2235            measurements: Vec::new(),
2236            operators: vec![InstanceBoundaryOperator {
2237                instance: TemplateInstanceId(0),
2238                face: BoundaryFace::Output,
2239                operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2240            }],
2241        }));
2242        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(4)));
2243        bloq.add_edge(include, observable, BloqEdge::compose(0));
2244
2245        assert!(matches!(
2246            program_detector_slices(&bloq),
2247            Err(ProgramSliceError::BreakOutsideTimeline {
2248                region: ProgramRegionId::Observable { index: 4 }
2249            })
2250        ));
2251    }
2252
2253    #[test]
2254    fn region_exported_observable_content_follows_selected_body() {
2255        let q = ivec2(0, 0);
2256        let mut circuit = CoordCircuit::new();
2257        circuit.do_gate(GateType::RZ, [q]).unwrap();
2258        circuit.tick();
2259        circuit.measure(PauliBasis::X, [q]);
2260        circuit.tick();
2261        circuit.do_gate(GateType::Z, [q]).unwrap();
2262
2263        let mut bloq = Bloq::new();
2264        let template = bloq.add_template(BloqTemplate::new(circuit));
2265        let mut body = SubGraph::new();
2266        let mut node = BloqNode::from_members(vec![]);
2267        node.expect_quantum_mut().instances = single_instance(0, template);
2268        let quantum = body.add_node(node);
2269        let accumulate = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2270            index: None,
2271            operators: Vec::new(),
2272            measurements: vec![instance_measurement(0, 0)],
2273        }));
2274        body.add_edge(quantum, accumulate, BloqEdge::Order);
2275        let binding = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2276            index: None,
2277            measurements: Vec::new(),
2278            operators: vec![InstanceBoundaryOperator {
2279                instance: TemplateInstanceId(0),
2280                face: BoundaryFace::Output,
2281                operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2282            }],
2283        }));
2284        let unrelated = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2285            expr: ClassicalExpr::And(Box::new([
2286                ClassicalExpr::Const(true),
2287                ClassicalExpr::Const(false),
2288            ])),
2289        }));
2290        // Reading this bit internally must not consume its exported boundary.
2291        // The boundary-only slice must not inspect the unused nonlinear bit.
2292        let consume = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2293            expr: ClassicalExpr::In(0),
2294        }));
2295        body.add_edge(unrelated, consume, BloqEdge::value(0));
2296        body.set_value_output(Some(accumulate.into()));
2297        body.set_boundary_outputs(vec![binding]);
2298
2299        let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2300            restart_source: None,
2301            restart_condition: ClassicalExpr::Const(false),
2302            body,
2303        }));
2304        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(9)));
2305        bloq.add_edge(region, observable, BloqEdge::compose(0));
2306
2307        let slices = program_detector_slices(&bloq).unwrap();
2308        assert_eq!(slices.skipped_cross_tape, 0);
2309        assert!(slices.breaks.is_empty());
2310
2311        let body_ref = NodeRef {
2312            path: LevelPath::default().child(region, BodySelector::Body),
2313            node: quantum,
2314        };
2315        let timeline = &slices.per_node[&body_ref];
2316        assert_eq!(timeline.0.len(), 3);
2317        assert_eq!(
2318            timeline.0[2],
2319            vec![RegionView {
2320                region: 0,
2321                terms: vec![RegionTerm {
2322                    qubit: q,
2323                    pauli: PauliBasis::X,
2324                }],
2325            }]
2326        );
2327    }
2328
2329    /// A looped template: flatten unrolls the repeat, and each iteration's
2330    /// round-to-round detector is a distinct region on the unrolled timeline.
2331    #[test]
2332    fn repeat_program_yields_distinct_per_iteration_regions() {
2333        let q = ivec2(0, 0);
2334        let mut circuit = CoordCircuit::new();
2335        let seed = circuit.measure(PauliBasis::Z, [q])[0];
2336        let repeated = circuit.reserve_measurement_id(q);
2337        // A tick before each repeated measurement separates the rounds into
2338        // their own moments once unrolled.
2339        let body = circuit.add_body(CircuitBody::from_ops(vec![
2340            Op::Tick,
2341            Op::Measure {
2342                basis: PauliBasis::Z,
2343                qubits: vec![q],
2344                measurements: vec![repeated],
2345                flip_probability: 0.0,
2346            },
2347        ]));
2348        circuit
2349            .body_mut(circuit.entry_body())
2350            .unwrap()
2351            .ops_mut()
2352            .push(Op::Repeat {
2353                body,
2354                repetitions: 3,
2355            });
2356        let state = LoopStateId(0);
2357        let template = BloqTemplate::with_parts(
2358            circuit,
2359            vec![TemplateDetector {
2360                scope: TemplateDetectorScope::RepeatBody { body },
2361                parity: DetectorParity::from_terms([
2362                    DetectorTerm::Measurement(repeated),
2363                    DetectorTerm::LoopState(state),
2364                ]),
2365                coords: Some(DetectorCoords::from_slice(&[1.0, 2.0])),
2366            }],
2367            vec![TemplateRepeatState {
2368                body,
2369                state,
2370                initial: DetectorParity::from_measurements([seed]),
2371                next: DetectorParity::from_measurements([repeated]),
2372            }],
2373            Vec::new(),
2374            Vec::new(),
2375        );
2376
2377        let mut bloq = Bloq::new();
2378        let tpl = bloq.add_template(template);
2379        let mut node = BloqNode::from_members(vec![]);
2380        node.expect_quantum_mut().instances = single_instance(0, tpl);
2381        let node_id = bloq.add_node(node);
2382
2383        let slices = program_detector_slices(&bloq).unwrap();
2384        assert_eq!(slices.skipped_cross_tape, 0);
2385        assert!(slices.breaks.is_empty());
2386
2387        let node_slices = &slices.per_node[&NodeRef::top_level(node_id)];
2388        // seed + three rounds = four measurement moments.
2389        assert_eq!(node_slices.0.len(), 4);
2390        let expected = vec![RegionTerm {
2391            qubit: q,
2392            pauli: PauliBasis::Z,
2393        }];
2394        // Moments 0..3 each carry exactly one region, and the three iteration
2395        // detectors are distinct (indices 0, 1, 2 in the flattened table).
2396        for moment in 0..3 {
2397            let regions = &node_slices.0[moment];
2398            assert_eq!(regions.len(), 1, "moment {moment}");
2399            assert_eq!(
2400                slices.regions[regions[0].region as usize].id,
2401                ProgramRegionId::Detector {
2402                    owner: NodeRef::top_level(node_id),
2403                    detector: moment as u32,
2404                }
2405            );
2406            assert_eq!(regions[0].terms, expected);
2407            assert_eq!(
2408                slices.regions[regions[0].region as usize]
2409                    .coords
2410                    .as_ref()
2411                    .map(|coords| coords.as_slice()),
2412                Some(&[1.0, 2.0][..])
2413            );
2414        }
2415        assert!(
2416            node_slices.0[3].is_empty(),
2417            "regions closed after last round"
2418        );
2419    }
2420
2421    /// The executed body of a region inlines into the main tape, so a seam
2422    /// detector spanning a top-level node and a region-body node resolves and
2423    /// its region spans both timelines (the point of the executed-path model).
2424    #[test]
2425    fn region_body_seam_detector_resolves_across_boundary() {
2426        let q = ivec2(0, 0);
2427        // Body node keeps the qubit alive through a Pauli `Z` (a tracker no-op)
2428        // so the seam region survives into the body's first moment.
2429        let mut body_circuit = CoordCircuit::new();
2430        body_circuit.do_gate(GateType::Z, [q]).unwrap();
2431        body_circuit.measure(PauliBasis::Z, [q]);
2432
2433        let mut bloq = Bloq::new();
2434        let top_tpl = bloq.add_template(z_measure_template(q));
2435        let body_tpl = bloq.add_template(BloqTemplate::new(body_circuit));
2436
2437        let mut top_node = BloqNode::from_members(vec![]);
2438        top_node.expect_quantum_mut().instances = single_instance(0, top_tpl);
2439
2440        let mut body = SubGraph::new();
2441        let mut body_node = BloqNode::from_members(vec![]);
2442        body_node.expect_quantum_mut().instances = single_instance(1, body_tpl);
2443        // Seam detector: top-level instance 0 meets body instance 1.
2444        body_node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2445            parity: DetectorParity::from_measurements([
2446                instance_measurement(0, 0),
2447                instance_measurement(1, 0),
2448            ]),
2449            coords: Some(DetectorCoords::from_slice(&[5.0, 6.0])),
2450        }];
2451        let body_node_id = body.add_node(body_node);
2452
2453        let region = BloqNode::region(RegionNode::RepeatUntilSuccess {
2454            restart_source: None,
2455            restart_condition: ClassicalExpr::Const(false),
2456            body,
2457        });
2458        let top_id = bloq.add_node(top_node);
2459        let region_id = bloq.add_node(region);
2460        // Order the top-level measurement before the region body.
2461        bloq.add_edge(top_id, region_id, BloqEdge::quantum(vec![]));
2462
2463        let slices = program_detector_slices(&bloq).unwrap();
2464        assert_eq!(slices.skipped_cross_tape, 0, "seam detector now resolves");
2465        assert!(slices.breaks.is_empty());
2466
2467        let top_ref = NodeRef::top_level(top_id);
2468        let body_ref = NodeRef {
2469            path: LevelPath::default().child(region_id, BodySelector::Body),
2470            node: body_node_id,
2471        };
2472        let top_slices = &slices.per_node[&top_ref];
2473        let body_slices = &slices.per_node[&body_ref];
2474        assert_eq!(top_slices.0.len(), 1, "top: one measurement moment");
2475        assert_eq!(body_slices.0.len(), 2, "body: rotation then measurement");
2476
2477        let expected = vec![RegionTerm {
2478            qubit: q,
2479            pauli: PauliBasis::Z,
2480        }];
2481        // Region alive at the top's measurement and the body's pre-measure
2482        // moment, spanning the seam; gone after the body measures.
2483        assert_eq!(top_slices.0[0].len(), 1);
2484        assert_eq!(body_slices.0[0].len(), 1);
2485        assert!(body_slices.0[1].is_empty());
2486
2487        let region_view = &top_slices.0[0][0];
2488        assert_eq!(
2489            slices.regions[region_view.region as usize].id,
2490            ProgramRegionId::Detector {
2491                owner: body_ref,
2492                detector: 0,
2493            },
2494            "body's side table owns it"
2495        );
2496        assert_eq!(
2497            slices.regions[region_view.region as usize]
2498                .coords
2499                .as_ref()
2500                .map(|coords| coords.as_slice()),
2501            Some(&[5.0, 6.0][..])
2502        );
2503        assert_eq!(region_view.terms, expected);
2504        assert_eq!(body_slices.0[0][0].terms, expected);
2505    }
2506}