Skip to main content

bloq_stim/
emit.rs

1use std::collections::BTreeMap;
2
3use bloq_utils::Pauli;
4use glam::IVec2;
5use itertools::Itertools;
6use rustc_hash::FxHashMap;
7
8use bloq_circuit::{
9    BodyId, CircuitError, ConditionalCorrection, CoordCircuit, DetectorParity, DetectorTerm,
10    GateType, LoopCarriedDetectorState, LoopStateId, MeasurementFrameError, Op, PauliBasis,
11    PauliMap, measurement_gate_name,
12};
13use thiserror::Error;
14
15use crate::layout::QubitLayout;
16use crate::measurement_frame::MeasurementFrame;
17use crate::text_utils::{push_int, push_number_prefer_int};
18
19/// Reason the static Stim backend could not emit a program.
20#[derive(Debug, Error, Clone, PartialEq, Eq)]
21pub enum StimEmissionError {
22    /// The input program failed Bloq IR well-formedness validation.
23    #[error("invalid Bloq IR: {0}")]
24    InvalidProgram(#[from] bloq_ir::BloqValidationError),
25    /// A shared detector use has an invalid bundle or owner binding.
26    #[error("{0}")]
27    DetectorBundle(#[from] bloq_ir::DetectorBundleError),
28    /// A node annotation names a measurement outside the prepared owner registry.
29    #[error("node annotation names unknown measurement {0:?}")]
30    UnknownInstanceMeasurement(bloq_ir::lowering::InstanceMeasurement),
31    /// Node annotations have no instance-local loop-state frame.
32    #[error("node annotation references unsupported loop state {0:?}")]
33    NodeLoopStateUnsupported(LoopStateId),
34    /// A gate with no Stim spelling on the static (Clifford) path, e.g. a
35    /// non-Clifford `T`.
36    #[error("unsupported Stim backend gate: {0}")]
37    UnsupportedGate(GateType),
38    /// A node the static backend cannot emit (a runtime-control region or
39    /// conditional activation), rejected by name before any emission work.
40    #[error("unsupported Stim backend node: {0}")]
41    UnsupportedNode(&'static str),
42    /// Alignment is noiseless.
43    #[error("moment-aligned Stim emission does not support noise")]
44    AlignMomentsWithNoise,
45    /// Alignment cannot preserve per-node segments.
46    #[error("moment-aligned Stim emission does not support segmented output")]
47    AlignMomentsWithSegments,
48    /// An empty circuit has no aligned slot for metadata.
49    #[error("moment-aligned Stim emission cannot place metadata for an empty quantum circuit")]
50    AlignMomentsWithEmptyMetadata,
51    /// Moment-alignment planning failed.
52    #[error("{0}")]
53    MomentAlignment(#[from] bloq_ir::MomentAlignmentError),
54    /// The graph violates an invariant the emit walk relies on (a template,
55    /// instance, or producer a node names is missing, or a measurement is
56    /// read before it is emitted). Well-formed programs from the compiler
57    /// never trigger this; it guards hand-built or unvalidated graphs.
58    #[error("malformed graph: {0}")]
59    MalformedGraph(&'static str),
60    /// A node's template instances failed to merge into one emission circuit.
61    /// Validation runs the same merge, so a validated Bloq never triggers
62    /// this; it guards hand-built or unvalidated graphs.
63    #[error("{0}")]
64    InstanceMerge(#[from] bloq_ir::lowering::NodeTemplateInstanceMergeError),
65    /// Flattening failed on a straight-line emission path.
66    #[error("failed to flatten Stim emission: {0}")]
67    Flatten(#[from] bloq_ir::FlattenError),
68    /// The uniform physical error probability is not finite or outside `[0, 1]`.
69    #[error("physical error probability must be finite and in [0, 1]")]
70    InvalidNoiseProbability,
71    /// A backend index cannot be represented in its unsigned 32-bit id space.
72    #[error("Stim {space} index {index} exceeds the u32 index range")]
73    IndexOutOfRange {
74        /// Index space being populated.
75        space: &'static str,
76        /// Index that cannot be represented.
77        index: usize,
78    },
79    /// A qubit target exceeds Stim's 24-bit target payload.
80    #[error("Stim qubit index {index} exceeds the target limit {max}")]
81    QubitIndexOutOfRange {
82        /// Qubit index that cannot be represented.
83        index: usize,
84        /// Largest qubit index supported by the target payload.
85        max: u32,
86    },
87    /// A template instance offset moves a circuit qubit outside the global
88    /// `i32` coordinate lattice.
89    #[error("{0}")]
90    CoordinateOverflow(#[from] bloq_ir::CoordinateOverflowError),
91    /// The circuit layer rejected an instruction while emitting a node's
92    /// circuit (e.g. a measurement id outside the emitted range).
93    #[error("{0}")]
94    Circuit(#[from] CircuitError),
95    /// A classical value has no affine record recipe on the static path.
96    #[error("{0}")]
97    ClassicalResolution(#[from] bloq_ir::ResolveError),
98    /// The graph contains a cycle, so it has no topological emit order.
99    /// Validated programs are acyclic; this guards hand-built or unvalidated
100    /// graphs.
101    #[error("{0}")]
102    CyclicGraph(#[from] bloq_ir::CycleDetected),
103}
104
105#[derive(Debug, Clone, Default)]
106pub(crate) struct StimCircuitAnnotations {
107    pub detectors: Vec<StimDetector>,
108    pub repeat_states: Vec<StimRepeatState>,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub(crate) struct StimDetector {
113    pub scope: StimAnnotationScope,
114    pub parity: DetectorParity,
115    pub coords: Option<bloq_circuit::DetectorCoords>,
116    pub postselection: bool,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub(crate) struct StimRepeatState {
121    pub body: BodyId,
122    pub state: LoopStateId,
123    pub initial: DetectorParity,
124    pub next: DetectorParity,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub(crate) enum StimAnnotationScope {
129    TopLevel,
130    RepeatBody { body: BodyId },
131}
132
133#[derive(Debug, Clone, Copy)]
134pub(crate) struct StimEmitOptions {
135    pub enable_clifford_proxy: bool,
136    pub allow_non_clifford: bool,
137    pub emit_postselection_tags: bool,
138    pub emit_qubit_coords: bool,
139    pub qubit_offset: IVec2,
140}
141
142impl Default for StimEmitOptions {
143    fn default() -> Self {
144        Self {
145            enable_clifford_proxy: false,
146            allow_non_clifford: false,
147            emit_postselection_tags: true,
148            emit_qubit_coords: true,
149            qubit_offset: IVec2::ZERO,
150        }
151    }
152}
153
154pub(crate) struct StimEmitContext<'a> {
155    pub layout: &'a QubitLayout,
156    pub frame: &'a mut MeasurementFrame,
157    pub resolved_loop_detector_states: &'a mut ResolvedLoopStates,
158    pub output: &'a mut String,
159    pub indent: usize,
160}
161
162/// Finished loop states mapped to their resolved absolute measurement parity.
163pub(crate) type ResolvedLoopStates = FxHashMap<LoopStateId, Vec<u32>>;
164
165/// One active `REPEAT` block's resolved detector states, keyed by loop state
166/// id. Ordered by id so finish-time scoping can walk the frame and the loop's
167/// state list together in ascending id order.
168type LoopStateFrame = BTreeMap<LoopStateId, Vec<u32>>;
169
170/// Append ` rec[<lookback>]`, a measurement-record target.
171fn push_lookback(output: &mut String, lookback: i32) {
172    output.push_str(" rec[");
173    push_int(output, lookback);
174    output.push(']');
175}
176
177pub(crate) trait StimMeasurementMapper {
178    /// Return the same mapped id for repeated calls with the same measurement.
179    ///
180    /// Stim repeat preservation probes future iterations before committing to
181    /// the output, so mapping may allocate lazily but must be idempotent.
182    fn map_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError>;
183
184    fn map_annotation_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError> {
185        self.map_measurement(measurement)
186    }
187}
188
189/// Passes measurement ids through unchanged. Used by the standalone
190/// coordinate-circuit paths (the `verify` feature and tests); the program
191/// emitter supplies its own instance-aware mappers instead.
192#[cfg(any(test, feature = "verify"))]
193#[derive(Debug, Clone, Copy, Default)]
194pub(crate) struct IdentityMeasurementMapper;
195
196#[cfg(any(test, feature = "verify"))]
197impl StimMeasurementMapper for IdentityMeasurementMapper {
198    fn map_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError> {
199        Ok(measurement)
200    }
201}
202
203/// Emits `circuit` on its own — fresh layout, fresh measurement frame, ids
204/// passed through unchanged. Shared by the `verify` feature and the emitter's
205/// own tests; the program emitter drives [`emit_stim_circuit`] directly with an
206/// instance-aware mapper instead.
207#[cfg(any(test, feature = "verify"))]
208pub(crate) fn emit_standalone_stim(
209    circuit: &CoordCircuit,
210    annotations: Option<&StimCircuitAnnotations>,
211    layout: &QubitLayout,
212    enable_clifford_proxy: bool,
213) -> Result<(String, MeasurementFrame), CircuitError> {
214    let mut output = String::new();
215    let mut frame = MeasurementFrame::default();
216    let mut resolved_loop_detector_states = ResolvedLoopStates::default();
217    let mut ctx = StimEmitContext {
218        layout,
219        frame: &mut frame,
220        resolved_loop_detector_states: &mut resolved_loop_detector_states,
221        output: &mut output,
222        indent: 0,
223    };
224    emit_stim_circuit(
225        circuit,
226        annotations,
227        &mut ctx,
228        &mut IdentityMeasurementMapper,
229        StimEmitOptions {
230            enable_clifford_proxy,
231            emit_qubit_coords: true,
232            ..StimEmitOptions::default()
233        },
234    )?;
235    Ok((output, frame))
236}
237
238pub(crate) fn emit_stim_circuit<M: StimMeasurementMapper>(
239    circuit: &CoordCircuit,
240    annotations: Option<&StimCircuitAnnotations>,
241    ctx: &mut StimEmitContext<'_>,
242    mapper: &mut M,
243    options: StimEmitOptions,
244) -> Result<bool, CircuitError> {
245    let base_indent = ctx.indent;
246    let mut emitter = StimTextEmitter {
247        source: circuit,
248        annotations,
249        ctx,
250        mapper,
251        options,
252        detector_state_stack: Vec::new(),
253        base_indent,
254        needs_node_boundary_tick: false,
255    };
256    if emitter.options.emit_qubit_coords {
257        emitter.emit_qubit_coords();
258    }
259    emitter.emit_body(circuit.entry_body())?;
260    emitter.emit_side_table_annotations(circuit.entry_body())?;
261    Ok(emitter.needs_node_boundary_tick)
262}
263
264struct StimTextEmitter<'circuit, 'ctx, 'data, 'mapper, M>
265where
266    M: StimMeasurementMapper,
267{
268    source: &'circuit CoordCircuit,
269    annotations: Option<&'circuit StimCircuitAnnotations>,
270    ctx: &'ctx mut StimEmitContext<'data>,
271    mapper: &'mapper mut M,
272    options: StimEmitOptions,
273    detector_state_stack: Vec<LoopStateFrame>,
274    /// The indent level [`emit_stim_circuit`] started at.
275    base_indent: usize,
276    needs_node_boundary_tick: bool,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280enum StimTarget {
281    Rec(i32),
282    Pauli { pauli: Pauli, qubit: u32 },
283}
284
285impl<M> StimTextEmitter<'_, '_, '_, '_, M>
286where
287    M: StimMeasurementMapper,
288{
289    fn note_instruction(&mut self, needs_node_boundary_tick: bool) {
290        if self.ctx.indent == self.base_indent {
291            self.needs_node_boundary_tick = needs_node_boundary_tick;
292        }
293    }
294
295    fn emit_qubit_coords(&mut self) {
296        // A qubit-coords line is a top-level instruction, so a following node
297        // needs a boundary tick. The flag is idempotent, so setting it once for
298        // the whole (non-empty) preamble matches the old per-line calls.
299        if self.ctx.layout.coord_index().is_empty() {
300            return;
301        }
302        self.note_instruction(true);
303        let coords = self
304            .ctx
305            .layout
306            .coord_index()
307            .iter()
308            .map(|(coord, qubit)| (*coord, *qubit))
309            .sorted_by_key(|&(_, qubit)| qubit);
310        emit_qubit_coords_lines(self.ctx.output, coords);
311    }
312
313    fn emit_body(&mut self, body: BodyId) -> Result<(), CircuitError> {
314        let Some(source) = self.source.body(body) else {
315            return Err(CircuitError::InvalidCircuitBody(body));
316        };
317        for op in source.ops() {
318            self.emit_op(op)?;
319        }
320        Ok(())
321    }
322
323    fn emit_side_table_annotations(&mut self, body: BodyId) -> Result<(), CircuitError> {
324        let Some(annotations) = self.annotations else {
325            return Ok(());
326        };
327        for detector in &annotations.detectors {
328            if detector.scope == scope_for_body(self.source, body) {
329                self.emit_detector_annotation(detector)?;
330            }
331        }
332        Ok(())
333    }
334
335    fn emit_op(&mut self, op: &Op) -> Result<(), CircuitError> {
336        match op {
337            Op::Gate { gate, qubits } => self.emit_gate(*gate, qubits),
338            Op::Measure {
339                basis,
340                qubits,
341                measurements,
342                flip_probability,
343            } => {
344                let records = self.map_measurements(measurements)?;
345                self.emit_measure(*basis, qubits, &records, *flip_probability)
346            }
347            Op::Tick => {
348                self.note_instruction(false);
349                self.write_line("TICK");
350                Ok(())
351            }
352            Op::MPP {
353                products,
354                measurements,
355            } => {
356                let records = self.map_measurements(measurements)?;
357                self.emit_mpp(products, &records)
358            }
359            Op::Repeat { body, repetitions } => self.emit_repeat(*body, *repetitions),
360            Op::ConditionalPauli(corrections) => self.emit_conditional_paulis(corrections),
361            Op::Depolarize1 {
362                probability,
363                qubits,
364            } => self.emit_noise("DEPOLARIZE1", *probability, qubits),
365            Op::Depolarize2 {
366                probability,
367                qubits,
368            } => self.emit_noise("DEPOLARIZE2", *probability, qubits),
369            Op::PauliError {
370                probability,
371                pauli,
372                qubits,
373            } => self.emit_noise(pauli_error_gate_name(*pauli), *probability, qubits),
374        }
375    }
376
377    fn emit_conditional_paulis(
378        &mut self,
379        corrections: &[ConditionalCorrection],
380    ) -> Result<(), CircuitError> {
381        for correction in corrections {
382            let measurement = self.mapper.map_measurement(correction.control)?;
383            let lookback = self.ctx.frame.resolve_measurement(measurement)?;
384            let coord = correction.target + self.options.qubit_offset;
385            let Some(qubit) = self.ctx.layout.get(coord) else {
386                return Err(CircuitError::QubitNotFoundInLayout(coord));
387            };
388
389            self.write_instruction_prefix(conditional_pauli_gate_name(correction.pauli), None);
390            push_lookback(self.ctx.output, lookback);
391            self.ctx.output.push(' ');
392            self.ctx.output.push_str(self.ctx.layout.label(qubit));
393            self.ctx.output.push('\n');
394        }
395        Ok(())
396    }
397
398    /// Map a body op's raw measurement ids to their backend ids, in order.
399    fn map_measurements(&mut self, measurements: &[u32]) -> Result<Vec<u32>, CircuitError> {
400        measurements
401            .iter()
402            .map(|&measurement| self.mapper.map_measurement(measurement))
403            .collect()
404    }
405
406    /// Emit one `MPP X0*X1 Z2*Z3` line: each product's non-identity Pauli targets
407    /// are joined by `*` combiners, products separated by a space. Each product
408    /// produced one measurement record, recorded into the frame like a measurement.
409    fn emit_mpp(&mut self, products: &[PauliMap], records: &[u32]) -> Result<(), CircuitError> {
410        self.write_instruction_prefix("MPP", None);
411        for product in products {
412            let mut first = true;
413            for (coord, pauli) in product
414                .iter()
415                .filter(|&(_, &pauli)| pauli != Pauli::I)
416                .sorted_by_key(|(coord, _)| {
417                    let coord = **coord + self.options.qubit_offset;
418                    self.ctx.layout.get(coord).unwrap_or(0)
419                })
420            {
421                let coord = *coord + self.options.qubit_offset;
422                let Some(qubit) = self.ctx.layout.get(coord) else {
423                    return Err(CircuitError::QubitNotFoundInLayout(coord));
424                };
425                self.ctx.output.push(if first { ' ' } else { '*' });
426                first = false;
427                self.ctx.output.push(match pauli {
428                    Pauli::X => 'X',
429                    Pauli::Y => 'Y',
430                    Pauli::Z => 'Z',
431                    Pauli::I => unreachable!("identity Paulis are filtered out above"),
432                });
433                self.ctx.output.push_str(self.ctx.layout.label(qubit));
434            }
435        }
436        self.ctx.output.push('\n');
437        for &record in records {
438            self.ctx.frame.record_emitted(record)?;
439        }
440        Ok(())
441    }
442
443    fn emit_repeat(&mut self, body: BodyId, repetitions: u32) -> Result<(), CircuitError> {
444        if repetitions == 0 {
445            return Ok(());
446        }
447
448        let side_table_detector_states = self.repeat_detector_states(body);
449        let detector_states = side_table_detector_states.as_slice();
450        let stack_depth = self.detector_state_stack.len();
451        let initial_states = self.resolve_initial_detector_states(detector_states)?;
452        self.detector_state_stack.push(initial_states);
453
454        // A non-jumpable loop (see `can_jump_loop_detector_states`) never
455        // probes for a stable body, so it unrolls all `repetitions` iterations
456        // — required for correctness, not a missed optimization.
457        let can_jump = can_jump_loop_detector_states(detector_states);
458        let mut stable_body = None;
459        let mut remaining = repetitions;
460        while remaining > 1 {
461            if can_jump
462                && let Some(rendered) = self.stable_repeat_body_render(body, detector_states)?
463            {
464                stable_body = Some(rendered);
465                break;
466            }
467            self.emit_repeat_iteration(body, detector_states)?;
468            remaining -= 1;
469        }
470
471        if remaining == 1 {
472            self.emit_repeat_iteration(body, detector_states)?;
473            self.finish_loop_detector_states(stack_depth, detector_states)?;
474            return Ok(());
475        }
476
477        // The while loop above only leaves `remaining > 1` via the stable-body
478        // break, so a summarized body is always available here.
479        let rendered_body =
480            stable_body.expect("remaining > 1 only survives the loop via the stable-body break");
481        self.note_instruction(false);
482        self.ctx.output.push_str(indent_prefix(self.ctx.indent));
483        self.ctx.output.push_str("REPEAT ");
484        push_int(self.ctx.output, remaining);
485        self.ctx.output.push_str(" {\n");
486        self.ctx.output.push_str(&rendered_body);
487        self.apply_rendered_repeat_body_effects(body, remaining - 1, detector_states)?;
488        self.write_line("}");
489        self.finish_loop_detector_states(stack_depth, detector_states)?;
490        Ok(())
491    }
492
493    fn stable_repeat_body_render(
494        &mut self,
495        body: BodyId,
496        detector_states: &[LoopCarriedDetectorState],
497    ) -> Result<Option<String>, CircuitError> {
498        let checkpoint = self.ctx.frame.checkpoint();
499        // Render the body twice from the same starting state; the loop is
500        // stable once two consecutive iterations produce identical text. The
501        // frame and loop-state stack are moved out so the sub-emitter can
502        // borrow them, then rolled back and restored afterwards.
503        let mut state_stack = self.detector_state_stack.clone();
504        let mut frame = std::mem::take(self.ctx.frame);
505        let mut first = String::new();
506        let mut second = String::new();
507        let result = self
508            .render_repeat_iteration_for_stability(
509                body,
510                detector_states,
511                &mut frame,
512                &mut state_stack,
513                &mut first,
514            )
515            .and_then(|()| {
516                self.render_repeat_iteration_for_stability(
517                    body,
518                    detector_states,
519                    &mut frame,
520                    &mut state_stack,
521                    &mut second,
522                )
523            })
524            .map(|()| (first == second).then_some(first));
525        frame.rollback_to(checkpoint);
526        *self.ctx.frame = frame;
527        result
528    }
529
530    fn apply_rendered_repeat_body_effects(
531        &mut self,
532        body: BodyId,
533        tail_repetitions: u32,
534        detector_states: &[LoopCarriedDetectorState],
535    ) -> Result<(), CircuitError> {
536        let (offsets, body_measurements) =
537            repeat_body_measurement_offsets(self.source, body, self.mapper)?;
538        self.record_repeat_body_measurements(body_measurements, 1, offsets.iter().copied())?;
539        self.update_loop_detector_states(detector_states)?;
540        self.record_repeat_body_measurements(body_measurements, tail_repetitions, offsets)?;
541        self.update_loop_detector_states(detector_states)
542    }
543
544    fn record_repeat_body_measurements(
545        &mut self,
546        body_measurements: u32,
547        repetitions: u32,
548        offsets: impl IntoIterator<Item = (u32, u32)>,
549    ) -> Result<(), CircuitError> {
550        if body_measurements == 0 || repetitions == 0 {
551            return Ok(());
552        }
553        self.ctx
554            .frame
555            .record_repeated_measurements(body_measurements, repetitions, offsets)
556    }
557
558    fn render_repeat_iteration_for_stability(
559        &mut self,
560        body: BodyId,
561        detector_states: &[LoopCarriedDetectorState],
562        frame: &mut MeasurementFrame,
563        detector_state_stack: &mut Vec<LoopStateFrame>,
564        output: &mut String,
565    ) -> Result<(), CircuitError> {
566        debug_assert!(!detector_state_stack.is_empty());
567        output.clear();
568        let mut ctx = StimEmitContext {
569            layout: self.ctx.layout,
570            frame,
571            resolved_loop_detector_states: self.ctx.resolved_loop_detector_states,
572            output,
573            indent: self.ctx.indent + 1,
574        };
575        // The probe shares the loop-state stack (moved in and back out) but
576        // renders with its own throwaway working buffers (including its own
577        // last-instruction tracking, discarded with the probe).
578        let mut emitter = StimTextEmitter {
579            source: self.source,
580            annotations: self.annotations,
581            ctx: &mut ctx,
582            mapper: self.mapper,
583            options: self.options,
584            detector_state_stack: std::mem::take(detector_state_stack),
585            base_indent: self.base_indent,
586            needs_node_boundary_tick: false,
587        };
588        let result = emitter.emit_repeat_iteration(body, detector_states);
589        *detector_state_stack = std::mem::take(&mut emitter.detector_state_stack);
590        result
591    }
592
593    fn emit_repeat_iteration(
594        &mut self,
595        body: BodyId,
596        detector_states: &[LoopCarriedDetectorState],
597    ) -> Result<(), CircuitError> {
598        self.emit_body(body)?;
599        self.emit_side_table_annotations(body)?;
600        self.update_loop_detector_states(detector_states)
601    }
602
603    fn emit_detector_annotation(&mut self, detector: &StimDetector) -> Result<(), CircuitError> {
604        let lookbacks = self.resolve_detector_lookbacks(&detector.parity)?;
605        let targets: Vec<StimTarget> = lookbacks.into_iter().map(StimTarget::Rec).collect();
606        self.write_targets_line(
607            if detector.postselection && self.options.emit_postselection_tags {
608                "DETECTOR[POST-SELECTION]"
609            } else {
610                "DETECTOR"
611            },
612            detector.coords.as_deref(),
613            &targets,
614        );
615        Ok(())
616    }
617
618    fn repeat_detector_states(&self, body: BodyId) -> Vec<LoopCarriedDetectorState> {
619        let Some(annotations) = self.annotations else {
620            return Vec::new();
621        };
622        annotations
623            .repeat_states
624            .iter()
625            .filter(|state| state.body == body)
626            .map(|state| LoopCarriedDetectorState {
627                state: state.state,
628                initial: state.initial.clone(),
629                next: state.next.clone(),
630            })
631            .collect()
632    }
633
634    fn emit_gate(&mut self, gate: GateType, qubits: &[IVec2]) -> Result<(), CircuitError> {
635        let gate = if self.options.enable_clifford_proxy {
636            gate.clifford_proxy()
637        } else {
638            gate
639        };
640        debug_assert!(
641            self.options.allow_non_clifford || !gate.is_non_clifford(),
642            "unsupported Stim gate should be preflighted"
643        );
644        self.write_qubit_instruction_line(gate.into(), None, qubits)?;
645        Ok(())
646    }
647
648    fn emit_measure(
649        &mut self,
650        basis: PauliBasis,
651        qubits: &[IVec2],
652        records: &[u32],
653        flip_probability: f64,
654    ) -> Result<(), CircuitError> {
655        let args = (flip_probability != 0.0).then_some(std::slice::from_ref(&flip_probability));
656        self.write_qubit_instruction_line(measurement_gate_name(basis), args, qubits)?;
657        for &record in records {
658            self.ctx.frame.record_emitted(record)?;
659        }
660        Ok(())
661    }
662
663    fn emit_noise(
664        &mut self,
665        name: &str,
666        probability: f64,
667        qubits: &[IVec2],
668    ) -> Result<(), CircuitError> {
669        self.write_qubit_instruction_line(name, Some(std::slice::from_ref(&probability)), qubits)?;
670        Ok(())
671    }
672
673    fn resolve_initial_detector_states(
674        &mut self,
675        detector_states: &[LoopCarriedDetectorState],
676    ) -> Result<LoopStateFrame, CircuitError> {
677        let mut resolved = LoopStateFrame::default();
678        for state in detector_states {
679            let parity = self.resolve_parity_absolute(&state.initial)?;
680            resolved.insert(state.state, parity);
681        }
682        Ok(resolved)
683    }
684
685    fn update_loop_detector_states(
686        &mut self,
687        detector_states: &[LoopCarriedDetectorState],
688    ) -> Result<(), CircuitError> {
689        if detector_states.is_empty() {
690            return Ok(());
691        }
692        // Identity-only updates leave every carried state unchanged, so the
693        // current frame already holds the right parities.
694        if detector_states.iter().all(is_identity_update) {
695            debug_assert!(detector_states.iter().all(|state| {
696                self.detector_state_stack
697                    .last()
698                    .is_some_and(|states| states.contains_key(&state.state))
699            }));
700            return Ok(());
701        }
702        let mut next = LoopStateFrame::new();
703        for state in detector_states {
704            let parity = if is_identity_update(state) {
705                let Some(current) = self
706                    .detector_state_stack
707                    .last()
708                    .and_then(|states| states.get(&state.state))
709                else {
710                    return Err(CircuitError::InvalidLoopDetectorState(state.state));
711                };
712                current.clone()
713            } else {
714                self.resolve_parity_absolute(&state.next)?
715            };
716            next.insert(state.state, parity);
717        }
718        let Some(current) = self.detector_state_stack.last_mut() else {
719            return Err(CircuitError::InvalidLoopDetectorState(
720                detector_states[0].state,
721            ));
722        };
723        *current = next;
724        Ok(())
725    }
726
727    fn finish_loop_detector_states(
728        &mut self,
729        stack_depth: usize,
730        detector_states: &[LoopCarriedDetectorState],
731    ) -> Result<(), CircuitError> {
732        if detector_states.is_empty() {
733            debug_assert_eq!(self.detector_state_stack.len(), stack_depth + 1);
734            self.detector_state_stack.pop();
735            return Ok(());
736        }
737        let Some(final_states) = self.detector_state_stack.pop() else {
738            return Err(CircuitError::InvalidLoopDetectorState(
739                detector_states[0].state,
740            ));
741        };
742        debug_assert_eq!(self.detector_state_stack.len(), stack_depth);
743        // Only this loop's listed states survive it; anything else in the
744        // frame (states a nested repeat handed back) is dropped here, like a
745        // lexical scope ending. Both the frame and `detector_states` are in
746        // ascending id order, so a two-pointer pass moves the listed entries.
747        debug_assert!(
748            detector_states
749                .windows(2)
750                .all(|pair| pair[0].state < pair[1].state),
751            "loop detector states are allocated in ascending id order"
752        );
753        let mut listed = detector_states.iter().map(|state| state.state).peekable();
754        let mut keep = |state: LoopStateId| {
755            while listed.next_if(|&id| id < state).is_some() {}
756            listed.next_if_eq(&state).is_some()
757        };
758        if let Some(parent) = self.detector_state_stack.last_mut() {
759            for (state, value) in final_states {
760                if keep(state) {
761                    parent.insert(state, value);
762                }
763            }
764        } else {
765            for (state, value) in final_states {
766                if keep(state) {
767                    self.ctx.resolved_loop_detector_states.insert(state, value);
768                }
769            }
770        }
771        Ok(())
772    }
773
774    fn resolve_parity_absolute(
775        &mut self,
776        parity: &DetectorParity,
777    ) -> Result<Vec<u32>, CircuitError> {
778        let mut absolutes = Vec::with_capacity(parity.terms().len());
779        for &term in parity.terms() {
780            match term {
781                DetectorTerm::Measurement(measurement) => {
782                    let measurement = self.mapper.map_annotation_measurement(measurement)?;
783                    absolutes.push(self.ctx.frame.resolve_absolute_measurement(measurement)?);
784                }
785                DetectorTerm::LoopState(state) => {
786                    let Some(resolved) = self
787                        .detector_state_stack
788                        .iter()
789                        .rev()
790                        .find_map(|states| states.get(&state))
791                        .or_else(|| self.ctx.resolved_loop_detector_states.get(&state))
792                    else {
793                        return Err(CircuitError::InvalidLoopDetectorState(state));
794                    };
795                    absolutes.extend(resolved.iter().copied());
796                }
797            }
798        }
799        canonicalize_parity(&mut absolutes);
800        Ok(absolutes)
801    }
802
803    fn resolve_detector_lookbacks(
804        &mut self,
805        parity: &DetectorParity,
806    ) -> Result<Vec<i32>, CircuitError> {
807        // Fast path: a pure measurement parity resolves each term to a lookback
808        // directly, skipping the absolute-id round trip. Duplicate lookbacks
809        // still XOR-cancel like the general path. Today no production input
810        // produces one here — `DetectorParity` canonicalizes at construction
811        // and re-canonicalizes after id remaps, and distinct backend ids
812        // resolve to distinct records — so this guards a future mapper whose
813        // `map_annotation_measurement` merges ids at emit time.
814        if parity
815            .terms()
816            .iter()
817            .all(|term| matches!(term, DetectorTerm::Measurement(_)))
818        {
819            let mut lookbacks = Vec::with_capacity(parity.terms().len());
820            for &term in parity.terms() {
821                let DetectorTerm::Measurement(measurement) = term else {
822                    unreachable!("the all-Measurement guard admits no LoopState terms")
823                };
824                let measurement = self.mapper.map_annotation_measurement(measurement)?;
825                lookbacks.push(self.ctx.frame.resolve_measurement(measurement)?);
826            }
827            lookbacks.sort_unstable();
828            canonicalize_parity(&mut lookbacks);
829            return Ok(lookbacks);
830        }
831
832        // General path: resolve terms to absolute ids, cancel duplicates, then
833        // convert back to lookbacks.
834        let absolutes = self.resolve_parity_absolute(parity)?;
835        let mut lookbacks = absolutes
836            .iter()
837            .map(|&abs| self.ctx.frame.lookback_to_absolute(abs))
838            .collect::<Result<Vec<_>, CircuitError>>()?;
839        lookbacks.sort_unstable();
840        Ok(lookbacks)
841    }
842
843    fn write_line(&mut self, line: &str) {
844        self.ctx.output.push_str(indent_prefix(self.ctx.indent));
845        self.ctx.output.push_str(line);
846        self.ctx.output.push('\n');
847    }
848
849    fn write_qubit_instruction_line(
850        &mut self,
851        gate: &str,
852        args: Option<&[f64]>,
853        qubits: &[IVec2],
854    ) -> Result<(), CircuitError> {
855        self.write_instruction_prefix(gate, args);
856        for &coord in qubits {
857            let coord = coord + self.options.qubit_offset;
858            let Some(qubit) = self.ctx.layout.get(coord) else {
859                return Err(CircuitError::QubitNotFoundInLayout(coord));
860            };
861            self.ctx.output.push(' ');
862            self.ctx.output.push_str(self.ctx.layout.label(qubit));
863        }
864        self.ctx.output.push('\n');
865        Ok(())
866    }
867
868    fn write_targets_line(&mut self, gate: &str, args: Option<&[f64]>, targets: &[StimTarget]) {
869        self.write_instruction_prefix(gate, args);
870        for target in targets {
871            target.write_to(self.ctx.output);
872        }
873        self.ctx.output.push('\n');
874    }
875
876    fn write_instruction_prefix(&mut self, gate: &str, args: Option<&[f64]>) {
877        self.note_instruction(true);
878        self.ctx.output.push_str(indent_prefix(self.ctx.indent));
879        self.ctx.output.push_str(gate);
880        if let Some(args) = args
881            && !args.is_empty()
882        {
883            self.ctx.output.push('(');
884            for (index, arg) in args.iter().enumerate() {
885                if index > 0 {
886                    self.ctx.output.push_str(", ");
887                }
888                push_number_prefer_int(self.ctx.output, *arg);
889            }
890            self.ctx.output.push(')');
891        }
892    }
893}
894
895impl StimTarget {
896    /// Append this target with a leading space.
897    fn write_to(self, output: &mut String) {
898        match self {
899            StimTarget::Rec(lookback) => push_lookback(output, lookback),
900            StimTarget::Pauli { pauli, qubit } => {
901                output.push(' ');
902                output.push(match pauli {
903                    Pauli::X => 'X',
904                    Pauli::Y => 'Y',
905                    Pauli::Z => 'Z',
906                    // Identity is filtered before a `StimTarget::Pauli` is built
907                    // (`append_pauli_map_stim_targets`), matching `emit_mpp`.
908                    Pauli::I => {
909                        unreachable!("identity Paulis are filtered before StimTarget::Pauli")
910                    }
911                });
912                push_int(output, qubit);
913            }
914        }
915    }
916}
917
918pub(crate) fn conditional_pauli_gate_name(pauli: PauliBasis) -> &'static str {
919    match pauli {
920        PauliBasis::X => "CX",
921        PauliBasis::Y => "CY",
922        PauliBasis::Z => "CZ",
923    }
924}
925
926pub(crate) fn pauli_error_gate_name(pauli: PauliBasis) -> &'static str {
927    match pauli {
928        PauliBasis::X => "X_ERROR",
929        PauliBasis::Y => "Y_ERROR",
930        PauliBasis::Z => "Z_ERROR",
931    }
932}
933
934/// Emit one `DETECTOR` line from pre-resolved measurement-record lookbacks,
935/// appended directly to `output`. The verify path's counterpart to the
936/// program emitter's annotation lines, sharing the `rec[...]` target
937/// formatting so the two cannot drift.
938#[cfg(feature = "verify")]
939pub(crate) fn emit_detector_records(output: &mut String, lookbacks: &[i32]) {
940    output.push_str("DETECTOR");
941    for &lookback in lookbacks {
942        push_lookback(output, lookback);
943    }
944    output.push('\n');
945}
946
947/// Append `QUBIT_COORDS(x, y) index` preamble lines for `coords`, already
948/// ordered by qubit index. Both the program emitter and the standalone circuit
949/// emitter route through this so the coordinate spelling stays byte-identical.
950/// Coordinates are integer grid positions, so they format as `i32` directly
951/// instead of through the float path. Ordering is the caller's job: a layout
952/// map has no inherent order and must be sorted before it reaches here.
953pub(crate) fn emit_qubit_coords_lines(
954    output: &mut String,
955    coords: impl IntoIterator<Item = (IVec2, u32)>,
956) {
957    for (coord, qubit_index) in coords {
958        output.push_str("QUBIT_COORDS(");
959        push_int(output, coord.x);
960        output.push_str(", ");
961        push_int(output, coord.y);
962        output.push_str(") ");
963        push_int(output, qubit_index);
964        output.push('\n');
965    }
966}
967
968/// Emit one `OBSERVABLE_INCLUDE(index)` line from measurement-record lookbacks
969/// the resolved measurement contribution of an [`Observable`](bloq_ir::ClassicalNode::Observable).
970/// Lookbacks are pre-resolved against the global frame (they resolve
971/// by absolute position, so the line is correct wherever the `Observable` node
972/// lands in emit order, as long as it follows its measurements). An empty set
973/// carries nothing, so no line is written.
974pub(crate) fn emit_observable_include_records(output: &mut String, index: u32, lookbacks: &[i32]) {
975    if lookbacks.is_empty() {
976        return;
977    }
978    output.push_str("OBSERVABLE_INCLUDE(");
979    push_int(output, index);
980    output.push(')');
981    for &lookback in lookbacks {
982        StimTarget::Rec(lookback).write_to(output);
983    }
984    output.push('\n');
985}
986
987/// Emit one `OBSERVABLE_INCLUDE(index)` line carrying Pauli targets only — no
988/// measurement records — appended directly to `output`.
989///
990/// Unlike record-only standalone observable nodes, a boundary logical operator's
991/// Pauli targets read the qubit state *at the point of emission*, so the backend
992/// positions these lines at temporal seams and open boundaries. An operator with
993/// no non-identity support (e.g. a seam XOR that cancelled to empty) writes
994/// nothing.
995pub(crate) fn emit_observable_include_pauli_targets(
996    output: &mut String,
997    index: u32,
998    operator: &PauliMap,
999    layout: &QubitLayout,
1000) -> Result<(), CircuitError> {
1001    let mut targets = Vec::new();
1002    append_pauli_map_stim_targets(operator, layout, &mut targets)?;
1003    if targets.is_empty() {
1004        return Ok(());
1005    }
1006    output.push_str("OBSERVABLE_INCLUDE(");
1007    push_int(output, index);
1008    output.push(')');
1009    for target in &targets {
1010        target.write_to(output);
1011    }
1012    output.push('\n');
1013    Ok(())
1014}
1015
1016fn append_pauli_map_stim_targets(
1017    pauli_map: &PauliMap,
1018    layout: &QubitLayout,
1019    targets: &mut Vec<StimTarget>,
1020) -> Result<(), CircuitError> {
1021    for (coord, pauli) in pauli_map
1022        .iter()
1023        .filter(|&(_, &pauli)| pauli != Pauli::I)
1024        .sorted_by_key(|(coord, _)| layout.get(**coord).unwrap_or(0))
1025    {
1026        let Some(qubit) = layout.get(*coord) else {
1027            return Err(CircuitError::QubitNotFoundInLayout(*coord));
1028        };
1029        targets.push(StimTarget::Pauli {
1030            pauli: *pauli,
1031            qubit,
1032        });
1033    }
1034    Ok(())
1035}
1036
1037/// Whether a loop's carried states allow summarizing the tail repetitions into
1038/// a `REPEAT` block instead of unrolling them.
1039///
1040/// Summarization records only the *final* iteration's measurement occurrences,
1041/// so it is sound exactly when every carried state's post-loop value is
1042/// reconstructible from that final frame: a cleared state, an identity update,
1043/// or a pure-measurement `next` parity (see [`loop_state_update_kind`]). A
1044/// `next` that references loop states (its own, or a nested loop's) can fold a
1045/// *different* absolute measurement set into the state on every iteration —
1046/// those intermediate occurrences are unrecoverable from a summarized frame —
1047/// so such loops must fully unroll, whatever their repetition count. Compiled
1048/// templates never mint one (loop recurrences lower to pure-measurement `next`
1049/// parities via the flow engine), so only hand-built graphs pay the unroll.
1050fn can_jump_loop_detector_states(detector_states: &[LoopCarriedDetectorState]) -> bool {
1051    detector_states
1052        .iter()
1053        .all(|state| loop_state_update_kind(state).is_some())
1054}
1055
1056/// Sort a parity's resolved terms and XOR-cancel duplicates: a value appearing
1057/// an even number of times contributes nothing and is dropped. Shared by the
1058/// absolute-id and lookback representations (equal terms resolve to equal
1059/// values in both).
1060fn canonicalize_parity<T: Ord + Copy>(values: &mut Vec<T>) {
1061    if values.len() < 2 {
1062        return;
1063    }
1064    if values.windows(2).all(|pair| pair[0] < pair[1]) {
1065        return;
1066    }
1067
1068    values.sort_unstable();
1069    let mut write = 0;
1070    let mut read = 0;
1071    while read < values.len() {
1072        let value = values[read];
1073        let mut count = 1;
1074        read += 1;
1075        while read < values.len() && values[read] == value {
1076            count += 1;
1077            read += 1;
1078        }
1079        if count % 2 == 1 {
1080            values[write] = value;
1081            write += 1;
1082        }
1083    }
1084    values.truncate(write);
1085}
1086
1087fn is_identity_update(state: &LoopCarriedDetectorState) -> bool {
1088    matches!(
1089        loop_state_update_kind(state),
1090        Some(LoopStateUpdate::Identity)
1091    )
1092}
1093
1094fn loop_state_update_kind(state: &LoopCarriedDetectorState) -> Option<LoopStateUpdate> {
1095    match state.next.terms() {
1096        [] => Some(LoopStateUpdate::Clear),
1097        [DetectorTerm::LoopState(loop_state)] if *loop_state == state.state => {
1098            Some(LoopStateUpdate::Identity)
1099        }
1100        terms
1101            if terms
1102                .iter()
1103                .all(|term| !matches!(term, DetectorTerm::LoopState(_))) =>
1104        {
1105            Some(LoopStateUpdate::Measurements)
1106        }
1107        _ => None,
1108    }
1109}
1110
1111fn scope_for_body(circuit: &CoordCircuit, body: BodyId) -> StimAnnotationScope {
1112    if body == circuit.entry_body() {
1113        StimAnnotationScope::TopLevel
1114    } else {
1115        StimAnnotationScope::RepeatBody { body }
1116    }
1117}
1118
1119pub(crate) fn first_unsupported_gate(circuit: &CoordCircuit) -> Option<GateType> {
1120    let mut pending = vec![circuit.entry_body()];
1121    let mut seen = rustc_hash::FxHashSet::default();
1122    while let Some(body) = pending.pop() {
1123        if !seen.insert(body) {
1124            continue;
1125        }
1126        let Some(body) = circuit.body(body) else {
1127            continue;
1128        };
1129        for op in body.ops() {
1130            match op {
1131                Op::Gate { gate, .. } if gate.is_non_clifford() => return Some(*gate),
1132                Op::Repeat { body, repetitions } if *repetitions != 0 => pending.push(*body),
1133                _ => {}
1134            }
1135        }
1136    }
1137    None
1138}
1139
1140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1141enum LoopStateUpdate {
1142    Clear,
1143    Identity,
1144    Measurements,
1145}
1146
1147/// The measurement offsets of one loop-body iteration that matter for frame
1148/// accounting, plus the total number of measurement records the iteration
1149/// emits.
1150///
1151/// The frame only ever resolves a measurement's *latest* occurrence, so the
1152/// offset list carries one entry per occurrence that can be a site's last —
1153/// for a nested `REPEAT` that is its final iteration alone, keeping the
1154/// accounting O(body ops) instead of O(inner repetitions). The total count
1155/// still covers every nested repetition, so callers advance the frame by the
1156/// true number of records.
1157fn repeat_body_measurement_offsets(
1158    circuit: &CoordCircuit,
1159    body: BodyId,
1160    mapper: &mut impl StimMeasurementMapper,
1161) -> Result<(Vec<(u32, u32)>, u32), CircuitError> {
1162    let mut offsets = Vec::new();
1163    let mut body_measurements = 0;
1164    collect_repeat_body_measurement_offsets(
1165        circuit,
1166        body,
1167        mapper,
1168        &mut offsets,
1169        &mut body_measurements,
1170    )?;
1171    Ok((offsets, body_measurements))
1172}
1173
1174fn collect_repeat_body_measurement_offsets(
1175    circuit: &CoordCircuit,
1176    body: BodyId,
1177    mapper: &mut impl StimMeasurementMapper,
1178    offsets: &mut Vec<(u32, u32)>,
1179    next_offset: &mut u32,
1180) -> Result<(), CircuitError> {
1181    let Some(body) = circuit.body(body) else {
1182        return Err(CircuitError::InvalidCircuitBody(body));
1183    };
1184    for op in body.ops() {
1185        match op {
1186            Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1187                for &measurement in measurements {
1188                    offsets.push((mapper.map_measurement(measurement)?, *next_offset));
1189                    let Some(next) = next_offset.checked_add(1) else {
1190                        return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1191                    };
1192                    *next_offset = next;
1193                }
1194            }
1195            Op::Repeat {
1196                body, repetitions, ..
1197            } => {
1198                if *repetitions == 0 {
1199                    continue;
1200                }
1201                let mut nested_offsets = Vec::new();
1202                let mut nested_body_measurements = 0;
1203                collect_repeat_body_measurement_offsets(
1204                    circuit,
1205                    *body,
1206                    mapper,
1207                    &mut nested_offsets,
1208                    &mut nested_body_measurements,
1209                )?;
1210                if nested_body_measurements == 0 {
1211                    continue;
1212                }
1213                // Only the nested loop's final iteration can hold a
1214                // measurement's last occurrence, so record just that
1215                // iteration's offsets (closed-form) rather than expanding all
1216                // `repetitions` of them.
1217                let Some(last_iteration_offset) =
1218                    nested_body_measurements.checked_mul(*repetitions - 1)
1219                else {
1220                    return Err(MeasurementFrameError::RepeatedMeasurementCountOverflow {
1221                        measurements_per_iteration: nested_body_measurements,
1222                        repetitions: *repetitions,
1223                    }
1224                    .into());
1225                };
1226                for &(measurement, nested_offset) in &nested_offsets {
1227                    let Some(offset) = next_offset
1228                        .checked_add(last_iteration_offset)
1229                        .and_then(|offset| offset.checked_add(nested_offset))
1230                    else {
1231                        return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1232                    };
1233                    offsets.push((measurement, offset));
1234                }
1235                let Some(additional) = nested_body_measurements.checked_mul(*repetitions) else {
1236                    return Err(MeasurementFrameError::RepeatedMeasurementCountOverflow {
1237                        measurements_per_iteration: nested_body_measurements,
1238                        repetitions: *repetitions,
1239                    }
1240                    .into());
1241                };
1242                let Some(next) = next_offset.checked_add(additional) else {
1243                    return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1244                };
1245                *next_offset = next;
1246            }
1247            Op::Gate { .. }
1248            | Op::Tick
1249            | Op::ConditionalPauli(_)
1250            | Op::Depolarize1 { .. }
1251            | Op::Depolarize2 { .. }
1252            | Op::PauliError { .. } => {}
1253        }
1254    }
1255    Ok(())
1256}
1257
1258#[inline]
1259fn indent_prefix(level: usize) -> &'static str {
1260    const INDENT_TABLE: [&str; 9] = [
1261        "",
1262        "    ",
1263        "        ",
1264        "            ",
1265        "                ",
1266        "                    ",
1267        "                        ",
1268        "                            ",
1269        "                                ",
1270    ];
1271
1272    INDENT_TABLE[level.min(INDENT_TABLE.len() - 1)]
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277    use glam::ivec2;
1278
1279    use super::*;
1280    use bloq_circuit::CircuitBody;
1281
1282    /// Emit a standalone `CoordCircuit` to Stim text. A test convenience;
1283    /// production emission goes through [`emit_stim_circuit`] on the
1284    /// program-assembly path.
1285    fn emit_stim_text(
1286        circuit: &CoordCircuit,
1287        annotations: Option<&StimCircuitAnnotations>,
1288        layout: FxHashMap<IVec2, u32>,
1289        enable_clifford_proxy: bool,
1290    ) -> Result<String, StimEmissionError> {
1291        if !enable_clifford_proxy && let Some(gate) = first_unsupported_gate(circuit) {
1292            return Err(StimEmissionError::UnsupportedGate(gate));
1293        }
1294        let layout = QubitLayout::new(layout)?;
1295        let (output, _frame) =
1296            emit_standalone_stim(circuit, annotations, &layout, enable_clifford_proxy)?;
1297        Ok(output)
1298    }
1299
1300    #[test]
1301    fn to_stim_text_preserves_repeat_blocks() {
1302        let qubit = ivec2(0, 0);
1303        let mut circuit = CoordCircuit::new();
1304        let measurement = circuit.reserve_measurement_id(qubit);
1305        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1306            basis: PauliBasis::Z,
1307            qubits: vec![qubit],
1308            measurements: vec![measurement],
1309            flip_probability: 0.0,
1310        }]));
1311        circuit.push_repeat(body, 3);
1312
1313        let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1314
1315        // The repeat body stays a single `REPEAT 3 { ... }` block rather than
1316        // unrolling into three `M 0` lines.
1317        insta::assert_snapshot!(text, @"
1318        QUBIT_COORDS(0, 0) 0
1319        REPEAT 3 {
1320            M 0
1321        }
1322        ");
1323    }
1324
1325    #[test]
1326    fn emits_first_class_noise_instructions_and_measurement_flip() {
1327        let q0 = ivec2(0, 0);
1328        let q1 = ivec2(1, 0);
1329        let mut circuit = CoordCircuit::new();
1330        circuit
1331            .body_mut(circuit.entry_body())
1332            .unwrap()
1333            .ops_mut()
1334            .extend([
1335                Op::Depolarize1 {
1336                    probability: 0.1,
1337                    qubits: vec![q0],
1338                },
1339                Op::Depolarize2 {
1340                    probability: 0.2,
1341                    qubits: vec![q0, q1],
1342                },
1343                Op::PauliError {
1344                    probability: 0.3,
1345                    pauli: PauliBasis::Z,
1346                    qubits: vec![q1],
1347                },
1348                Op::Measure {
1349                    basis: PauliBasis::X,
1350                    qubits: vec![q0],
1351                    measurements: vec![0],
1352                    flip_probability: 0.4,
1353                },
1354            ]);
1355        circuit.register_measurement_id(0, q0);
1356
1357        let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1358
1359        assert!(text.contains("DEPOLARIZE1(0.1) 0"));
1360        assert!(text.contains("DEPOLARIZE2(0.2) 0 1"));
1361        assert!(text.contains("Z_ERROR(0.3) 1"));
1362        assert!(text.contains("MX(0.4) 0"));
1363    }
1364
1365    #[test]
1366    fn mpp_emits_pauli_product_targets_with_combiners() {
1367        use bloq_circuit::{Pauli, PauliMap};
1368        // Two stabilizer products: X(0,0)*X(2,0) and Z(0,0)*Z(0,2).
1369        let x_product =
1370            PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::X), (ivec2(2, 0), Pauli::X)]);
1371        let z_product =
1372            PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::Z), (ivec2(0, 2), Pauli::Z)]);
1373        let mut circuit = CoordCircuit::new();
1374        let ids = circuit
1375            .measure_pauli_products([x_product, z_product])
1376            .unwrap();
1377        assert_eq!(ids.len(), 2, "one record per product");
1378        assert_eq!(circuit.num_measurements(), 2);
1379
1380        let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1381        // Layout sorts coords (x,y): (0,0)=0, (0,2)=1, (2,0)=2; each product's
1382        // Paulis are joined with `*` combiners on one `MPP` line.
1383        insta::assert_snapshot!(text, @"
1384        QUBIT_COORDS(0, 0) 0
1385        QUBIT_COORDS(0, 2) 1
1386        QUBIT_COORDS(2, 0) 2
1387        MPP X0*X2 Z0*Z1
1388        ");
1389    }
1390
1391    #[test]
1392    fn conditional_pauli_emits_record_controlled_xyz() {
1393        use bloq_circuit::{ConditionalCorrection, PauliBasis};
1394        let control_qubit = ivec2(0, 0);
1395        let mut circuit = CoordCircuit::new();
1396        let control = circuit.measure(PauliBasis::Z, [control_qubit])[0];
1397        circuit
1398            .body_mut(circuit.entry_body())
1399            .unwrap()
1400            .ops_mut()
1401            .push(Op::ConditionalPauli(
1402                [PauliBasis::X, PauliBasis::Y, PauliBasis::Z]
1403                    .into_iter()
1404                    .enumerate()
1405                    .map(|(index, pauli)| ConditionalCorrection {
1406                        pauli,
1407                        control,
1408                        target: ivec2(index as i32 + 1, 0),
1409                    })
1410                    .collect(),
1411            ));
1412
1413        let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1414
1415        insta::assert_snapshot!(text, @"
1416        QUBIT_COORDS(0, 0) 0
1417        QUBIT_COORDS(1, 0) 1
1418        QUBIT_COORDS(2, 0) 2
1419        QUBIT_COORDS(3, 0) 3
1420        M 0
1421        CX rec[-1] 1
1422        CY rec[-1] 2
1423        CZ rec[-1] 3
1424        ");
1425        #[cfg(feature = "verify")]
1426        text.parse::<stim::Circuit>()
1427            .expect("record-controlled Pauli gates should parse as Stim");
1428    }
1429
1430    #[test]
1431    fn conditional_pauli_keeps_repeat_body_stable() {
1432        use bloq_circuit::ConditionalCorrection;
1433        let qubit = ivec2(0, 0);
1434        let mut circuit = CoordCircuit::new();
1435        let measurement = circuit.reserve_measurement_id(qubit);
1436        let body = circuit.add_body(CircuitBody::from_ops(vec![
1437            Op::Measure {
1438                basis: PauliBasis::Z,
1439                qubits: vec![qubit],
1440                measurements: vec![measurement],
1441                flip_probability: 0.0,
1442            },
1443            Op::ConditionalPauli(vec![ConditionalCorrection {
1444                pauli: PauliBasis::X,
1445                control: measurement,
1446                target: qubit,
1447            }]),
1448        ]));
1449        circuit.push_repeat(body, 3);
1450
1451        let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1452
1453        insta::assert_snapshot!(text, @"
1454        QUBIT_COORDS(0, 0) 0
1455        REPEAT 3 {
1456            M 0
1457            CX rec[-1] 0
1458        }
1459        ");
1460        #[cfg(feature = "verify")]
1461        text.parse::<stim::Circuit>()
1462            .expect("feed-forward inside REPEAT should parse as Stim");
1463    }
1464
1465    #[test]
1466    fn side_table_repeat_keeps_following_lookbacks_relative_to_final_iteration() {
1467        let qubit = ivec2(0, 0);
1468        let mut circuit = CoordCircuit::new();
1469        let seed = circuit.measure(PauliBasis::Z, [qubit])[0];
1470        let repeated = circuit.reserve_measurement_id(qubit);
1471        let state = LoopStateId(0);
1472        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1473            basis: PauliBasis::Z,
1474            qubits: vec![qubit],
1475            measurements: vec![repeated],
1476            flip_probability: 0.0,
1477        }]));
1478        circuit.push_repeat(body, 3);
1479        let annotations = StimCircuitAnnotations {
1480            detectors: vec![
1481                StimDetector {
1482                    scope: StimAnnotationScope::RepeatBody { body },
1483                    parity: DetectorParity::from_terms([
1484                        DetectorTerm::Measurement(repeated),
1485                        DetectorTerm::LoopState(state),
1486                    ]),
1487                    coords: None,
1488                    postselection: false,
1489                },
1490                StimDetector {
1491                    scope: StimAnnotationScope::TopLevel,
1492                    parity: DetectorParity::from_measurements([repeated]),
1493                    coords: None,
1494                    postselection: false,
1495                },
1496            ],
1497            repeat_states: vec![StimRepeatState {
1498                body,
1499                state,
1500                initial: DetectorParity::from_measurements([seed]),
1501                next: DetectorParity::from_measurements([repeated]),
1502            }],
1503        };
1504
1505        let text = emit_stim_text(
1506            &circuit,
1507            Some(&annotations),
1508            circuit.build_coord_to_index(),
1509            true,
1510        )
1511        .unwrap();
1512
1513        assert!(text.contains("REPEAT 3 {"), "{text}");
1514        assert!(text.ends_with("}\nDETECTOR rec[-1]\n"), "{text}");
1515    }
1516
1517    #[test]
1518    fn side_table_repeat_partially_unrolls_until_loop_state_lookbacks_are_stable() {
1519        let seed_qubit = ivec2(0, 0);
1520        let repeated_qubit = ivec2(1, 0);
1521        let mut circuit = CoordCircuit::new();
1522        let seed = circuit.measure(PauliBasis::Z, [seed_qubit])[0];
1523        let repeated = circuit.reserve_measurement_id(repeated_qubit);
1524        let state = LoopStateId(0);
1525        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1526            basis: PauliBasis::Z,
1527            qubits: vec![repeated_qubit],
1528            measurements: vec![repeated],
1529            flip_probability: 0.0,
1530        }]));
1531        circuit.measure(PauliBasis::Z, [seed_qubit]);
1532        circuit.push_repeat(body, 4);
1533        let annotations = StimCircuitAnnotations {
1534            detectors: vec![StimDetector {
1535                scope: StimAnnotationScope::RepeatBody { body },
1536                parity: DetectorParity::from_terms([
1537                    DetectorTerm::Measurement(repeated),
1538                    DetectorTerm::LoopState(state),
1539                ]),
1540                coords: Some(bloq_circuit::DetectorCoords::from_slice(&[1.0, 0.0, 1.0])),
1541                postselection: false,
1542            }],
1543            repeat_states: vec![StimRepeatState {
1544                body,
1545                state,
1546                initial: DetectorParity::from_measurements([seed]),
1547                next: DetectorParity::from_measurements([repeated]),
1548            }],
1549        };
1550
1551        let text = emit_stim_text(
1552            &circuit,
1553            Some(&annotations),
1554            circuit.build_coord_to_index(),
1555            true,
1556        )
1557        .unwrap();
1558
1559        assert!(
1560            text.contains("M 0\nM 0\nM 1\nDETECTOR(1, 0, 1) rec[-3] rec[-1]\nREPEAT 3 {"),
1561            "{text}"
1562        );
1563        assert!(
1564            text.contains("    DETECTOR(1, 0, 1) rec[-2] rec[-1]"),
1565            "{text}"
1566        );
1567    }
1568
1569    #[test]
1570    fn side_table_detector_cancels_loop_state_against_measurement() {
1571        let qubit = ivec2(0, 0);
1572        let mut circuit = CoordCircuit::new();
1573        let seed = circuit.measure(PauliBasis::Z, [qubit])[0];
1574        let repeated = circuit.reserve_measurement_id(qubit);
1575        let state = LoopStateId(0);
1576        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1577            basis: PauliBasis::Z,
1578            qubits: vec![qubit],
1579            measurements: vec![repeated],
1580            flip_probability: 0.0,
1581        }]));
1582        circuit.push_repeat(body, 1);
1583        let annotations = StimCircuitAnnotations {
1584            detectors: vec![StimDetector {
1585                scope: StimAnnotationScope::RepeatBody { body },
1586                parity: DetectorParity::from_terms([
1587                    DetectorTerm::Measurement(seed),
1588                    DetectorTerm::LoopState(state),
1589                ]),
1590                coords: None,
1591                postselection: false,
1592            }],
1593            repeat_states: vec![StimRepeatState {
1594                body,
1595                state,
1596                initial: DetectorParity::from_measurements([seed]),
1597                next: DetectorParity::from_measurements([repeated]),
1598            }],
1599        };
1600
1601        let text = emit_stim_text(
1602            &circuit,
1603            Some(&annotations),
1604            circuit.build_coord_to_index(),
1605            true,
1606        )
1607        .unwrap();
1608
1609        assert!(text.contains("M 0\nM 0\nDETECTOR\n"), "{text}");
1610    }
1611}