Skip to main content

bloq_stim/
plan.rs

1//! Lowering one node's [`NodeEmissionPlan`] to standalone Stim text.
2//!
3//! [`emit_bloq_stim`](crate::emit_bloq_stim) linearizes a whole program into
4//! one global qubit and measurement space. A caller assembling a circuit of
5//! its own — splicing a compiled T source into a larger sampling circuit,
6//! rendering one node against a *different* program's layout — needs the
7//! opposite: a single plan, addressed through a qubit map it chose, with the
8//! measurement columns reported back so its own indices line up.
9//!
10//! Two things separate this from the program emitter. `REPEAT` blocks are
11//! **unrolled**, because the columns this returns are absolute positions in
12//! the emitted record stream and a loop has no single position. And feedback
13//! Paulis are written as their X/Z factors rather than as `CY` (see
14//! [`emit_plan_stim`]).
15
16use std::collections::BTreeMap;
17
18use bloq_circuit::{
19    BodyId, CircuitError, ConditionalCorrection, CoordCircuit, GateType, MeasurementFrameError, Op,
20    Pauli, PauliBasis, PauliMap, measurement_gate_name,
21};
22use bloq_ir::lowering::{InstanceMeasurement, NodeEmissionPlan};
23use glam::IVec2;
24use rustc_hash::FxHashMap;
25
26use crate::dialect::{HONEST_T_TAG, StimDialect};
27use crate::emit::{StimEmissionError, conditional_pauli_gate_name, pauli_error_gate_name};
28use crate::layout::QubitLayout;
29use crate::measurement_frame::{MAX_STIM_RECORD_LOOKBACK, stim_record_lookback};
30use crate::text_utils::{push_int, push_number_prefer_int, write_stim_tag};
31
32/// How [`emit_plan_stim`] should render a plan.
33#[derive(Debug, Clone, Copy)]
34pub struct PlanStimOptions<'a> {
35    qubit_map: &'a FxHashMap<IVec2, u32>,
36    dialect: StimDialect,
37    tag: Option<&'a str>,
38}
39
40impl<'a> PlanStimOptions<'a> {
41    /// Render against `qubit_map`, in the default [`StimDialect`] and with no
42    /// tag.
43    ///
44    /// `qubit_map` addresses the emitted circuit: every qubit coordinate the
45    /// plan touches must appear in it, and the index it maps to is the qubit
46    /// number written out. Passing the caller's own global layout is what lets
47    /// several plans be emitted into one shared index space.
48    /// Sparse indices in `0..=16_777_215` are supported without allocating entries
49    /// for missing indices. Both dialects share Stim's 24-bit qubit targets.
50    pub fn new(qubit_map: &'a FxHashMap<IVec2, u32>) -> Self {
51        Self {
52            qubit_map,
53            dialect: StimDialect::default(),
54            tag: None,
55        }
56    }
57
58    /// Choose how non-Clifford `T` gates are spelled.
59    #[must_use]
60    pub fn with_dialect(mut self, dialect: StimDialect) -> Self {
61        self.dialect = dialect;
62        self
63    }
64
65    /// Tag every emitted instruction with `tag`, so the caller can find this
66    /// plan's slice of a larger circuit structurally instead of by position.
67    ///
68    /// The `HONEST_T` tag wins on the gates it marks: it is load-bearing for
69    /// the [`StimDialect`] round trip, whereas this tag is the caller's own
70    /// bookkeeping.
71    #[must_use]
72    pub fn with_tag(mut self, tag: &'a str) -> Self {
73        self.tag = Some(tag);
74        self
75    }
76}
77
78/// One lowered plan: its Stim text and where its measurements landed.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct PlanStim {
81    /// The plan's Stim text, with every `REPEAT` unrolled.
82    pub text: String,
83    /// The absolute column each of the plan's instance-space measurements
84    /// occupies in [`Self::text`]'s record stream.
85    ///
86    /// Relative to the start of this text: a caller splicing it at offset `n`
87    /// adds `n` to every column.
88    pub measurement_columns: BTreeMap<InstanceMeasurement, u32>,
89    /// How many measurement records [`Self::text`] emits — the offset a
90    /// following fragment starts at.
91    pub measurement_count: u32,
92}
93
94/// Lower one node's emission plan to standalone Stim text.
95///
96/// Every [`Op`] variant is handled, so a plan is never rejected for containing
97/// an operation this does not know about. Two conventions are worth naming
98/// because they are not the only defensible choices:
99///
100/// - **`REPEAT` is unrolled.** [`Self::measurement_columns`] are absolute
101///   positions, and a measurement inside a loop occupies a different one every
102///   iteration. Unrolling is what makes the map well defined; the cost is that
103///   a plan with a large repetition count produces proportionally large text.
104/// - **A feedback `Y` is written as `CX` then `CZ`, not `CY`.** The control is
105///   a classical record, so the phase difference between `Y` and `XZ` is
106///   constant across the whole branch and cannot be observed. This matches the
107///   convention the artifacts already in the field were built with, and
108///   deliberately differs from [`emit_bloq_stim`](crate::emit_bloq_stim),
109///   whose text is pinned by its own consumers.
110///
111/// [`Self::measurement_columns`]: PlanStim::measurement_columns
112///
113/// # Errors
114///
115/// - [`StimEmissionError::Circuit`] if a plan qubit is missing from the qubit
116///   map, a body reference dangles or recurses, or a record lookback leaves
117///   Stim's range.
118/// - [`StimEmissionError::QubitIndexOutOfRange`] if a caller-supplied qubit index
119///   exceeds `16_777_215`.
120/// - [`StimEmissionError::UnsupportedGate`] for a non-Clifford gate other than
121///   `T`/`T_DAG` in the [`StimDialect::Stim`] dialect, which Stim could not
122///   parse.
123/// - [`StimEmissionError::MalformedGraph`] if a feedback control is read
124///   before its measurement is emitted, or the plan names a measurement its
125///   own circuit never emits.
126///
127/// # Panics
128///
129/// In debug builds, panics if plan emission disagrees with the circuit column walk.
130pub fn emit_plan_stim(
131    plan: &NodeEmissionPlan,
132    options: &PlanStimOptions<'_>,
133) -> Result<PlanStim, StimEmissionError> {
134    let layout = QubitLayout::new(options.qubit_map.clone())?;
135    let mut emitter = PlanStimEmitter {
136        circuit: &plan.circuit,
137        layout: &layout,
138        dialect: options.dialect,
139        tag: options.tag,
140        text: String::new(),
141        columns: FxHashMap::default(),
142        count: 0,
143        stack: Vec::new(),
144    };
145    emitter.emit_body(plan.circuit.entry_body())?;
146
147    // The standalone column walk and this emitter unroll the same circuit by
148    // different routes (closed-form there, literal repetition here). They can
149    // only disagree if one of them is wrong, so cross-check them rather than
150    // let a divergence surface as a misaligned artifact.
151    debug_assert_eq!(
152        plan.circuit
153            .expanded_measurement_columns()
154            .map(|expanded| expanded.count()),
155        Ok(emitter.count),
156        "plan lowering and the column walk disagree on the record count",
157    );
158
159    let measurement_columns =
160        plan.measurements
161            .iter()
162            .map(|(&measurement, local)| {
163                let column = emitter.columns.get(local).copied().ok_or(
164                    StimEmissionError::MalformedGraph(
165                        "emission plan names a measurement its circuit never emits",
166                    ),
167                )?;
168                Ok((measurement, column))
169            })
170            .collect::<Result<BTreeMap<_, _>, StimEmissionError>>()?;
171
172    Ok(PlanStim {
173        text: emitter.text,
174        measurement_columns,
175        measurement_count: emitter.count,
176    })
177}
178
179struct PlanStimEmitter<'a> {
180    circuit: &'a CoordCircuit,
181    layout: &'a QubitLayout,
182    dialect: StimDialect,
183    tag: Option<&'a str>,
184    text: String,
185    /// Emitted column of each measurement id's latest occurrence so far —
186    /// what a feedback control resolves against, and what the finished map is
187    /// built from.
188    /// Unlike whole-program emission, standalone plans may retain sparse local
189    /// ids near u32::MAX. A dense MeasurementFrame would allocate by that id.
190    columns: FxHashMap<u32, u32>,
191    count: u32,
192    /// Bodies currently being unrolled, so a self-referential body is reported
193    /// instead of recursing forever.
194    stack: Vec<BodyId>,
195}
196
197impl PlanStimEmitter<'_> {
198    fn emit_body(&mut self, body: BodyId) -> Result<(), StimEmissionError> {
199        if self.stack.contains(&body) {
200            return Err(CircuitError::InvalidCircuitBody(body).into());
201        }
202        self.stack.push(body);
203        // Copied out so the op borrow is on the circuit, not on `self`.
204        let circuit = self.circuit;
205        let source = circuit
206            .body(body)
207            .ok_or(CircuitError::InvalidCircuitBody(body))?;
208        for op in source.ops() {
209            self.emit_op(op)?;
210        }
211        self.stack.pop();
212        Ok(())
213    }
214
215    fn emit_op(&mut self, op: &Op) -> Result<(), StimEmissionError> {
216        match op {
217            Op::Gate { gate, qubits } => self.emit_gate(*gate, qubits),
218            Op::Measure {
219                basis,
220                qubits,
221                measurements,
222                flip_probability,
223            } => {
224                self.record_measurements(measurements)?;
225                let arg = (*flip_probability != 0.0).then_some(*flip_probability);
226                self.write_qubit_line(measurement_gate_name(*basis), self.tag, arg, qubits)
227            }
228            Op::MPP {
229                products,
230                measurements,
231            } => {
232                self.record_measurements(measurements)?;
233                self.emit_mpp(products)
234            }
235            Op::ConditionalPauli(corrections) => self.emit_conditional_paulis(corrections),
236            Op::Depolarize1 {
237                probability,
238                qubits,
239            } => self.write_qubit_line("DEPOLARIZE1", self.tag, Some(*probability), qubits),
240            Op::Depolarize2 {
241                probability,
242                qubits,
243            } => self.write_qubit_line("DEPOLARIZE2", self.tag, Some(*probability), qubits),
244            Op::PauliError {
245                probability,
246                pauli,
247                qubits,
248            } => self.write_qubit_line(
249                pauli_error_gate_name(*pauli),
250                self.tag,
251                Some(*probability),
252                qubits,
253            ),
254            Op::Tick => self.write_qubit_line("TICK", self.tag, None, &[]),
255            Op::Repeat { body, repetitions } => {
256                for _ in 0..*repetitions {
257                    self.emit_body(*body)?;
258                }
259                Ok(())
260            }
261        }
262    }
263
264    fn emit_gate(&mut self, gate: GateType, qubits: &[IVec2]) -> Result<(), StimEmissionError> {
265        let (name, tag) = match (self.dialect, gate) {
266            (StimDialect::Stim, GateType::T) => ("S", Some(HONEST_T_TAG)),
267            (StimDialect::Stim, GateType::T_DAG) => ("S_DAG", Some(HONEST_T_TAG)),
268            // The other non-Cliffords have no Stim spelling at all — not even
269            // a tagged proxy — so emitting them would produce text Stim
270            // rejects. The Clifft dialect writes them literally.
271            (StimDialect::Stim, gate) if gate.is_non_clifford() => {
272                return Err(StimEmissionError::UnsupportedGate(gate));
273            }
274            (_, gate) => (gate.into(), self.tag),
275        };
276        self.write_qubit_line(name, tag, None, qubits)
277    }
278
279    /// One `MPP X0*X1 Z2*Z3` line: each product's Paulis joined by `*`,
280    /// products separated by a space, in the products' own coordinate order.
281    /// A plan with no Pauli targets writes nothing rather than a bare `MPP`,
282    /// which Stim rejects.
283    fn emit_mpp(&mut self, products: &[PauliMap]) -> Result<(), StimEmissionError> {
284        let mut targets = String::new();
285        for product in products {
286            let mut first = true;
287            for (coord, pauli) in product.iter().filter(|&(_, &pauli)| pauli != Pauli::I) {
288                let qubit = self.qubit(*coord)?;
289                targets.push(if first { ' ' } else { '*' });
290                first = false;
291                targets.push(match pauli {
292                    Pauli::X => 'X',
293                    Pauli::Y => 'Y',
294                    Pauli::Z => 'Z',
295                    Pauli::I => unreachable!("identity Paulis are filtered out above"),
296                });
297                targets.push_str(self.layout.label(qubit));
298            }
299        }
300        if targets.is_empty() {
301            return Ok(());
302        }
303        self.write_instruction_prefix("MPP", self.tag, None);
304        self.text.push_str(&targets);
305        self.text.push('\n');
306        Ok(())
307    }
308
309    fn emit_conditional_paulis(
310        &mut self,
311        corrections: &[ConditionalCorrection],
312    ) -> Result<(), StimEmissionError> {
313        for correction in corrections {
314            let lookback = self.lookback_to(correction.control)?;
315            let qubit = self.qubit(correction.target)?;
316            match correction.pauli {
317                // A record-controlled `Y` is written as its `X` and `Z`
318                // factors. The control is classical, so the phase separating
319                // `Y` from `XZ` is constant over the branch and unobservable —
320                // and the artifacts this reproduces were built this way.
321                PauliBasis::Y => {
322                    self.write_feedback("CX", lookback, qubit);
323                    self.write_feedback("CZ", lookback, qubit);
324                }
325                pauli => self.write_feedback(conditional_pauli_gate_name(pauli), lookback, qubit),
326            }
327        }
328        Ok(())
329    }
330
331    /// Claim the next columns for `measurements`, in order. A repeated
332    /// occurrence overwrites the earlier one, so a control or a plan lookup
333    /// always resolves to the latest — the rule the emission frame and
334    /// `CoordCircuit::flatten` both use.
335    fn record_measurements(&mut self, measurements: &[u32]) -> Result<(), StimEmissionError> {
336        for &measurement in measurements {
337            let next = self.count.checked_add(1).ok_or_else(|| {
338                CircuitError::from(MeasurementFrameError::EmittedMeasurementCountOverflow {
339                    emitted_count: self.count,
340                    additional: 1,
341                })
342            })?;
343            self.columns.insert(measurement, self.count);
344            self.count = next;
345        }
346        Ok(())
347    }
348
349    /// The `rec[-k]` offset from the current position back to `measurement`'s
350    /// latest occurrence.
351    fn lookback_to(&self, measurement: u32) -> Result<i32, StimEmissionError> {
352        let column =
353            self.columns
354                .get(&measurement)
355                .copied()
356                .ok_or(StimEmissionError::MalformedGraph(
357                    "conditional Pauli control is read before its measurement is emitted",
358                ))?;
359        let distance = self.count - column;
360        stim_record_lookback(distance).ok_or_else(|| {
361            CircuitError::from(MeasurementFrameError::RecordLookbackOutOfRange {
362                lookback: distance,
363                max: MAX_STIM_RECORD_LOOKBACK,
364            })
365            .into()
366        })
367    }
368
369    fn qubit(&self, coord: IVec2) -> Result<u32, StimEmissionError> {
370        self.layout
371            .get(coord)
372            .ok_or_else(|| CircuitError::QubitNotFoundInLayout(coord).into())
373    }
374
375    fn write_feedback(&mut self, gate: &str, lookback: i32, qubit: u32) {
376        self.write_instruction_prefix(gate, self.tag, None);
377        self.text.push_str(" rec[");
378        push_int(&mut self.text, lookback);
379        self.text.push_str("] ");
380        self.text.push_str(self.layout.label(qubit));
381        self.text.push('\n');
382    }
383
384    fn write_qubit_line(
385        &mut self,
386        gate: &str,
387        tag: Option<&str>,
388        arg: Option<f64>,
389        qubits: &[IVec2],
390    ) -> Result<(), StimEmissionError> {
391        // Resolved before any text is written so a missing qubit fails without
392        // leaving a half-finished line behind.
393        let indices = qubits
394            .iter()
395            .map(|&coord| self.qubit(coord))
396            .collect::<Result<Vec<_>, _>>()?;
397        self.write_instruction_prefix(gate, tag, arg);
398        for qubit in indices {
399            self.text.push(' ');
400            self.text.push_str(self.layout.label(qubit));
401        }
402        self.text.push('\n');
403        Ok(())
404    }
405
406    fn write_instruction_prefix(&mut self, gate: &str, tag: Option<&str>, arg: Option<f64>) {
407        self.text.push_str(gate);
408        if let Some(tag) = tag {
409            write_stim_tag(&mut self.text, tag);
410        }
411        if let Some(arg) = arg {
412            self.text.push('(');
413            push_number_prefer_int(&mut self.text, arg);
414            self.text.push(')');
415        }
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use bloq_circuit::CircuitBody;
422    use bloq_ir::lowering::TemplateInstanceId;
423    use glam::ivec2;
424
425    use super::*;
426
427    fn instance_measurement(measurement: u32) -> InstanceMeasurement {
428        InstanceMeasurement {
429            instance: TemplateInstanceId(0),
430            measurement,
431        }
432    }
433
434    fn plan_of(circuit: CoordCircuit, measurements: &[u32]) -> NodeEmissionPlan {
435        NodeEmissionPlan {
436            normalized_templates: None,
437            circuit,
438            measurements: measurements
439                .iter()
440                .map(|&id| (instance_measurement(id), id))
441                .collect(),
442            bodies: FxHashMap::default(),
443        }
444    }
445
446    fn layout_of(circuit: &CoordCircuit) -> FxHashMap<IVec2, u32> {
447        circuit.build_coord_to_index()
448    }
449
450    /// One plan exercising every [`Op`] variant, so the lowering's
451    /// exhaustiveness is a fact rather than a claim.
452    fn every_op_circuit() -> (CoordCircuit, Vec<u32>) {
453        let [a, b] = [ivec2(0, 0), ivec2(1, 0)];
454        let mut circuit = CoordCircuit::new();
455        circuit.do_gate(GateType::RZ, [a, b]).unwrap();
456        circuit.tick();
457        circuit.do_gate(GateType::CX, [a, b]).unwrap();
458        circuit
459            .body_mut(circuit.entry_body())
460            .unwrap()
461            .ops_mut()
462            .extend([
463                Op::Depolarize1 {
464                    probability: 0.001,
465                    qubits: vec![a],
466                },
467                Op::Depolarize2 {
468                    probability: 0.002,
469                    qubits: vec![a, b],
470                },
471                Op::PauliError {
472                    probability: 0.003,
473                    pauli: PauliBasis::Z,
474                    qubits: vec![b],
475                },
476            ]);
477        let mpp = circuit
478            .measure_pauli_products([PauliMap::from_unique_entries([
479                (a, Pauli::X),
480                (b, Pauli::X),
481            ])])
482            .unwrap();
483        let measured = circuit.measure(PauliBasis::Z, [a])[0];
484        circuit
485            .body_mut(circuit.entry_body())
486            .unwrap()
487            .ops_mut()
488            .push(Op::ConditionalPauli(vec![
489                ConditionalCorrection {
490                    pauli: PauliBasis::X,
491                    control: measured,
492                    target: b,
493                },
494                ConditionalCorrection {
495                    pauli: PauliBasis::Y,
496                    control: mpp[0],
497                    target: b,
498                },
499            ]));
500        let looped = circuit.reserve_measurement_id(b);
501        let body = circuit.add_body(CircuitBody::from_ops(vec![
502            Op::Gate {
503                gate: GateType::T,
504                qubits: vec![a],
505            },
506            Op::Measure {
507                basis: PauliBasis::X,
508                qubits: vec![b],
509                measurements: vec![looped],
510                flip_probability: 0.004,
511            },
512        ]));
513        circuit.push_repeat(body, 2);
514        (circuit, vec![mpp[0], measured, looped])
515    }
516
517    #[test]
518    fn every_op_lowers_and_unrolls_its_repeat() {
519        let (circuit, measurements) = every_op_circuit();
520        let qubit_map = layout_of(&circuit);
521        let plan = plan_of(circuit, &measurements);
522
523        let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
524
525        insta::assert_snapshot!(lowered.text, @r"
526        R 0 1
527        TICK
528        CX 0 1
529        DEPOLARIZE1(0.001) 0
530        DEPOLARIZE2(0.002) 0 1
531        Z_ERROR(0.003) 1
532        MPP X0*X1
533        M 0
534        CX rec[-1] 1
535        CX rec[-2] 1
536        CZ rec[-2] 1
537        S[HONEST_T] 0
538        MX(0.004) 1
539        S[HONEST_T] 0
540        MX(0.004) 1
541        ");
542        // Two loop iterations of one measurement each, on top of the MPP and
543        // the plain measurement.
544        assert_eq!(lowered.measurement_count, 4);
545        assert_eq!(
546            lowered.measurement_columns[&instance_measurement(measurements[2])],
547            3,
548            "a looped measurement reports its final occurrence",
549        );
550    }
551
552    /// The dialects differ only in how `T` is spelled, so converting one into
553    /// the other must reproduce it exactly.
554    #[test]
555    fn the_two_dialects_are_related_by_the_text_codec() {
556        let (circuit, measurements) = every_op_circuit();
557        let qubit_map = layout_of(&circuit);
558        let plan = plan_of(circuit, &measurements);
559        let options = PlanStimOptions::new(&qubit_map);
560
561        let as_stim = emit_plan_stim(&plan, &options).unwrap();
562        let as_clifft = emit_plan_stim(&plan, &options.with_dialect(StimDialect::Clifft)).unwrap();
563
564        assert!(as_clifft.text.contains("T 0"), "{}", as_clifft.text);
565        assert_eq!(crate::stim_to_clifft_text(&as_stim.text), as_clifft.text);
566        assert_eq!(crate::clifft_to_stim_text(&as_clifft.text), as_stim.text);
567        assert_eq!(as_stim.measurement_columns, as_clifft.measurement_columns);
568    }
569
570    #[test]
571    fn plan_columns_agree_with_the_standalone_column_walk() {
572        let (circuit, measurements) = every_op_circuit();
573        let qubit_map = layout_of(&circuit);
574        let expanded = circuit.expanded_measurement_columns().unwrap();
575        let plan = plan_of(circuit, &measurements);
576
577        let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
578
579        assert_eq!(lowered.measurement_count, expanded.count());
580        for (measurement, column) in &lowered.measurement_columns {
581            assert_eq!(expanded.column(measurement.measurement), Some(*column));
582        }
583    }
584
585    #[test]
586    fn a_caller_tag_marks_every_instruction_but_yields_to_honest_t() {
587        let (circuit, measurements) = every_op_circuit();
588        let qubit_map = layout_of(&circuit);
589        let plan = plan_of(circuit, &measurements);
590
591        let lowered = emit_plan_stim(
592            &plan,
593            &PlanStimOptions::new(&qubit_map).with_tag("T_SOURCE"),
594        )
595        .unwrap();
596
597        assert!(lowered.text.contains("TICK[T_SOURCE]"));
598        assert!(lowered.text.contains("MPP[T_SOURCE] X0*X1"));
599        assert!(lowered.text.contains("CX[T_SOURCE] rec[-1] 1"));
600        assert!(
601            lowered.text.contains("S[HONEST_T] 0"),
602            "the dialect tag is load-bearing and wins: {}",
603            lowered.text
604        );
605    }
606
607    #[test]
608    fn a_qubit_outside_the_map_is_reported_rather_than_emitted() {
609        let mut circuit = CoordCircuit::new();
610        circuit.do_gate(GateType::H, [ivec2(4, 4)]).unwrap();
611        let plan = plan_of(circuit, &[]);
612        let qubit_map = FxHashMap::from_iter([(ivec2(0, 0), 0)]);
613
614        assert!(matches!(
615            emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
616            Err(StimEmissionError::Circuit(CircuitError::QubitNotFoundInLayout(
617                coord
618            ))) if coord == ivec2(4, 4)
619        ));
620    }
621
622    /// Stim has no spelling at all for the off-axis non-Cliffords, so the
623    /// dialect that has to parse must refuse them.
624    #[test]
625    fn stim_dialect_rejects_a_non_clifford_without_an_s_proxy() {
626        let qubit = ivec2(0, 0);
627        let mut circuit = CoordCircuit::new();
628        circuit.do_gate(GateType::T_XZ, [qubit]).unwrap();
629        let qubit_map = layout_of(&circuit);
630        let plan = plan_of(circuit, &[]);
631
632        assert!(matches!(
633            emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
634            Err(StimEmissionError::UnsupportedGate(GateType::T_XZ))
635        ));
636        let clifft = PlanStimOptions::new(&qubit_map).with_dialect(StimDialect::Clifft);
637        assert_eq!(emit_plan_stim(&plan, &clifft).unwrap().text, "T_XZ 0\n");
638    }
639
640    #[test]
641    fn a_feedback_control_read_before_its_measurement_is_reported() {
642        let qubit = ivec2(0, 0);
643        let mut circuit = CoordCircuit::new();
644        let measurement = circuit.reserve_measurement_id(qubit);
645        circuit
646            .body_mut(circuit.entry_body())
647            .unwrap()
648            .ops_mut()
649            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
650                pauli: PauliBasis::X,
651                control: measurement,
652                target: qubit,
653            }]));
654        let qubit_map = layout_of(&circuit);
655        let plan = plan_of(circuit, &[]);
656
657        assert!(matches!(
658            emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
659            Err(StimEmissionError::MalformedGraph(_))
660        ));
661    }
662
663    #[test]
664    fn feedback_lookbacks_obey_stims_limit() {
665        let circuit = CoordCircuit::new();
666        let layout = QubitLayout::default();
667        let emitter = PlanStimEmitter {
668            circuit: &circuit,
669            layout: &layout,
670            dialect: StimDialect::Stim,
671            tag: None,
672            text: String::new(),
673            columns: FxHashMap::from_iter([(0, 0)]),
674            count: u32::MAX,
675            stack: Vec::new(),
676        };
677
678        assert!(matches!(
679            emitter.lookback_to(0),
680            Err(StimEmissionError::Circuit(CircuitError::MeasurementFrame(
681                MeasurementFrameError::RecordLookbackOutOfRange {
682                    lookback,
683                    max: MAX_STIM_RECORD_LOOKBACK,
684                }
685            ))) if lookback == u32::MAX
686        ));
687    }
688
689    #[test]
690    fn standalone_plan_rejects_qubit_indices_outside_stim_target_payload() {
691        let qubit = ivec2(0, 0);
692        let mut circuit = CoordCircuit::new();
693        circuit.do_gate(GateType::H, [qubit]).unwrap();
694        let plan = plan_of(circuit, &[]);
695        for index in [1 << 24, u32::MAX] {
696            let qubit_map = FxHashMap::from_iter([(qubit, index)]);
697            for dialect in [StimDialect::Stim, StimDialect::Clifft] {
698                let options = PlanStimOptions::new(&qubit_map).with_dialect(dialect);
699                assert_eq!(
700                    emit_plan_stim(&plan, &options),
701                    Err(StimEmissionError::QubitIndexOutOfRange {
702                        index: index as usize,
703                        max: 16_777_215,
704                    })
705                );
706            }
707        }
708    }
709
710    #[test]
711    fn standalone_plan_preserves_sparse_caller_qubit_indices() {
712        let qubit = ivec2(0, 0);
713        let mut circuit = CoordCircuit::new();
714        circuit.do_gate(GateType::H, [qubit]).unwrap();
715        let plan = plan_of(circuit, &[]);
716        for index in [100_000, 16_777_215] {
717            let qubit_map = FxHashMap::from_iter([(qubit, index)]);
718            let output = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
719            assert_eq!(output.text, format!("H {index}\n"));
720        }
721    }
722
723    #[test]
724    fn standalone_plan_emits_sparse_measurement_ids_without_dense_allocation() {
725        let qubit = ivec2(0, 0);
726        let measurement = u32::MAX - 1;
727        let mut circuit = CoordCircuit::new();
728        circuit.register_measurement_id(measurement, qubit);
729        circuit
730            .body_mut(circuit.entry_body())
731            .unwrap()
732            .ops_mut()
733            .extend([
734                Op::Measure {
735                    basis: PauliBasis::Z,
736                    qubits: vec![qubit],
737                    measurements: vec![measurement],
738                    flip_probability: 0.0,
739                },
740                Op::ConditionalPauli(vec![ConditionalCorrection {
741                    pauli: PauliBasis::X,
742                    control: measurement,
743                    target: qubit,
744                }]),
745            ]);
746        bloq_ir::lowering::validate_template_circuit(&circuit).unwrap();
747        let layout = layout_of(&circuit);
748        let plan = plan_of(circuit, &[measurement]);
749
750        let emitted = emit_plan_stim(&plan, &PlanStimOptions::new(&layout)).unwrap();
751
752        assert_eq!(emitted.text, "M 0\nCX rec[-1] 0\n");
753        assert_eq!(emitted.measurement_count, 1);
754        assert_eq!(
755            emitted.measurement_columns[&instance_measurement(measurement)],
756            0
757        );
758    }
759
760    #[cfg(feature = "verify")]
761    #[test]
762    fn arbitrary_caller_tags_round_trip_through_stim() {
763        let mut circuit = CoordCircuit::new();
764        circuit.do_gate(GateType::H, [ivec2(0, 0)]).unwrap();
765        let qubit_map = layout_of(&circuit);
766        let plan = plan_of(circuit, &[]);
767        let tag = "a]b\\c\nd\rλ";
768        let lowered =
769            emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map).with_tag(tag)).unwrap();
770        let parsed: stim::Circuit = lowered.text.parse().unwrap();
771        let instructions = parsed.into_iter().collect::<Vec<_>>();
772        let [stim::CircuitItem::Instruction(instruction)] = instructions.as_slice() else {
773            panic!("one tagged gate expected");
774        };
775        assert_eq!(instruction.tag(), tag);
776    }
777
778    #[cfg(feature = "verify")]
779    #[test]
780    fn lowered_text_parses_as_stim_with_the_reported_record_count() {
781        let (circuit, measurements) = every_op_circuit();
782        let qubit_map = layout_of(&circuit);
783        let plan = plan_of(circuit, &measurements);
784
785        let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
786
787        let parsed: stim::Circuit = lowered.text.parse().expect("lowered text is valid Stim");
788        assert_eq!(
789            parsed.num_measurements(),
790            u64::from(lowered.measurement_count)
791        );
792    }
793}