Skip to main content

bloq_stim/
bloq.rs

1use crate::emit::{
2    ResolvedLoopStates, StimAnnotationScope, StimCircuitAnnotations, StimDetector,
3    StimEmissionError, StimEmitContext, StimEmitOptions, StimMeasurementMapper, StimRepeatState,
4    emit_stim_circuit, first_unsupported_gate,
5};
6use crate::layout::{QubitLayout, coordinate_index, detector_index};
7use crate::measurement_frame::{MeasurementFrame, stim_record_lookback};
8use crate::text_utils::push_int;
9use bloq_circuit::{
10    Basis, BodyId, CircuitError, CoordCircuit, FlowMarker, GateType, LoopStateId, NoiseModel, Op,
11    Pauli, PauliMap,
12};
13use bloq_ir::{
14    Bloq, BloqNode, BloqNodeId, BloqNodeKind, BodySelector, ClassicalAssignment, ClassicalNode,
15    LevelPath, MomentLane, RegionNode, SubGraph, TemplateId, ValidatedPlans, align_moment_lanes,
16    aligned_moment_segments,
17    lowering::{
18        InstanceMeasurement, InstantiationOptions, NodeEmissionPlan, TemplateDetectorScope,
19        TemplateInstanceId,
20    },
21};
22use glam::IVec2;
23use rustc_hash::{FxHashMap, FxHashSet};
24use std::{
25    borrow::Cow,
26    collections::{BTreeMap, BTreeSet},
27};
28
29/// One output-frame correction recipe for an isolated T attempt.
30/// The corrected bit XORs measurement parity, `sign`, and the GAP decoder flip.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct IsolatedTFrameRecipe {
33    /// Physical measurement columns XORed into the raw frame bit.
34    pub measurements: Vec<u32>,
35    /// Constant XORed with the measurement parity before decoder correction.
36    pub sign: bool,
37    /// GAP observable whose decoder flip corrects this frame bit.
38    pub gap_observable: u32,
39}
40
41/// One terminal stabilizer sheet appended to the causal companion.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct IsolatedTFrontierSheet {
44    /// Observable id this sheet uses in [`IsolatedTAttemptArtifacts::sheets`].
45    pub observable: u32,
46    /// Homogeneous stabilizer basis of the terminal boundary operator.
47    pub basis: Basis,
48    /// Expected parity sign of the boundary flow.
49    pub sign: bool,
50}
51
52/// Index data shared by all isolated-attempt circuit variants.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct IsolatedTAttemptManifest {
55    /// Path of the emitted `RepeatUntilSuccess` body.
56    pub rus_path: LevelPath,
57    /// Expected sign of every emitted detector, in circuit detector order.
58    pub detector_signs: Vec<bool>,
59    /// Zero-based detector indices tagged `POST-SELECTION`.
60    pub postselection_detectors: Vec<u32>,
61    /// GAP observable carrying the logical X sheet.
62    pub gap_x_observable: u32,
63    /// GAP observable carrying the logical Z sheet.
64    pub gap_z_observable: u32,
65    /// Terminal output X-frame correction recipe.
66    pub frame_x: IsolatedTFrameRecipe,
67    /// Terminal output Z-frame correction recipe.
68    pub frame_z: IsolatedTFrameRecipe,
69    /// Frontier sheets in append order. Sheet `i` maps to synthetic detector
70    /// `base_companion_detector_count + i` in the causal DEM transform.
71    pub frontier_sheets: Vec<IsolatedTFrontierSheet>,
72    /// Static signs multiplying direct X/Y/Z `EXP_VAL` probes.
73    pub exp_val_signs: [i8; 3],
74}
75
76/// Physical sampling circuits and their shared causal decoding companion.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct IsolatedTAttemptArtifacts {
79    /// Full-noise S control, with direct X/Y/Z `EXP_VAL` probes.
80    pub physical_s: String,
81    /// Full-noise honest-T circuit, with direct X/Y/Z `EXP_VAL` probes.
82    pub physical_t: String,
83    /// Partially-noiseless S-proxy companion carrying the GAP pair.
84    pub companion: String,
85    /// Terminal stabilizer sheet observables, as a standalone suffix.
86    ///
87    /// Appending this to [`Self::companion`] gives the sheeted companion.
88    /// It is kept separate because consumers need the suffix *alone* (to
89    /// build the frontier DEM), and recovering it from a concatenated field
90    /// means slicing one circuit's text by another's byte length.
91    pub sheets: String,
92    /// Detector, frame, GAP, frontier, and probe index data.
93    pub manifest: IsolatedTAttemptManifest,
94}
95
96/// Whether an emission entry point checks its input program first.
97///
98/// Emission needs the WF-9 per-node merge either way (it is what the backend
99/// emits from); this only selects whether the full T2 well-formedness audit
100/// runs alongside it. The default is [`Trusted`](Self::Trusted), so ordinary
101/// emission does not run that audit.
102///
103/// Use [`Checked`](Self::Checked) to request the audit before emitting. In
104/// trusted mode, malformed input can produce a wrong circuit, although plan
105/// construction and emission still report errors they encounter.
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
107pub enum InputTrust {
108    /// Skip the full T2 audit; still build the required emission plans.
109    #[default]
110    Trusted,
111    /// Run the full T2 audit before emitting.
112    Checked,
113}
114
115/// Options for noise, input validation, and whole-program moment alignment.
116///
117/// Alignment rejects noise and segmented output with
118/// [`StimEmissionError::AlignMomentsWithNoise`] and
119/// [`StimEmissionError::AlignMomentsWithSegments`].
120///
121/// # Examples
122///
123/// ```
124/// use bloq_stim::{BloqStimOptions, InputTrust, emit_bloq_stim_with};
125///
126/// let graph = bloq_graph::GalleryItem::CNOT.build();
127/// let program = bloq_compile::compile(&graph, 3)?;
128/// let noise = bloq_circuit::NoiseModel::uniform_depolarizing(1e-3);
129///
130/// let options = BloqStimOptions::new()
131///     .with_noise(&noise)
132///     .with_trust(InputTrust::Checked);
133/// assert!(emit_bloq_stim_with(&program, &options)?.contains("DEPOLARIZE1"));
134/// # Ok::<(), Box<dyn std::error::Error>>(())
135/// ```
136#[derive(Debug, Clone, Copy, Default)]
137pub struct BloqStimOptions<'a> {
138    noise: Option<&'a NoiseModel>,
139    trust: InputTrust,
140    align_moments: bool,
141}
142
143impl<'a> BloqStimOptions<'a> {
144    /// Noiseless emission without a full input audit, as [`emit_bloq_stim`] uses.
145    #[must_use]
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Annotate every instantiated circuit with `noise` before emitting.
151    #[must_use]
152    pub fn with_noise(mut self, noise: &'a NoiseModel) -> Self {
153        self.noise = Some(noise);
154        self
155    }
156
157    /// Whether to run the full T2 audit first; see [`InputTrust`].
158    #[must_use]
159    pub fn with_trust(mut self, trust: InputTrust) -> Self {
160        self.trust = trust;
161        self
162    }
163
164    /// Align compatible node moments across each block layer.
165    #[must_use]
166    pub fn with_align_moments(mut self, align: bool) -> Self {
167        self.align_moments = align;
168        self
169    }
170}
171
172/// Linearizes a [`Bloq`] into Stim text, noiseless and trusting the program to
173/// be well-formed ([`InputTrust::Trusted`]).
174///
175/// Emits node-local circuits in deterministic topological order and resolves
176/// node-local annotations against one global measurement frame. Use
177/// [`emit_bloq_stim_with`] for noise or a checked input.
178///
179/// # Errors
180///
181/// Returns [`StimEmissionError`] if the program cannot be statically emitted.
182pub fn emit_bloq_stim(program: &Bloq) -> Result<String, StimEmissionError> {
183    emit_bloq_stim_with(program, &BloqStimOptions::new())
184}
185
186/// [`emit_bloq_stim`] under explicit [`BloqStimOptions`].
187///
188/// With moment alignment enabled, repeat bodies are flattened and compatible raw moments from
189/// quantum nodes occupying the same source layer share global `TICK` slots.
190///
191/// # Errors
192///
193/// Returns [`StimEmissionError`] for invalid, unsupported, or inconsistent input.
194pub fn emit_bloq_stim_with(
195    program: &Bloq,
196    options: &BloqStimOptions<'_>,
197) -> Result<String, StimEmissionError> {
198    if options.align_moments {
199        if options.noise.is_some() {
200            return Err(StimEmissionError::AlignMomentsWithNoise);
201        }
202        if options.trust == InputTrust::Checked {
203            program.validate()?;
204        }
205        reject_unsupported(program)?;
206        let mut flat = program.clone();
207        flat.flatten()?;
208        let plans = flat.emission_plans(&InstantiationOptions::default())?;
209        let order = flat.deterministic_emit_order()?;
210        return BloqStimEmitter::new(&flat, None, &plans).emit_order_aligned(&order);
211    }
212    let (plans, order) = prepare_untransformed(program, options)?;
213    BloqStimEmitter::new(program, options.noise, &plans).emit_order_untransformed(&order)
214}
215
216/// Shared plans and order for whole-text and segmented emission.
217///
218/// Checked inputs return validation errors before capability errors. Trusted
219/// inputs skip the audit, so unsupported constructs must be rejected before
220/// merge work.
221fn prepare_untransformed(
222    program: &Bloq,
223    options: &BloqStimOptions<'_>,
224) -> Result<(ValidatedPlans, Vec<BloqNodeId>), StimEmissionError> {
225    if options.trust == InputTrust::Trusted {
226        reject_unsupported(program)?;
227    }
228    let plans = emission_plans(program, options.noise, options.trust)?;
229    if options.trust == InputTrust::Checked {
230        reject_unsupported(program)?;
231    }
232    Ok((plans, program.deterministic_emit_order()?))
233}
234
235/// Emit one flattened attempt of the unique isolated-T RUS body.
236///
237/// The physical strings contain no `OBSERVABLE_INCLUDE`; they end in direct
238/// X/Y/Z `EXP_VAL` probes for Clifft. The companion strings are Stim/DEM
239/// circuits: `companion` carries the X/Z GAP pair, and `sheets` is a suffix
240/// carrying one observable per terminal stabilizer frontier, which appended
241/// to `companion` gives the sheeted circuit.
242/// Memory latency must already be inserted into `program`.
243///
244/// # Errors
245///
246/// Returns [`StimEmissionError`] if the program is not one valid isolated-T attempt.
247pub fn emit_isolated_t_attempts(
248    program: &Bloq,
249    physical_error_probability: f64,
250) -> Result<IsolatedTAttemptArtifacts, StimEmissionError> {
251    if !physical_error_probability.is_finite() || !(0.0..=1.0).contains(&physical_error_probability)
252    {
253        return Err(StimEmissionError::InvalidNoiseProbability);
254    }
255
256    let mut flat = program.clone();
257    flat.flatten()?;
258    // The flattened clone is this function's own construction, not caller
259    // input, so it inherits the caller's trust in `program`.
260    let plans = emission_plans(&flat, None, InputTrust::default())?;
261    let path = isolated_rus_path(&flat)?;
262    let body = flat
263        .level_at(&path)
264        .ok_or(StimEmissionError::MalformedGraph(
265            "isolated-T RUS body path is stale",
266        ))?;
267    let order = body.deterministic_emit_order()?;
268    let cultivation = isolated_cultivation_node(&flat, body)?;
269    let terminal = order
270        .iter()
271        .rev()
272        .copied()
273        .find(|&node| body[node].try_quantum().is_some())
274        .ok_or(StimEmissionError::MalformedGraph(
275            "isolated-T RUS body has no quantum node",
276        ))?;
277    let (gap_x, gap_z, gap_records) = isolated_gap_observables(body)?;
278    let frames = flat.output_frames();
279    if frames.len() != 1 || flat.logical_outputs().len() != 1 {
280        return Err(StimEmissionError::MalformedGraph(
281            "isolated-T program must have exactly one output frame and logical output",
282        ));
283    }
284
285    let noise = NoiseModel::uniform_depolarizing(physical_error_probability);
286    let render = |mode| {
287        BloqStimEmitter::new_attempt(&flat, body, path.clone(), &noise, mode, &plans)
288            .emit_order_attempt(&order)
289    };
290    let mut physical_s = render(AttemptMode::SControl)?;
291    let mut physical_t = render(AttemptMode::HonestT)?;
292    let companion = render(AttemptMode::Companion { cultivation })?;
293    ensure_attempt_alignment(&physical_s, &physical_t, &companion)?;
294
295    let frame_x = isolated_frame_recipe(
296        flat.top(),
297        frames[0].x,
298        &companion.measurement_columns,
299        &gap_records,
300    )?;
301    let frame_z = isolated_frame_recipe(
302        flat.top(),
303        frames[0].z,
304        &companion.measurement_columns,
305        &gap_records,
306    )?;
307    if frame_x.gap_observable != gap_z || frame_z.gap_observable != gap_x {
308        return Err(StimEmissionError::MalformedGraph(
309            "isolated-T output frames do not cross-map X to GAP-Z and Z to GAP-X",
310        ));
311    }
312
313    let exp_val_signs = append_direct_exp_vals(
314        &mut physical_s.text,
315        &flat,
316        &flat.logical_outputs()[0].x,
317        &flat.logical_outputs()[0].z,
318    )?;
319    let t_signs = append_direct_exp_vals(
320        &mut physical_t.text,
321        &flat,
322        &flat.logical_outputs()[0].x,
323        &flat.logical_outputs()[0].z,
324    )?;
325    if t_signs != exp_val_signs {
326        return Err(StimEmissionError::MalformedGraph(
327            "isolated-T S/T probe signs disagree",
328        ));
329    }
330
331    let mut sheets = String::new();
332    let frontier_sheets = append_frontier_sheets(
333        &mut sheets,
334        &flat,
335        body,
336        terminal,
337        &companion.measurement_columns,
338    )?;
339    let detector_signs = companion
340        .detectors
341        .iter()
342        .map(|detector| detector.sign)
343        .collect();
344    let postselection_detectors = companion
345        .detectors
346        .iter()
347        .enumerate()
348        .filter(|(_, detector)| detector.postselection)
349        .map(|(index, _)| detector_index(index))
350        .collect::<Result<_, _>>()?;
351
352    Ok(IsolatedTAttemptArtifacts {
353        physical_s: physical_s.text,
354        physical_t: physical_t.text,
355        companion: companion.text,
356        sheets,
357        manifest: IsolatedTAttemptManifest {
358            rus_path: path,
359            detector_signs,
360            postselection_detectors,
361            gap_x_observable: gap_x,
362            gap_z_observable: gap_z,
363            frame_x,
364            frame_z,
365            frontier_sheets,
366            exp_val_signs,
367        },
368    })
369}
370
371fn isolated_rus_path(program: &Bloq) -> Result<LevelPath, StimEmissionError> {
372    let mut rus = program.nodes().filter_map(|(id, node)| {
373        matches!(
374            node.try_region(),
375            Some(RegionNode::RepeatUntilSuccess { .. })
376        )
377        .then_some(id)
378    });
379    let id = rus.next().ok_or(StimEmissionError::MalformedGraph(
380        "isolated-T program has no top-level RepeatUntilSuccess",
381    ))?;
382    if rus.next().is_some() {
383        return Err(StimEmissionError::MalformedGraph(
384            "isolated-T program has more than one top-level RepeatUntilSuccess",
385        ));
386    }
387    Ok(LevelPath::default().child(id, BodySelector::Body))
388}
389
390fn isolated_cultivation_node(
391    program: &Bloq,
392    body: &SubGraph,
393) -> Result<BloqNodeId, StimEmissionError> {
394    let mut cultivation = None;
395    let mut quantum_count = 0;
396    for (node, quantum) in body.quantum_nodes() {
397        quantum_count += 1;
398        if quantum.instances.len() != 1 {
399            return Err(StimEmissionError::MalformedGraph(
400                "isolated-T body quantum nodes must have one template instance",
401            ));
402        }
403        let template = program
404            .templates()
405            .get(quantum.instances[0].template_id)
406            .ok_or(StimEmissionError::MalformedGraph(
407                "isolated-T body instance references a missing template",
408            ))?;
409        if first_unsupported_gate(&template.circuit).is_some()
410            && cultivation.replace(node).is_some()
411        {
412            return Err(StimEmissionError::MalformedGraph(
413                "isolated-T body has more than one non-Clifford stage",
414            ));
415        }
416    }
417    if quantum_count < 2 {
418        return Err(StimEmissionError::MalformedGraph(
419            "isolated-T body must contain cultivation and escape stages",
420        ));
421    }
422    cultivation.ok_or(StimEmissionError::MalformedGraph(
423        "isolated-T body has no non-Clifford cultivation stage",
424    ))
425}
426
427type GapRecordSets = BTreeMap<u32, BTreeSet<InstanceMeasurement>>;
428
429fn isolated_gap_observables(
430    body: &SubGraph,
431) -> Result<(u32, u32, GapRecordSets), StimEmissionError> {
432    let mut indices = BTreeSet::new();
433    for (_, node) in body.nodes() {
434        if let Some(ClassicalNode::Observable {
435            index: Some(index), ..
436        }) = node.try_classical()
437        {
438            indices.insert(*index);
439        }
440    }
441    if indices.len() != 2 {
442        return Err(StimEmissionError::MalformedGraph(
443            "isolated-T body must contain two GAP observables",
444        ));
445    }
446    let mut gap_x = None;
447    let mut gap_z = None;
448    let mut records = BTreeMap::new();
449    for index in indices {
450        let observable = observable_node(body, index)?;
451        let measurements = body
452            .resolve_classical(observable, ClassicalAssignment::Uniform(false))?
453            .measurements;
454        records.insert(index, measurements);
455        match observable_boundary_basis(body, observable)? {
456            Basis::X if gap_x.replace(index).is_none() => {}
457            Basis::Z if gap_z.replace(index).is_none() => {}
458            _ => {
459                return Err(StimEmissionError::MalformedGraph(
460                    "isolated-T GAP observables must have one X and one Z sheet",
461                ));
462            }
463        }
464    }
465    Ok((
466        gap_x.expect("two checked GAP bases include X"),
467        gap_z.expect("two checked GAP bases include Z"),
468        records,
469    ))
470}
471
472fn observable_node(level: &SubGraph, index: u32) -> Result<BloqNodeId, StimEmissionError> {
473    let mut nodes = level.nodes().filter_map(|(id, node)| {
474        matches!(
475            node.try_classical(),
476            Some(ClassicalNode::Observable { index: Some(candidate), .. }) if *candidate == index
477        )
478        .then_some(id)
479    });
480    let node = nodes.next().ok_or(StimEmissionError::MalformedGraph(
481        "decoder references a missing isolated-T observable",
482    ))?;
483    if nodes.next().is_some() {
484        return Err(StimEmissionError::MalformedGraph(
485            "isolated-T graph repeats an observable index in one level",
486        ));
487    }
488    Ok(node)
489}
490
491fn observable_boundary_basis(
492    level: &SubGraph,
493    observable: BloqNodeId,
494) -> Result<Basis, StimEmissionError> {
495    let mut basis = None;
496    let mut stack = vec![observable];
497    let mut visited = FxHashSet::default();
498    while let Some(producer) = stack.pop() {
499        if !visited.insert(producer) {
500            continue;
501        }
502        let Some(ClassicalNode::Observable { operators, .. }) = level[producer].try_classical()
503        else {
504            continue;
505        };
506        stack.extend(
507            level
508                .data_inputs(producer)
509                .filter(|input| input.output.is_none())
510                .map(|input| input.producer),
511        );
512        for operator in operators {
513            for (_, pauli) in &operator.operator {
514                let next = match pauli {
515                    Pauli::X => Basis::X,
516                    Pauli::Z => Basis::Z,
517                    Pauli::I | Pauli::Y => {
518                        return Err(StimEmissionError::MalformedGraph(
519                            "isolated-T boundary sheet is not homogeneous X or Z",
520                        ));
521                    }
522                };
523                if basis.is_some_and(|current| current != next) {
524                    return Err(StimEmissionError::MalformedGraph(
525                        "isolated-T boundary sheet mixes X and Z support",
526                    ));
527                }
528                basis = Some(next);
529            }
530        }
531    }
532    basis.ok_or(StimEmissionError::MalformedGraph(
533        "isolated-T GAP observable has no boundary sheet",
534    ))
535}
536
537fn isolated_frame_recipe(
538    top: &SubGraph,
539    frame: BloqNodeId,
540    columns: &FxHashMap<InstanceMeasurement, u32>,
541    gap_records: &GapRecordSets,
542) -> Result<IsolatedTFrameRecipe, StimEmissionError> {
543    let value = top.resolve_classical(frame, ClassicalAssignment::Uniform(false))?;
544    let records = value.measurements;
545    let gap_observable = gap_records
546        .iter()
547        .find_map(|(&index, candidate)| (candidate == &records).then_some(index))
548        .ok_or(StimEmissionError::MalformedGraph(
549            "isolated-T output frame records do not match either GAP observable",
550        ))?;
551    let mut measurements = records
552        .iter()
553        .map(|measurement| {
554            columns
555                .get(measurement)
556                .copied()
557                .ok_or(StimEmissionError::MalformedGraph(
558                    "isolated-T frame references a measurement outside the RUS body",
559                ))
560        })
561        .collect::<Result<Vec<_>, _>>()?;
562    measurements.sort_unstable();
563    Ok(IsolatedTFrameRecipe {
564        measurements,
565        sign: value.sign,
566        gap_observable,
567    })
568}
569
570fn ensure_attempt_alignment(
571    physical_s: &AttemptStimRender,
572    physical_t: &AttemptStimRender,
573    companion: &AttemptStimRender,
574) -> Result<(), StimEmissionError> {
575    if physical_s.measurement_columns != physical_t.measurement_columns
576        || physical_s.measurement_columns != companion.measurement_columns
577        || physical_s.detectors != physical_t.detectors
578        || physical_s.detectors != companion.detectors
579    {
580        return Err(StimEmissionError::MalformedGraph(
581            "isolated-T S, T, and companion measurement/detector recipes disagree",
582        ));
583    }
584    Ok(())
585}
586
587fn append_frontier_sheets(
588    output: &mut String,
589    program: &Bloq,
590    body: &SubGraph,
591    terminal: BloqNodeId,
592    columns: &FxHashMap<InstanceMeasurement, u32>,
593) -> Result<Vec<IsolatedTFrontierSheet>, StimEmissionError> {
594    let quantum = body[terminal].expect_quantum();
595    let instance = quantum
596        .instances
597        .first()
598        .ok_or(StimEmissionError::MalformedGraph(
599            "isolated-T terminal node has no template instance",
600        ))?;
601    let template =
602        program
603            .templates()
604            .get(instance.template_id)
605            .ok_or(StimEmissionError::MalformedGraph(
606                "isolated-T terminal instance references a missing template",
607            ))?;
608    let measurement_count = columns
609        .values()
610        .copied()
611        .max()
612        .and_then(|last| last.checked_add(1))
613        .ok_or(StimEmissionError::MalformedGraph(
614            "isolated-T attempt emitted no measurements",
615        ))?;
616    let coords = program.sorted_layout_coords()?;
617    let layout = QubitLayout::new(coordinate_index(coords.iter().copied())?)?;
618    let max_observable = program
619        .levels()
620        .flat_map(|(_, level)| level.nodes())
621        .filter_map(|(_, node)| match node.try_classical() {
622            Some(ClassicalNode::Observable { index, .. }) => *index,
623            _ => None,
624        })
625        .max();
626    let mut next_observable = match max_observable {
627        Some(index) => index
628            .checked_add(1)
629            .ok_or(StimEmissionError::MalformedGraph(
630                "isolated-T observable id overflow",
631            ))?,
632        None => 0,
633    };
634    let mut sheets = Vec::new();
635    for flow in template.boundary_flows.iter().filter(|flow| {
636        flow.start.is_empty() && !flow.end.is_empty() && flow.marker == FlowMarker::Detector
637    }) {
638        let basis = homogeneous_basis(&flow.end)?;
639        let mut lookbacks = flow
640            .measurements
641            .iter()
642            .map(|&measurement| {
643                let column = columns
644                    .get(&InstanceMeasurement {
645                        instance: instance.id,
646                        measurement,
647                    })
648                    .copied()
649                    .ok_or(StimEmissionError::MalformedGraph(
650                        "terminal frontier references an unemitted measurement",
651                    ))?;
652                let lookback = measurement_count - column;
653                stim_record_lookback(lookback).ok_or(StimEmissionError::MalformedGraph(
654                    "terminal frontier measurement lookback exceeds Stim range",
655                ))
656            })
657            .collect::<Result<Vec<_>, _>>()?;
658        lookbacks.sort_unstable();
659        crate::emit::emit_observable_include_records(output, next_observable, &lookbacks);
660        let operator = flow.end.try_translated(instance.offset)?;
661        crate::emit::emit_observable_include_pauli_targets(
662            output,
663            next_observable,
664            &operator,
665            &layout,
666        )?;
667        sheets.push(IsolatedTFrontierSheet {
668            observable: next_observable,
669            basis,
670            sign: flow.sign,
671        });
672        next_observable =
673            next_observable
674                .checked_add(1)
675                .ok_or(StimEmissionError::MalformedGraph(
676                    "isolated-T observable id overflow",
677                ))?;
678    }
679    if sheets.is_empty() {
680        return Err(StimEmissionError::MalformedGraph(
681            "isolated-T terminal node has no open detector frontier",
682        ));
683    }
684    Ok(sheets)
685}
686
687fn homogeneous_basis(operator: &PauliMap) -> Result<Basis, StimEmissionError> {
688    let mut basis = None;
689    for (_, pauli) in operator {
690        let next = match pauli {
691            Pauli::X => Basis::X,
692            Pauli::Z => Basis::Z,
693            Pauli::I | Pauli::Y => {
694                return Err(StimEmissionError::MalformedGraph(
695                    "terminal frontier is not homogeneous X or Z",
696                ));
697            }
698        };
699        if basis.is_some_and(|current| current != next) {
700            return Err(StimEmissionError::MalformedGraph(
701                "terminal frontier mixes X and Z support",
702            ));
703        }
704        basis = Some(next);
705    }
706    basis.ok_or(StimEmissionError::MalformedGraph(
707        "terminal frontier has empty Pauli support",
708    ))
709}
710
711fn append_direct_exp_vals(
712    output: &mut String,
713    program: &Bloq,
714    logical_x: &PauliMap,
715    logical_z: &PauliMap,
716) -> Result<[i8; 3], StimEmissionError> {
717    let coords = program.sorted_layout_coords()?;
718    let layout = coordinate_index(coords.iter().copied())?;
719    let (logical_y, y_sign) = signed_logical_y(logical_x, logical_z)?;
720    for operator in [logical_x, &logical_y, logical_z] {
721        output.push_str("EXP_VAL ");
722        let mut first = true;
723        for (coord, pauli) in operator {
724            if !first {
725                output.push('*');
726            }
727            first = false;
728            output.push_str(&pauli.to_string());
729            push_int(
730                output,
731                *layout.get(coord).ok_or(StimEmissionError::MalformedGraph(
732                    "logical output probe names a qubit outside the global layout",
733                ))?,
734            );
735        }
736        if first {
737            return Err(StimEmissionError::MalformedGraph(
738                "logical output probe has empty Pauli support",
739            ));
740        }
741        output.push('\n');
742    }
743    Ok([1, y_sign, 1])
744}
745
746fn signed_logical_y(
747    logical_x: &PauliMap,
748    logical_z: &PauliMap,
749) -> Result<(PauliMap, i8), StimEmissionError> {
750    // Phase exponent in powers of i. The leading i makes Y = iXZ Hermitian.
751    let phase = (1 + logical_x.product_phase(logical_z)) % 4;
752    let sign = match phase {
753        0 => 1,
754        2 => -1,
755        _ => {
756            return Err(StimEmissionError::MalformedGraph(
757                "logical iXZ probe is not Hermitian",
758            ));
759        }
760    };
761    Ok((logical_x ^ logical_z, sign))
762}
763
764/// The per-node emission plans the merge pass built (IR-02) — the exact plans
765/// [`BloqStimEmitter::emit_instantiated_node`] emits from, so the SEM-MERGE
766/// runs once here rather than again per node.
767///
768/// `trust` selects whether the full T2 audit runs alongside the merge.
769fn emission_plans(
770    program: &Bloq,
771    noise: Option<&NoiseModel>,
772    trust: InputTrust,
773) -> Result<ValidatedPlans, StimEmissionError> {
774    let options = noise.map_or_else(InstantiationOptions::default, InstantiationOptions::noisy);
775    if trust == InputTrust::Checked {
776        return Ok(program.validate_with_plans(&options)?);
777    }
778    Ok(program.emission_plans(&options)?)
779}
780
781/// Reject a program the static Stim backend cannot emit, before any emission
782/// work. Un-emittability of a gate or a node *kind* (runtime-control regions,
783/// [`Discard`](ClassicalNode::Discard)) is a static property, so both are checked here rather than
784/// mid-walk in [`BloqStimEmitter::emit_node`].
785fn reject_unsupported(program: &Bloq) -> Result<(), StimEmissionError> {
786    // Region nodes are rejected by name first: a T block's cultivation template
787    // carries the real non-Clifford `T_DAG`, but the reason the static path
788    // cannot emit it is the enclosing `RepeatUntilSuccess` region (dynamic
789    // control), not the gate. Rejecting the gate here would leak an
790    // implementation detail and mask the structural reason. A genuinely
791    // top-level non-Clifford gate (no region) still falls through to the gate
792    // check below.
793    if let Some(node) = first_unsupported_program_node(program) {
794        return Err(StimEmissionError::UnsupportedNode(node));
795    }
796    if let Some(gate) = first_unsupported_program_gate(program) {
797        return Err(StimEmissionError::UnsupportedGate(gate));
798    }
799    Ok(())
800}
801
802/// The name of the first node the static backend cannot emit, in node-id order,
803/// detected up front so no partial emission happens first.
804fn first_unsupported_program_node(program: &Bloq) -> Option<&'static str> {
805    program
806        .nodes()
807        .find_map(|(_, node)| stim_unsupported_reason(node))
808        .or_else(|| {
809            program
810                .has_conditional_membership()
811                .then_some("conditional quantum seams")
812        })
813}
814
815/// Why the static backend cannot emit `node`, or `None` if it can. The single
816/// source of truth for emittability: `reject_unsupported` scans it up front and
817/// [`BloqStimEmitter::emit_node`] asserts against it, so the two never diverge.
818fn stim_unsupported_reason(node: &BloqNode) -> Option<&'static str> {
819    if node.activation.is_some() {
820        return Some("activation (conditional execution)");
821    }
822    if node
823        .try_quantum()
824        .is_some_and(|quantum| !quantum.guards.is_empty())
825    {
826        return Some("conditional component membership");
827    }
828    match (node.try_classical(), node.try_region()) {
829        // An Observable produces corrected parity; its static-path flip
830        // estimate is identically false (no decoder in the loop — corrections
831        // defer to the offline decoder / logical Pauli frame), so it reduces to
832        // its records (SEM-DECODE), the same degenerate reduction
833        // the noiseless executor uses.
834        // `OBSERVABLE_INCLUDE` is a *linear* (XOR) accumulation of measurement
835        // records, so a feedback whose condition is linear (`Xor`/`Not`/`Const`
836        // over measurement bits) folds directly into the affected observable and
837        // emits fine. A *nonlinear* condition (`And`/`Or`) is not XOR-expressible;
838        // it needs conditional execution, so we reject it up front
839        // rather than fold it incorrectly.
840        // `Xor`/`Not`/`In`/`Const` are linear (a `Const` or `Not` only toggles
841        // a sign Stim already ignores); see [`ClassicalExpr::is_linear`].
842        (Some(ClassicalNode::Compute { expr }), _) if !expr.is_linear() => {
843            Some("Compute (nonlinear And/Or condition requires conditional execution)")
844        }
845        // A `Discard` is shot postselection the static backend cannot express
846        // (Stim has no reject-shot op), so reject it rather than silently drop it
847        // (SEM-DISCARD).
848        (Some(ClassicalNode::Discard { .. }), _) => Some("Discard (shot postselection)"),
849        (_, Some(_)) => Some("RepeatUntilSuccess (postselected retry)"),
850        _ => None,
851    }
852}
853
854/// One node's slice of a segmented emission: its Stim text plus where that
855/// text sits in the program's measurement record stream.
856///
857/// A node that emits nothing (most classical nodes) still gets a segment, with
858/// empty `text` and a zero `measurement_count`, so the segment list always
859/// mirrors [`Bloq::deterministic_emit_order`] one-for-one.
860#[derive(Debug, Clone, PartialEq, Eq)]
861pub struct StimSegment {
862    /// The node this chunk was emitted from.
863    pub node_id: BloqNodeId,
864    /// The node's Stim text.
865    pub text: String,
866    /// Absolute index of this chunk's first measurement record in the
867    /// reassembled circuit.
868    pub measurement_start: usize,
869    /// How many measurement records this chunk contributes.
870    pub measurement_count: usize,
871}
872
873/// Emitted Stim text split into the shared header and one chunk per node.
874///
875/// Chunks share one global measurement frame, so a node's `rec[-k]` detector
876/// targets cross chunk boundaries and only resolve once reassembled — see
877/// [`Self::to_text`].
878#[derive(Debug, Clone)]
879pub struct BloqStimSegments {
880    /// `QUBIT_COORDS` preamble shared by every node.
881    pub header: String,
882    /// Per-node chunks in [`Bloq::deterministic_emit_order`].
883    pub segments: Vec<StimSegment>,
884}
885
886impl BloqStimSegments {
887    /// Reassemble the header and every chunk into one circuit — the whole
888    /// point of the split being reversible, and the only form in which the
889    /// cross-chunk `rec[-k]` targets resolve.
890    pub fn to_text(&self) -> String {
891        let capacity = self.header.len()
892            + self
893                .segments
894                .iter()
895                .map(|segment| segment.text.len())
896                .sum::<usize>();
897        let mut text = String::with_capacity(capacity);
898        text.push_str(&self.header);
899        for segment in &self.segments {
900            text.push_str(&segment.text);
901        }
902        text
903    }
904
905    /// How many measurement records the reassembled circuit emits.
906    pub fn num_measurements(&self) -> usize {
907        self.segments.last().map_or(0, |segment| {
908            segment.measurement_start + segment.measurement_count
909        })
910    }
911}
912
913/// Linearize a [`Bloq`] into per-node Stim chunks (see [`BloqStimSegments`]),
914/// noiseless and trusting the program.
915///
916/// Production callers want [`emit_bloq_stim`], which streams into one buffer.
917/// This variant exists for per-node inspection and index alignment. Use
918/// [`emit_bloq_stim_segments_with`] for noise or a checked input; noise is
919/// already part of each node's instantiated circuit by the time it is chunked.
920///
921/// # Errors
922///
923/// Returns [`StimEmissionError`] if the program cannot be statically emitted.
924pub fn emit_bloq_stim_segments(program: &Bloq) -> Result<BloqStimSegments, StimEmissionError> {
925    emit_bloq_stim_segments_with(program, &BloqStimOptions::new())
926}
927
928/// [`emit_bloq_stim_segments`] under explicit [`BloqStimOptions`].
929///
930/// # Errors
931///
932/// Returns [`StimEmissionError`] for invalid, unsupported, or inconsistent input.
933pub fn emit_bloq_stim_segments_with(
934    program: &Bloq,
935    options: &BloqStimOptions<'_>,
936) -> Result<BloqStimSegments, StimEmissionError> {
937    if options.align_moments {
938        return Err(StimEmissionError::AlignMomentsWithSegments);
939    }
940    let (plans, order) = prepare_untransformed(program, options)?;
941    BloqStimEmitter::new(program, options.noise, &plans).emit_order_segments(&order)
942}
943
944/// Emit the same program twice, clean and with `noise`, as `(clean, noisy)`.
945///
946/// A Monte-Carlo consumer needs both: the noisy circuit to sample and the
947/// clean one to read reference signs and decoder recipes off. Because both
948/// come from one call, this guarantees what a caller emitting twice can only
949/// assert afterwards — the two share a node order and a measurement layout, so
950/// a column resolved against one indexes the other. If they ever disagree the
951/// emission is inconsistent and this fails rather than handing back a pair
952/// whose indices silently diverge.
953///
954/// # Errors
955///
956/// Anything either emission would return on its own, plus
957/// [`StimEmissionError::MalformedGraph`] if the two disagree on node order or
958/// measurement layout.
959pub fn emit_bloq_stim_segments_pair(
960    program: &Bloq,
961    noise: &NoiseModel,
962) -> Result<(BloqStimSegments, BloqStimSegments), StimEmissionError> {
963    reject_unsupported(program)?;
964    let order = program.deterministic_emit_order()?;
965    let trust = InputTrust::default();
966
967    let clean_plans = emission_plans(program, None, trust)?;
968    let clean = BloqStimEmitter::new(program, None, &clean_plans).emit_order_segments(&order)?;
969    let noisy_plans = emission_plans(program, Some(noise), trust)?;
970    let noisy =
971        BloqStimEmitter::new(program, Some(noise), &noisy_plans).emit_order_segments(&order)?;
972
973    let aligned = clean.segments.len() == noisy.segments.len()
974        && std::iter::zip(&clean.segments, &noisy.segments).all(|(clean, noisy)| {
975            clean.node_id == noisy.node_id
976                && clean.measurement_start == noisy.measurement_start
977                && clean.measurement_count == noisy.measurement_count
978        });
979    if !aligned {
980        return Err(StimEmissionError::MalformedGraph(
981            "clean and noisy emission disagree on node order or measurement layout",
982        ));
983    }
984    Ok((clean, noisy))
985}
986
987struct BloqStimEmitter<'a> {
988    program: &'a Bloq,
989    level: &'a SubGraph,
990    path: LevelPath,
991    noise: Option<&'a NoiseModel>,
992    attempt: Option<AttemptRender<'a>>,
993    /// The per-node emission plans (IR-02). A multi-instance or noisy
994    /// quantum node emits from its plan here instead of re-running the merge; the
995    /// single-instance fast path emits its template verbatim and ignores its plan.
996    plans: &'a ValidatedPlans,
997    output: String,
998    frame: MeasurementFrame,
999    num_measurements: u32,
1000    layout: QubitLayout,
1001    indexed_coords: &'a [IVec2],
1002    resolved_loop_detector_states: ResolvedLoopStates,
1003    instance_measurements: FxHashMap<InstanceMeasurement, u32>,
1004    instance_templates: FxHashMap<TemplateInstanceId, TemplateId>,
1005    instance_loop_states: FxHashMap<InstanceLoopState, LoopStateId>,
1006    emitted_observables: FxHashSet<u32>,
1007    detector_metadata: Option<Vec<EmittedDetectorMetadata>>,
1008    /// Reused across nodes so per-node annotation vectors amortize capacity.
1009    annotations_scratch: StimCircuitAnnotations,
1010    /// Observable boundary logical operators grouped by their
1011    /// producing quantum node, then by observable index → (`operator_in`,
1012    /// `operator_out`). Emitted bracketing that node's circuit, so
1013    /// each Pauli face reads qubit state at the right temporal boundary.
1014    boundary_faces: FxHashMap<BloqNodeId, BTreeMap<u32, (PauliMap, PauliMap)>>,
1015}
1016
1017struct AlignedNodeEmission {
1018    /// The already-instantiated circuit, with its entry ops temporarily removed.
1019    circuit: CoordCircuit,
1020    moments: Vec<Vec<Op>>,
1021    annotations: StimCircuitAnnotations,
1022    ends_with_tick: bool,
1023    boundary_faces: BTreeMap<u32, (PauliMap, PauliMap)>,
1024}
1025
1026#[derive(Debug, Clone, Copy)]
1027enum AttemptMode {
1028    HonestT,
1029    SControl,
1030    Companion { cultivation: BloqNodeId },
1031}
1032
1033#[derive(Debug, Clone, Copy)]
1034struct AttemptRender<'a> {
1035    mode: AttemptMode,
1036    noise: &'a NoiseModel,
1037}
1038
1039impl AttemptRender<'_> {
1040    fn emits_observables(self) -> bool {
1041        matches!(self.mode, AttemptMode::Companion { .. })
1042    }
1043
1044    fn enables_clifford_proxy(self) -> bool {
1045        !matches!(self.mode, AttemptMode::HonestT)
1046    }
1047
1048    fn emits_postselection_tags(self) -> bool {
1049        matches!(self.mode, AttemptMode::Companion { .. })
1050    }
1051
1052    fn noises_node(self, node: BloqNodeId) -> bool {
1053        !matches!(self.mode, AttemptMode::Companion { cultivation } if cultivation == node)
1054    }
1055}
1056
1057#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1058struct EmittedDetectorMetadata {
1059    sign: bool,
1060    postselection: bool,
1061}
1062
1063struct AttemptStimRender {
1064    text: String,
1065    measurement_columns: FxHashMap<InstanceMeasurement, u32>,
1066    detectors: Vec<EmittedDetectorMetadata>,
1067}
1068
1069impl<'a> BloqStimEmitter<'a> {
1070    fn new(program: &'a Bloq, noise: Option<&'a NoiseModel>, plans: &'a ValidatedPlans) -> Self {
1071        Self::new_level(program, program.top(), LevelPath::default(), noise, plans)
1072    }
1073
1074    fn new_level(
1075        program: &'a Bloq,
1076        level: &'a SubGraph,
1077        path: LevelPath,
1078        noise: Option<&'a NoiseModel>,
1079        plans: &'a ValidatedPlans,
1080    ) -> Self {
1081        Self {
1082            program,
1083            level,
1084            path,
1085            noise,
1086            attempt: None,
1087            plans,
1088            output: String::new(),
1089            frame: MeasurementFrame::default(),
1090            num_measurements: 0,
1091            layout: QubitLayout::default(),
1092            indexed_coords: &[],
1093            resolved_loop_detector_states: ResolvedLoopStates::default(),
1094            instance_measurements: FxHashMap::default(),
1095            instance_templates: FxHashMap::default(),
1096            instance_loop_states: FxHashMap::default(),
1097            emitted_observables: FxHashSet::default(),
1098            detector_metadata: None,
1099            annotations_scratch: StimCircuitAnnotations::default(),
1100            boundary_faces: FxHashMap::default(),
1101        }
1102    }
1103
1104    fn new_attempt(
1105        program: &'a Bloq,
1106        level: &'a SubGraph,
1107        path: LevelPath,
1108        noise: &'a NoiseModel,
1109        mode: AttemptMode,
1110        plans: &'a ValidatedPlans,
1111    ) -> Self {
1112        let mut emitter = Self::new_level(program, level, path, None, plans);
1113        emitter.attempt = Some(AttemptRender { mode, noise });
1114        emitter.detector_metadata = Some(Vec::new());
1115        emitter
1116    }
1117
1118    fn prepare(&mut self) -> Result<(), StimEmissionError> {
1119        self.instance_templates = self
1120            .program
1121            .levels()
1122            .flat_map(|(_, level)| {
1123                level.quantum_nodes().flat_map(|(_, quantum)| {
1124                    quantum
1125                        .instances
1126                        .iter()
1127                        .map(|instance| (instance.id, instance.template_id))
1128                })
1129            })
1130            .collect();
1131        self.build_global_layout()?;
1132        self.build_global_measurements()?;
1133        self.build_global_loop_states()?;
1134        self.build_boundary_faces()?;
1135        self.output
1136            .reserve(estimate_bloq_stim_capacity(self.program, self.layout.len()));
1137        self.emit_qubit_coords();
1138        Ok(())
1139    }
1140
1141    fn emit_order_untransformed(
1142        mut self,
1143        order: &[BloqNodeId],
1144    ) -> Result<String, StimEmissionError> {
1145        self.prepare()?;
1146        for id in order {
1147            self.emit_node(*id, &self.level[*id])?;
1148        }
1149        Ok(self.output)
1150    }
1151
1152    fn emit_order_aligned(mut self, order: &[BloqNodeId]) -> Result<String, StimEmissionError> {
1153        self.prepare()?;
1154        let (lanes, mut nodes) = self.prepare_aligned_nodes(order)?;
1155
1156        for layer in align_moment_lanes(self.level, lanes)? {
1157            for slot in layer.slots {
1158                let mut completed = Vec::new();
1159                for entry in slot.entries {
1160                    let emission = nodes
1161                        .get_mut(&entry.node)
1162                        .expect("alignment returns a submitted node");
1163                    if self.emit_aligned_moment(entry.node, entry.moment, emission)? {
1164                        completed.push(entry.node);
1165                    }
1166                }
1167                self.output.push_str("TICK\n");
1168                let mut needs_boundary_tick = false;
1169                for &node_id in &completed {
1170                    let emission = nodes.get_mut(&node_id).expect("completed node is prepared");
1171                    if !emission.ends_with_tick {
1172                        continue;
1173                    }
1174                    needs_boundary_tick |=
1175                        self.emit_aligned_circuit(&emission.circuit, Some(&emission.annotations))?;
1176                }
1177                if needs_boundary_tick {
1178                    self.output.push_str("TICK\n");
1179                }
1180                for node_id in completed {
1181                    let emission = nodes.get(&node_id).expect("completed node is prepared");
1182                    for (index, (_, operator_out)) in &emission.boundary_faces {
1183                        self.emit_observable_pauli_line(*index, operator_out)?;
1184                    }
1185                }
1186            }
1187        }
1188
1189        // Record sinks can move after all quantum measurements. `prepare`
1190        // already attached boundary operators to their quantum nodes.
1191        for &node_id in order {
1192            if self.level[node_id].try_quantum().is_none() {
1193                self.emit_node(node_id, &self.level[node_id])?;
1194            }
1195        }
1196        Ok(self.output)
1197    }
1198
1199    fn prepare_aligned_nodes(
1200        &mut self,
1201        order: &[BloqNodeId],
1202    ) -> Result<(Vec<MomentLane>, FxHashMap<BloqNodeId, AlignedNodeEmission>), StimEmissionError>
1203    {
1204        let mut lanes = Vec::with_capacity(order.len());
1205        let mut nodes = FxHashMap::with_capacity_and_hasher(order.len(), Default::default());
1206
1207        for &node_id in order {
1208            let node = &self.level[node_id];
1209            let Some(quantum) = node.try_quantum() else {
1210                continue;
1211            };
1212            let plan =
1213                self.plans
1214                    .get(&self.path, node_id)
1215                    .ok_or(StimEmissionError::MalformedGraph(
1216                        "quantum node has no emission plan",
1217                    ))?;
1218            let mut circuit = remapped_plan_circuit(&mut self.instance_measurements, plan);
1219
1220            let mut annotations = StimCircuitAnnotations::default();
1221            node_annotations(
1222                &mut annotations,
1223                self.program,
1224                node,
1225                self.program.templates(),
1226                &self.instance_measurements,
1227                &self.instance_loop_states,
1228                NodeAnnotationOptions {
1229                    instance_templates: &self.instance_templates,
1230                    bodies: Some(&plan.bodies),
1231                    include_restarts: false,
1232                },
1233            )?;
1234
1235            let qubits = circuit.qubits().into_iter().collect();
1236            let entry = circuit.entry_body();
1237            let body = circuit
1238                .body_mut(entry)
1239                .expect("entry body is created with the circuit");
1240            let ops = std::mem::take(body.ops_mut());
1241            let ends_with_tick = matches!(ops.last(), Some(Op::Tick));
1242            let segments = aligned_moment_segments(&ops)?;
1243            let (kinds, moments): (Vec<_>, Vec<_>) = segments
1244                .into_iter()
1245                .map(|segment| {
1246                    segment.map_or((None, Vec::new()), |segment| {
1247                        (Some(segment.kind), segment.ops)
1248                    })
1249                })
1250                .unzip();
1251            let boundary_faces = self.boundary_faces.remove(&node_id).unwrap_or_default();
1252            if moments.is_empty() && quantum.timeline.is_none() {
1253                let has_faces = boundary_faces
1254                    .values()
1255                    .any(|(input, output)| !input.is_empty() || !output.is_empty());
1256                if !annotations.detectors.is_empty()
1257                    || !annotations.repeat_states.is_empty()
1258                    || has_faces
1259                {
1260                    return Err(StimEmissionError::AlignMomentsWithEmptyMetadata);
1261                }
1262                continue;
1263            }
1264            lanes.push(MomentLane {
1265                node: node_id,
1266                moments: kinds,
1267                qubits,
1268            });
1269            nodes.insert(
1270                node_id,
1271                AlignedNodeEmission {
1272                    circuit,
1273                    moments,
1274                    annotations,
1275                    ends_with_tick,
1276                    boundary_faces,
1277                },
1278            );
1279        }
1280        Ok((lanes, nodes))
1281    }
1282
1283    fn emit_aligned_moment(
1284        &mut self,
1285        node_id: BloqNodeId,
1286        moment: usize,
1287        emission: &mut AlignedNodeEmission,
1288    ) -> Result<bool, StimEmissionError> {
1289        let moment_count = emission.moments.len();
1290        let ops = emission
1291            .moments
1292            .get_mut(moment)
1293            .expect("alignment returns a submitted moment");
1294        let last = moment + 1 == moment_count;
1295        let finalize_now = last && !emission.ends_with_tick;
1296
1297        if moment == 0 {
1298            let node = &self.level[node_id];
1299            self.emit_node_comment(node_id, node);
1300            for (index, (operator_in, _)) in &emission.boundary_faces {
1301                self.emit_observable_pauli_line(*index, operator_in)?;
1302            }
1303        }
1304
1305        let entry = emission.circuit.entry_body();
1306        std::mem::swap(
1307            emission
1308                .circuit
1309                .body_mut(entry)
1310                .expect("entry body is created with the circuit")
1311                .ops_mut(),
1312            ops,
1313        );
1314        let result = self.emit_aligned_circuit(
1315            &emission.circuit,
1316            finalize_now.then_some(&emission.annotations),
1317        );
1318        std::mem::swap(
1319            emission
1320                .circuit
1321                .body_mut(entry)
1322                .expect("entry body is created with the circuit")
1323                .ops_mut(),
1324            ops,
1325        );
1326        result?;
1327
1328        Ok(last)
1329    }
1330
1331    fn emit_aligned_circuit(
1332        &mut self,
1333        circuit: &CoordCircuit,
1334        annotations: Option<&StimCircuitAnnotations>,
1335    ) -> Result<bool, CircuitError> {
1336        let mut mapper = PassthroughMeasurementMapper {
1337            num_measurements: self.num_measurements,
1338        };
1339        let mut ctx = StimEmitContext {
1340            layout: &self.layout,
1341            frame: &mut self.frame,
1342            resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1343            output: &mut self.output,
1344            indent: 0,
1345        };
1346        emit_stim_circuit(
1347            circuit,
1348            annotations,
1349            &mut ctx,
1350            &mut mapper,
1351            StimEmitOptions {
1352                emit_qubit_coords: false,
1353                ..StimEmitOptions::default()
1354            },
1355        )
1356    }
1357
1358    fn emit_order_attempt(
1359        mut self,
1360        order: &[BloqNodeId],
1361    ) -> Result<AttemptStimRender, StimEmissionError> {
1362        self.prepare()?;
1363        for id in order {
1364            self.emit_node(*id, &self.level[*id])?;
1365        }
1366        let mut measurement_columns = FxHashMap::with_capacity_and_hasher(
1367            self.instance_measurements.len(),
1368            Default::default(),
1369        );
1370        for (&measurement, &backend) in &self.instance_measurements {
1371            measurement_columns.insert(
1372                measurement,
1373                self.frame.resolve_absolute_measurement(backend)?,
1374            );
1375        }
1376        Ok(AttemptStimRender {
1377            text: self.output,
1378            measurement_columns,
1379            detectors: self
1380                .detector_metadata
1381                .take()
1382                .expect("attempt emitter owns detector metadata"),
1383        })
1384    }
1385
1386    /// Linearize into per-node Stim chunks sharing one global measurement frame
1387    /// (see [`BloqStimSegments`]). Unlike [`Self::emit_order_untransformed`], the
1388    /// node text is drained after each node so callers can inspect and align one
1389    /// node in isolation. `prepare` emits only the `QUBIT_COORDS` preamble, taken as the
1390    /// header; the frame and global maps persist across the drain so cross-node
1391    /// `rec[-k]` detector targets still resolve once reassembled.
1392    fn emit_order_segments(
1393        mut self,
1394        order: &[BloqNodeId],
1395    ) -> Result<BloqStimSegments, StimEmissionError> {
1396        self.prepare()?;
1397        let header = std::mem::take(&mut self.output);
1398        let mut segments = Vec::with_capacity(order.len());
1399        for &node_id in order {
1400            let measurement_start = self.frame.emitted_count() as usize;
1401            self.emit_node(node_id, &self.level[node_id])?;
1402            segments.push(StimSegment {
1403                node_id,
1404                text: std::mem::take(&mut self.output),
1405                measurement_start,
1406                measurement_count: self.frame.emitted_count() as usize - measurement_start,
1407            });
1408        }
1409        Ok(BloqStimSegments { header, segments })
1410    }
1411
1412    /// Emit one node at its position in the deterministic order.
1413    /// A quantum node instantiates its templates; an [`Observable`](ClassicalNode::Observable)
1414    /// emits its local records and records from its input fragments. Boundary
1415    /// operators are emitted at their quantum owners. Fragments and computations
1416    /// carry no standalone op; their records fold into their consuming observable. Regions and
1417    /// [`Discard`](ClassicalNode::Discard) are rejected by name when the walk reaches them.
1418    fn emit_node(&mut self, node_id: BloqNodeId, node: &BloqNode) -> Result<(), StimEmissionError> {
1419        // Prepend a provenance comment so a reader can trace each slice of the
1420        // circuit back to its source node. Many classical nodes emit nothing
1421        // (their contribution folds into an Observable elsewhere), so the
1422        // comment is rolled back if the node produced no text — no dangling
1423        // headers over empty regions.
1424        let comment_start = self.output.len();
1425        self.emit_node_comment(node_id, node);
1426        let after_comment = self.output.len();
1427
1428        match (node.try_classical(), &node.kind) {
1429            (_, BloqNodeKind::Quantum(_)) => {
1430                // Bracket the node with its boundary logical operators (IR spec
1431                // §4.7): input faces read state entering the node (before its
1432                // circuit), output faces read on exit (after). Removed (not
1433                // borrowed) so the map entry is owned here — each node id appears
1434                // once in the emit order, so we never need it again.
1435                let faces = self
1436                    .attempt
1437                    .is_none_or(AttemptRender::emits_observables)
1438                    .then(|| self.boundary_faces.remove(&node_id))
1439                    .flatten();
1440                if let Some(faces) = &faces {
1441                    for (index, (operator_in, _)) in faces {
1442                        self.emit_observable_pauli_line(*index, operator_in)?;
1443                    }
1444                }
1445                self.emit_instantiated_node(node_id, node)?;
1446                if let Some(faces) = &faces {
1447                    for (index, (_, operator_out)) in faces {
1448                        self.emit_observable_pauli_line(*index, operator_out)?;
1449                    }
1450                }
1451            }
1452            (
1453                Some(ClassicalNode::Observable {
1454                    index: Some(index), ..
1455                }),
1456                _,
1457            ) => {
1458                if self.attempt.is_none_or(AttemptRender::emits_observables) {
1459                    self.emit_observable_node(*index, node_id)?;
1460                }
1461            }
1462            (
1463                Some(ClassicalNode::Observable { index: None, .. } | ClassicalNode::Compute { .. }),
1464                _,
1465            ) => {}
1466            // Every other kind is un-emittable (`Discard`, regions), rejected up
1467            // front by `reject_unsupported` — the single source of truth for
1468            // emittability — so the walk never reaches them.
1469            _ => {
1470                debug_assert!(
1471                    stim_unsupported_reason(node).is_some(),
1472                    "unhandled emittable node kind"
1473                );
1474                unreachable!("un-emittable node kind passed reject_unsupported")
1475            }
1476        }
1477        if self.output.len() == after_comment {
1478            self.output.truncate(comment_start);
1479        }
1480        Ok(())
1481    }
1482
1483    /// Write a `# node <id>: <kind> <provenance>` header for the node about to
1484    /// be emitted. Rolled back by [`Self::emit_node`] if the node emits no text.
1485    fn emit_node_comment(&mut self, node_id: BloqNodeId, node: &BloqNode) {
1486        use std::fmt::Write as _;
1487        let kind: Cow<'static, str> = match (node.try_classical(), &node.kind) {
1488            (_, BloqNodeKind::Quantum(_)) => "quantum".into(),
1489            (
1490                Some(ClassicalNode::Observable {
1491                    index: Some(index), ..
1492                }),
1493                _,
1494            ) => format!("observable {index}").into(),
1495            (_, BloqNodeKind::Classical(_)) => "classical".into(),
1496            (_, BloqNodeKind::Region(_)) => "region".into(),
1497        };
1498        let _ = writeln!(
1499            self.output,
1500            "# node {}: {kind} {}",
1501            node_id.0, node.provenance
1502        );
1503    }
1504
1505    /// Emit canonical record parity at the observable's position. Boundary
1506    /// operators stay at their quantum owners. Static Stim uses a zero decoder
1507    /// estimate, ignoring the symbolic decoder flips and affine reference sign.
1508    fn emit_observable_node(
1509        &mut self,
1510        index: u32,
1511        node_id: BloqNodeId,
1512    ) -> Result<(), StimEmissionError> {
1513        let value = self
1514            .level
1515            .resolve_classical(node_id, ClassicalAssignment::Uniform(false))?;
1516        self.emit_observable_records(index, &value.measurements)?;
1517        if self.emitted_observables.insert(index) {
1518            self.output.push_str("OBSERVABLE_INCLUDE(");
1519            push_int(&mut self.output, index);
1520            self.output.push_str(")\n");
1521        }
1522        Ok(())
1523    }
1524
1525    /// Resolve an observable's named measurement sites to `rec[-k]` lookbacks
1526    /// against the global frame and emit them as one `OBSERVABLE_INCLUDE` line.
1527    /// A validated graph always names emitted measurements and reads them after
1528    /// their producing node (the read-after-measure `Order` edge); an unvalidated
1529    /// graph that breaks either invariant surfaces as [`MalformedGraph`](StimEmissionError::MalformedGraph).
1530    fn emit_observable_records(
1531        &mut self,
1532        index: u32,
1533        measurements: &BTreeSet<InstanceMeasurement>,
1534    ) -> Result<(), StimEmissionError> {
1535        let mut lookbacks = Vec::with_capacity(measurements.len());
1536        for measurement in measurements {
1537            let backend_id = *self.instance_measurements.get(measurement).ok_or(
1538                StimEmissionError::MalformedGraph(
1539                    "observable references an unemitted instance measurement",
1540                ),
1541            )?;
1542            lookbacks.push(self.frame.resolve_measurement(backend_id).map_err(
1543                |error| match error {
1544                    CircuitError::InvalidMeasurementId(_) => StimEmissionError::MalformedGraph(
1545                        "observable reads a measurement before its producing node",
1546                    ),
1547                    error => StimEmissionError::Circuit(error),
1548                },
1549            )?);
1550        }
1551        lookbacks.sort_unstable();
1552        crate::emit::emit_observable_include_records(&mut self.output, index, &lookbacks);
1553        if !lookbacks.is_empty() {
1554            self.emitted_observables.insert(index);
1555        }
1556        Ok(())
1557    }
1558
1559    /// Group inline and fragment boundary operators by their quantum owner,
1560    /// combining faces per observable index for bracketed emission.
1561    fn build_boundary_faces(&mut self) -> Result<(), StimEmissionError> {
1562        use bloq_ir::BoundaryFace;
1563
1564        let level = self.level;
1565
1566        // Plain record observables need no instance→node boundary map.
1567        let has_operators = level.nodes().any(|(_, node)| {
1568            matches!(
1569                node.try_classical(),
1570                Some(ClassicalNode::Observable { operators, .. })
1571                    if !operators.is_empty()
1572            )
1573        });
1574        if !has_operators {
1575            return Ok(());
1576        }
1577
1578        // Composition propagates owned boundaries by XOR path multiplicity.
1579        // Boolean value edges contribute parity only, never boundary operators.
1580        let mut consumers: FxHashMap<BloqNodeId, BTreeSet<u32>> = FxHashMap::default();
1581        for id in level.deterministic_emit_order()?.into_iter().rev() {
1582            let mut indices = BTreeSet::new();
1583            if let Some(ClassicalNode::Observable {
1584                index: Some(index), ..
1585            }) = level[id].try_classical()
1586            {
1587                indices.insert(*index);
1588            }
1589            for (consumer, _) in level.compose_consumers(id) {
1590                for index in consumers.get(&consumer).into_iter().flatten() {
1591                    if !indices.insert(*index) {
1592                        indices.remove(index);
1593                    }
1594                }
1595            }
1596            consumers.insert(id, indices);
1597        }
1598
1599        // Each template instance is produced by exactly one quantum node.
1600        let mut instance_node: FxHashMap<TemplateInstanceId, BloqNodeId> = FxHashMap::default();
1601        for (id, quantum) in level.quantum_nodes() {
1602            for instance in &quantum.instances {
1603                instance_node.insert(instance.id, id);
1604            }
1605        }
1606
1607        let mut faces: FxHashMap<BloqNodeId, BTreeMap<u32, (PauliMap, PauliMap)>> =
1608            FxHashMap::default();
1609        for (node, weight) in level.nodes() {
1610            let Some(ClassicalNode::Observable { operators, .. }) = weight.try_classical() else {
1611                continue;
1612            };
1613            let indices = &consumers[&node];
1614            if indices.is_empty() {
1615                continue;
1616            }
1617            // Each operator resolves its own producing node.
1618            for operator in operators {
1619                let producer = *instance_node.get(&operator.instance).ok_or(
1620                    StimEmissionError::MalformedGraph(
1621                        "observable boundary operator names an unproduced instance",
1622                    ),
1623                )?;
1624                for &index in indices {
1625                    let (operator_in, operator_out) =
1626                        faces.entry(producer).or_default().entry(index).or_default();
1627                    match operator.face {
1628                        BoundaryFace::Input => *operator_in = &*operator_in ^ &operator.operator,
1629                        BoundaryFace::Output => {
1630                            *operator_out = &*operator_out ^ &operator.operator;
1631                        }
1632                    }
1633                }
1634            }
1635        }
1636        self.boundary_faces = faces;
1637        Ok(())
1638    }
1639
1640    /// Emit one boundary logical operator face as a positioned, Pauli-target
1641    /// `OBSERVABLE_INCLUDE`. An empty face (cancelled at a seam or
1642    /// absent) contributes nothing. A qubit missing from the layout means the
1643    /// graph was never validated, so it surfaces as [`MalformedGraph`](StimEmissionError::MalformedGraph) like the
1644    /// rest of this path.
1645    fn emit_observable_pauli_line(
1646        &mut self,
1647        index: u32,
1648        operator: &bloq_circuit::PauliMap,
1649    ) -> Result<(), StimEmissionError> {
1650        if operator.is_empty() {
1651            return Ok(());
1652        }
1653        let output_start = self.output.len();
1654        crate::emit::emit_observable_include_pauli_targets(
1655            &mut self.output,
1656            index,
1657            operator,
1658            &self.layout,
1659        )
1660        .map_err(|_| {
1661            StimEmissionError::MalformedGraph(
1662                "boundary operator names a qubit outside the global qubit layout",
1663            )
1664        })?;
1665        if self.output.len() != output_start {
1666            self.emitted_observables.insert(index);
1667        }
1668        Ok(())
1669    }
1670
1671    fn build_global_layout(&mut self) -> Result<(), StimEmissionError> {
1672        let coords = self.program.sorted_layout_coords()?;
1673
1674        self.layout = QubitLayout::new(coordinate_index(coords.iter().copied())?)?;
1675        self.indexed_coords = coords;
1676        Ok(())
1677    }
1678
1679    /// Allocate backend measurement ids for every instance measurement.
1680    ///
1681    /// A dangling `template_id` is rejected here, up front — skipping it would
1682    /// defer the failure to a misleading lookup panic far from the defect —
1683    /// so the emit paths may treat every instance template as present.
1684    fn build_global_measurements(&mut self) -> Result<(), StimEmissionError> {
1685        // Measurement ids are allocated here from each quantum node's instances,
1686        // contiguously from 0. Classical / region nodes carry no instances, so
1687        // they are skipped here too — see the per-node emit walk in `emit_node`.
1688        let mut next_instance_measurement = 0u32;
1689        self.instance_measurements.clear();
1690        for (node_id, quantum) in self.level.quantum_nodes() {
1691            let templates = self
1692                .plans
1693                .get(&self.path, node_id)
1694                .map_or(self.program.templates(), |plan| {
1695                    plan.templates(self.program.templates())
1696                });
1697            for instance in &quantum.instances {
1698                let template = templates.get(instance.template_id).ok_or(
1699                    StimEmissionError::MalformedGraph("instance references a missing template"),
1700                )?;
1701                for record in template.circuit.meas_registry().records() {
1702                    self.instance_measurements.insert(
1703                        InstanceMeasurement {
1704                            instance: instance.id,
1705                            measurement: record.id,
1706                        },
1707                        next_instance_measurement,
1708                    );
1709                    next_instance_measurement = next_instance_measurement
1710                        .checked_add(1)
1711                        .expect("id space is u32; programs stay far below 2^32 measurements");
1712                }
1713            }
1714        }
1715        self.num_measurements = next_instance_measurement;
1716        // The frame's occurrence table is indexed by the same ids;
1717        // pre-sizing it avoids repeated reallocation during emission.
1718        self.frame
1719            .reserve_measurement_ids(next_instance_measurement as usize);
1720        Ok(())
1721    }
1722
1723    /// Allocate global loop-state ids. Rejects dangling `template_id`s for the
1724    /// same reason as [`Self::build_global_measurements`].
1725    fn build_global_loop_states(&mut self) -> Result<(), StimEmissionError> {
1726        self.instance_loop_states.clear();
1727        let mut next_state = 0;
1728        for (node_id, quantum) in self.level.quantum_nodes() {
1729            let templates = self
1730                .plans
1731                .get(&self.path, node_id)
1732                .map_or(self.program.templates(), |plan| {
1733                    plan.templates(self.program.templates())
1734                });
1735            for instance in &quantum.instances {
1736                let template = templates.get(instance.template_id).ok_or(
1737                    StimEmissionError::MalformedGraph("instance references a missing template"),
1738                )?;
1739                for state in &template.repeat_states {
1740                    let key = InstanceLoopState {
1741                        instance: instance.id,
1742                        state: state.state,
1743                    };
1744                    if self.instance_loop_states.contains_key(&key) {
1745                        continue;
1746                    }
1747                    self.instance_loop_states
1748                        .insert(key, LoopStateId(next_state));
1749                    next_state = next_state
1750                        .checked_add(1)
1751                        .expect("id space is u32; programs stay far below 2^32 loop states");
1752                }
1753            }
1754        }
1755        Ok(())
1756    }
1757
1758    fn emit_qubit_coords(&mut self) {
1759        // `indexed_coords` is the sorted global layout, so a slice position is
1760        // its backend qubit index and the pairs are already in index order.
1761        // Copy the shared slice out first so the borrow is independent of the
1762        // `&mut self.output` the helper writes into.
1763        let coords = self.indexed_coords;
1764        crate::emit::emit_qubit_coords_lines(
1765            &mut self.output,
1766            coords
1767                .iter()
1768                .enumerate()
1769                .map(|(index, coord)| (*coord, index as u32)),
1770        );
1771    }
1772
1773    fn collect_detector_metadata(&mut self) -> Result<(), StimEmissionError> {
1774        let Some(metadata) = &mut self.detector_metadata else {
1775            return Ok(());
1776        };
1777        if !self.annotations_scratch.repeat_states.is_empty()
1778            || self
1779                .annotations_scratch
1780                .detectors
1781                .iter()
1782                .any(|detector| detector.scope != StimAnnotationScope::TopLevel)
1783        {
1784            return Err(StimEmissionError::MalformedGraph(
1785                "isolated-T attempt must be flattened before detector indexing",
1786            ));
1787        }
1788        metadata.extend(self.annotations_scratch.detectors.iter().map(|detector| {
1789            EmittedDetectorMetadata {
1790                sign: detector.parity.sign(),
1791                postselection: detector.postselection,
1792            }
1793        }));
1794        Ok(())
1795    }
1796
1797    fn emit_instantiated_node(
1798        &mut self,
1799        node_id: BloqNodeId,
1800        node: &BloqNode,
1801    ) -> Result<(), StimEmissionError> {
1802        // Single instances emit their template circuit verbatim: there is nothing
1803        // to merge, and routing them through the merge would reshape the output
1804        // (e.g. inserting ticks around REPEAT blocks the template never had).
1805        if node.expect_quantum().instances.len() == 1 && self.noise.is_none() {
1806            return self.emit_single_instance_node(node_id, node);
1807        }
1808        let instantiated =
1809            self.plans
1810                .get(&self.path, node_id)
1811                .ok_or(StimEmissionError::MalformedGraph(
1812                    "quantum node has no emission plan",
1813                ))?;
1814        let circuit = remapped_plan_circuit(&mut self.instance_measurements, instantiated);
1815        node_annotations(
1816            &mut self.annotations_scratch,
1817            self.program,
1818            node,
1819            instantiated.templates(self.program.templates()),
1820            &self.instance_measurements,
1821            &self.instance_loop_states,
1822            NodeAnnotationOptions {
1823                instance_templates: &self.instance_templates,
1824                bodies: Some(&instantiated.bodies),
1825                include_restarts: self.attempt.is_some(),
1826            },
1827        )?;
1828        let mut mapper = PassthroughMeasurementMapper {
1829            num_measurements: self.num_measurements,
1830        };
1831        let mut ctx = StimEmitContext {
1832            layout: &self.layout,
1833            frame: &mut self.frame,
1834            resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1835            output: &mut self.output,
1836            indent: 0,
1837        };
1838        let needs_boundary_tick = emit_stim_circuit(
1839            &circuit,
1840            Some(&self.annotations_scratch),
1841            &mut ctx,
1842            &mut mapper,
1843            StimEmitOptions {
1844                enable_clifford_proxy: false,
1845                emit_qubit_coords: false,
1846                ..StimEmitOptions::default()
1847            },
1848        )?;
1849        self.collect_detector_metadata()?;
1850        if needs_boundary_tick {
1851            self.output.push_str("TICK\n");
1852        }
1853        Ok(())
1854    }
1855
1856    fn emit_single_instance_node(
1857        &mut self,
1858        node_id: BloqNodeId,
1859        node: &BloqNode,
1860    ) -> Result<(), StimEmissionError> {
1861        let instance = node
1862            .expect_quantum()
1863            .instances
1864            .first()
1865            .expect("single-instance path has one instance");
1866        let template = self
1867            .program
1868            .templates()
1869            .get(instance.template_id)
1870            .expect("prepare rejected instances referencing missing templates");
1871        // A single instance emits its template bodies verbatim, so every body
1872        // maps to itself; the identity mapping skips building (and hashing) a
1873        // per-node body map.
1874        node_annotations(
1875            &mut self.annotations_scratch,
1876            self.program,
1877            node,
1878            self.program.templates(),
1879            &self.instance_measurements,
1880            &self.instance_loop_states,
1881            NodeAnnotationOptions {
1882                instance_templates: &self.instance_templates,
1883                bodies: None,
1884                include_restarts: self.attempt.is_some(),
1885            },
1886        )?;
1887        let noisy_circuit = self
1888            .attempt
1889            .filter(|attempt| attempt.noises_node(node_id))
1890            .map(|attempt| {
1891                let ideal_qubits = if instance.provenance.is_spatial_port_substitution() {
1892                    template.qubits()
1893                } else {
1894                    &[]
1895                };
1896                attempt
1897                    .noise
1898                    .noisy_circuit_excluding(&template.circuit, ideal_qubits)
1899            })
1900            .transpose()?;
1901        let circuit = noisy_circuit.as_ref().unwrap_or(&template.circuit);
1902        let enable_clifford_proxy = self
1903            .attempt
1904            .is_some_and(AttemptRender::enables_clifford_proxy);
1905        let allow_non_clifford = matches!(
1906            self.attempt.map(|attempt| attempt.mode),
1907            Some(AttemptMode::HonestT)
1908        );
1909        let emit_postselection_tags = self
1910            .attempt
1911            .is_none_or(AttemptRender::emits_postselection_tags);
1912        let mut mapper = SingleInstanceMeasurementMapper {
1913            instance: instance.id,
1914            measurements: &self.instance_measurements,
1915            num_measurements: self.num_measurements,
1916        };
1917        let mut ctx = StimEmitContext {
1918            layout: &self.layout,
1919            frame: &mut self.frame,
1920            resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1921            output: &mut self.output,
1922            indent: 0,
1923        };
1924        let needs_boundary_tick = emit_stim_circuit(
1925            circuit,
1926            Some(&self.annotations_scratch),
1927            &mut ctx,
1928            &mut mapper,
1929            StimEmitOptions {
1930                enable_clifford_proxy,
1931                allow_non_clifford,
1932                emit_postselection_tags,
1933                emit_qubit_coords: false,
1934                qubit_offset: instance.offset,
1935            },
1936        )?;
1937        self.collect_detector_metadata()?;
1938        if needs_boundary_tick {
1939            self.output.push_str("TICK\n");
1940        }
1941        Ok(())
1942    }
1943}
1944
1945/// The first gate the static backend cannot emit, in level then node order, or
1946/// `None`.
1947///
1948/// Gate preflight recurses through every level (region bodies included) via
1949/// [`Bloq::levels`], because a non-Clifford gate can hide in a region body's
1950/// template. Node preflight ([`first_unsupported_program_node`]) scans only the
1951/// top level: any region there is rejected by name before its body is ever
1952/// inspected, so it never needs to descend.
1953///
1954/// A node carries many instances of one template, so each template circuit is
1955/// scanned at most once (memoized per [`TemplateId`]).
1956fn first_unsupported_program_gate(program: &Bloq) -> Option<GateType> {
1957    let templates = program.templates();
1958    let mut scanned: FxHashMap<TemplateId, Option<GateType>> = FxHashMap::default();
1959    for (_, level) in program.levels() {
1960        for (_, quantum) in level.quantum_nodes() {
1961            for instance in &quantum.instances {
1962                let unsupported = *scanned.entry(instance.template_id).or_insert_with(|| {
1963                    templates
1964                        .get(instance.template_id)
1965                        .and_then(|template| first_unsupported_gate(&template.circuit))
1966                });
1967                if unsupported.is_some() {
1968                    return unsupported;
1969                }
1970            }
1971        }
1972    }
1973    None
1974}
1975
1976/// Flat per-instruction output budget for the reserve estimate.
1977///
1978/// Walking every op and its qubit list to compute an exact reserve costs a
1979/// measurable fraction of emission itself, so the reserve uses op counts
1980/// only. Suite-wide averages sit near 90 bytes per op (dominated by gate
1981/// qubit lists and detector lookbacks); 128 leaves headroom so reallocation
1982/// stays rare at the cost of a modest over-reserve.
1983const STIM_BYTES_PER_OP: usize = 128;
1984
1985/// Clone a merged plan into the emitter-wide measurement namespace.
1986fn remapped_plan_circuit(
1987    instance_measurements: &mut FxHashMap<InstanceMeasurement, u32>,
1988    plan: &NodeEmissionPlan,
1989) -> CoordCircuit {
1990    let mut circuit = plan.circuit.clone();
1991    let mut local_measurements =
1992        FxHashMap::with_capacity_and_hasher(plan.measurements.len(), Default::default());
1993    // Each merged id uses the minimum preallocated id of its folded sources.
1994    for (local, sources) in plan.grouped_measurements() {
1995        let backend = sources
1996            .iter()
1997            .map(|source| {
1998                instance_measurements.get(source).copied().expect(
1999                    "build_global_measurements allocated an id for every instance measurement",
2000                )
2001            })
2002            .min()
2003            .expect("a grouped-measurement bucket is never empty");
2004        for source in sources {
2005            instance_measurements.insert(source, backend);
2006        }
2007        local_measurements.insert(local, backend);
2008    }
2009    circuit.remap_measurement_ids(&local_measurements);
2010    circuit
2011}
2012
2013fn estimate_bloq_stim_capacity(program: &Bloq, global_qubits: usize) -> usize {
2014    let qubit_coords = global_qubits * 24;
2015    let templates = program.templates();
2016    let node_bodies = program
2017        .quantum_nodes()
2018        .map(|(_, quantum)| {
2019            // A node's emitted text comes from each instance's template circuit,
2020            // so the reserve sums those; the node itself carries no ops. Classical
2021            // / region nodes emit nothing and so are skipped (filtered above).
2022            let body = quantum
2023                .instances
2024                .iter()
2025                .filter_map(|instance| templates.get(instance.template_id))
2026                .map(|template| estimate_circuit_stim_capacity(&template.circuit))
2027                .sum::<usize>();
2028            body + 8
2029        })
2030        .sum::<usize>();
2031    qubit_coords + node_bodies
2032}
2033
2034/// Estimate the emitted size of a node circuit from its op counts alone.
2035///
2036/// Repeat bodies live in the same circuit, so counting every body covers the
2037/// nesting without walking individual ops.
2038fn estimate_circuit_stim_capacity(circuit: &bloq_circuit::CoordCircuit) -> usize {
2039    (0..circuit.body_count())
2040        .map(|index| {
2041            circuit
2042                .body(BodyId(index as u32))
2043                .map_or(0, |body| body.ops().len())
2044        })
2045        .sum::<usize>()
2046        * STIM_BYTES_PER_OP
2047}
2048
2049/// A measurement id passes through unchanged when it is live: global ids are
2050/// allocated contiguously from 0, so any id below the total count exists.
2051/// Otherwise it refers to a measurement outside the emitted range and is rejected.
2052fn passthrough_live_measurement(
2053    measurement: u32,
2054    num_measurements: u32,
2055) -> Result<u32, bloq_circuit::CircuitError> {
2056    if measurement < num_measurements {
2057        return Ok(measurement);
2058    }
2059    Err(bloq_circuit::CircuitError::InvalidMeasurementId(
2060        measurement,
2061    ))
2062}
2063
2064/// Passes already-global measurement ids through unchanged, rejecting any id
2065/// not live in this node. Used by the multi-instance merge path, whose merged
2066/// circuit already carries backend-global ids.
2067struct PassthroughMeasurementMapper {
2068    num_measurements: u32,
2069}
2070
2071impl StimMeasurementMapper for PassthroughMeasurementMapper {
2072    fn map_measurement(&mut self, measurement: u32) -> Result<u32, bloq_circuit::CircuitError> {
2073        passthrough_live_measurement(measurement, self.num_measurements)
2074    }
2075}
2076
2077struct SingleInstanceMeasurementMapper<'a> {
2078    instance: TemplateInstanceId,
2079    measurements: &'a FxHashMap<InstanceMeasurement, u32>,
2080    num_measurements: u32,
2081}
2082
2083impl StimMeasurementMapper for SingleInstanceMeasurementMapper<'_> {
2084    fn map_measurement(&mut self, measurement: u32) -> Result<u32, bloq_circuit::CircuitError> {
2085        let measurement = InstanceMeasurement {
2086            instance: self.instance,
2087            measurement,
2088        };
2089        self.measurements.get(&measurement).copied().ok_or(
2090            bloq_circuit::CircuitError::InvalidMeasurementId(measurement.measurement),
2091        )
2092    }
2093
2094    fn map_annotation_measurement(
2095        &mut self,
2096        measurement: u32,
2097    ) -> Result<u32, bloq_circuit::CircuitError> {
2098        passthrough_live_measurement(measurement, self.num_measurements)
2099    }
2100}
2101
2102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2103struct InstanceLoopState {
2104    instance: TemplateInstanceId,
2105    state: LoopStateId,
2106}
2107
2108/// Rebuilds `out`'s side-table annotations for `node`, reusing its buffers.
2109///
2110/// One reusable [`StimCircuitAnnotations`] is threaded across every node so
2111/// the per-node detector and repeat-state vectors amortize their capacity.
2112struct NodeAnnotationOptions<'a> {
2113    instance_templates: &'a FxHashMap<TemplateInstanceId, TemplateId>,
2114    bodies: Option<&'a FxHashMap<(TemplateInstanceId, BodyId), BodyId>>,
2115    include_restarts: bool,
2116}
2117
2118fn node_annotations(
2119    out: &mut StimCircuitAnnotations,
2120    program: &Bloq,
2121    node: &BloqNode,
2122    templates: &bloq_ir::lowering::BloqTemplatePool,
2123    measurements: &FxHashMap<InstanceMeasurement, u32>,
2124    loop_states: &FxHashMap<InstanceLoopState, LoopStateId>,
2125    options: NodeAnnotationOptions<'_>,
2126) -> Result<(), StimEmissionError> {
2127    let NodeAnnotationOptions {
2128        instance_templates,
2129        bodies,
2130        include_restarts,
2131    } = options;
2132    let map_body = |instance, body| {
2133        bodies.map_or(body, |bodies| {
2134            bodies
2135                .get(&(instance, body))
2136                .copied()
2137                .expect("the emission plan maps every body of every merged instance")
2138        })
2139    };
2140    let StimCircuitAnnotations {
2141        detectors,
2142        repeat_states,
2143    } = out;
2144    detectors.clear();
2145    repeat_states.clear();
2146    let node = node.expect_quantum();
2147    program.check_detector_bundle_bindings(node, |instance| {
2148        instance_templates.get(&instance).copied()
2149    })?;
2150    for instance in &node.instances {
2151        let template = templates
2152            .get(instance.template_id)
2153            .expect("prepare rejected instances referencing missing templates");
2154        for detector in &template.detectors {
2155            let scope = match detector.scope {
2156                TemplateDetectorScope::TopLevel => StimAnnotationScope::TopLevel,
2157                TemplateDetectorScope::RepeatBody { body } => StimAnnotationScope::RepeatBody {
2158                    body: map_body(instance.id, body),
2159                },
2160            };
2161            detectors.push(StimDetector {
2162                scope,
2163                parity: remap_template_parity(
2164                    &detector.parity,
2165                    instance.id,
2166                    measurements,
2167                    loop_states,
2168                ),
2169                coords: detector
2170                    .coords
2171                    .as_ref()
2172                    .map(|coords| bloq_circuit::translate_detector_coords(coords, instance.offset)),
2173                postselection: false,
2174            });
2175        }
2176        if include_restarts {
2177            detectors.extend(template.restarts.iter().map(|restart| StimDetector {
2178                scope: StimAnnotationScope::TopLevel,
2179                parity: remap_template_parity(
2180                    &restart.parity,
2181                    instance.id,
2182                    measurements,
2183                    loop_states,
2184                ),
2185                coords: None,
2186                postselection: true,
2187            }));
2188        }
2189        for state in &template.repeat_states {
2190            let state_id = InstanceLoopState {
2191                instance: instance.id,
2192                state: state.state,
2193            };
2194            repeat_states.push(StimRepeatState {
2195                body: map_body(instance.id, state.body),
2196                state: loop_states
2197                    .get(&state_id)
2198                    .copied()
2199                    .expect("build_global_loop_states allocated an id for every instance state"),
2200                initial: remap_template_parity(
2201                    &state.initial,
2202                    instance.id,
2203                    measurements,
2204                    loop_states,
2205                ),
2206                next: remap_template_parity(&state.next, instance.id, measurements, loop_states),
2207            });
2208        }
2209    }
2210    for detector in program.node_detectors(node)? {
2211        detectors.push(StimDetector {
2212            scope: StimAnnotationScope::TopLevel,
2213            parity: remap_instance_parity(&detector.parity(), measurements)?,
2214            coords: detector.coords().map(Iterator::collect),
2215            postselection: false,
2216        });
2217    }
2218    if include_restarts {
2219        for restart in &node.restarts {
2220            detectors.push(StimDetector {
2221                scope: StimAnnotationScope::TopLevel,
2222                parity: remap_instance_parity(&restart.parity, measurements)?,
2223                coords: None,
2224                postselection: true,
2225            });
2226        }
2227    }
2228    repeat_states.sort_by_key(|state| (state.body, state.state));
2229    Ok(())
2230}
2231
2232fn remap_template_parity(
2233    parity: &bloq_ir::lowering::TemplateDetectorParity,
2234    instance: bloq_ir::lowering::TemplateInstanceId,
2235    measurements: &FxHashMap<InstanceMeasurement, u32>,
2236    loop_states: &FxHashMap<InstanceLoopState, LoopStateId>,
2237) -> bloq_circuit::DetectorParity {
2238    // `from_terms` collects the iterator into the parity's own (inline-capable)
2239    // term buffer, so feed it the mapped terms directly rather than through an
2240    // intermediate heap `Vec`.
2241    bloq_circuit::DetectorParity::from_terms(parity.terms().iter().copied().map(
2242        |term| match term {
2243            bloq_circuit::DetectorTerm::Measurement(measurement) => {
2244                let measurement = InstanceMeasurement {
2245                    instance,
2246                    measurement,
2247                };
2248                measurements
2249                    .get(&measurement)
2250                    .copied()
2251                    .map(bloq_circuit::DetectorTerm::Measurement)
2252                    .expect(
2253                        "build_global_measurements allocated an id for every instance measurement",
2254                    )
2255            }
2256            bloq_circuit::DetectorTerm::LoopState(state) => {
2257                let state = InstanceLoopState { instance, state };
2258                loop_states
2259                    .get(&state)
2260                    .copied()
2261                    .map(bloq_circuit::DetectorTerm::LoopState)
2262                    .expect("build_global_loop_states allocated an id for every instance state")
2263            }
2264        },
2265    ))
2266    .with_sign(parity.sign())
2267}
2268
2269fn remap_instance_parity(
2270    parity: &bloq_ir::NodeDetectorParity,
2271    measurements: &FxHashMap<InstanceMeasurement, u32>,
2272) -> Result<bloq_circuit::DetectorParity, StimEmissionError> {
2273    if let Some(state) = parity.terms().iter().find_map(|term| match term {
2274        bloq_circuit::DetectorTerm::LoopState(state) => Some(*state),
2275        bloq_circuit::DetectorTerm::Measurement(_) => None,
2276    }) {
2277        return Err(StimEmissionError::NodeLoopStateUnsupported(state));
2278    }
2279    parity.try_map_measurements(|measurement| {
2280        measurements
2281            .get(&measurement)
2282            .copied()
2283            .ok_or(StimEmissionError::UnknownInstanceMeasurement(measurement))
2284    })
2285}
2286
2287#[cfg(test)]
2288mod tests {
2289    use super::*;
2290    use bloq_circuit::{CircuitBody, DetectorParity, DetectorTerm, PauliBasis};
2291    use bloq_ir::{
2292        BloqEdge, BoundaryFace, ClassicalExpr, MemoryRoundTarget, NodeDetector, QuantumTimeline,
2293        SourceBlockRef, TemplateDetector,
2294        lowering::{BloqTemplate, InstanceBoundaryOperator, TemplateInstance, TemplateRepeatState},
2295    };
2296    use glam::{ivec2, ivec3};
2297
2298    fn compiled_isolated_t(distance: u32, latency: u32) -> Bloq {
2299        let graph = bloq_graph::parse_blog_to_graph(
2300            "BLOG 1.0\n\n  0: T [0, 0, 0]\n  1: Port [0, 0, 1]\n  [0, 0, 0] -> +Z\n",
2301        )
2302        .expect("isolated T BLOG parses");
2303        let mut program =
2304            bloq_compile::CompileContext::new(bloq_compile::CompileConfig::new(distance))
2305                .compile(&graph)
2306                .expect("isolated T compiles")
2307                .bloq;
2308        if latency != 0 {
2309            let path = isolated_rus_path(&program).expect("RUS path");
2310            let terminal = program
2311                .level_at(&path)
2312                .expect("RUS body")
2313                .deterministic_emit_order()
2314                .expect("body order")
2315                .into_iter()
2316                .rev()
2317                .find(|&node| {
2318                    program
2319                        .level_at(&path)
2320                        .expect("RUS body")
2321                        .node(node)
2322                        .is_some_and(|node| node.try_quantum().is_some())
2323                })
2324                .expect("escape stage");
2325            program
2326                .insert_memory_rounds(
2327                    MemoryRoundTarget::After {
2328                        path,
2329                        node: terminal,
2330                    },
2331                    latency,
2332                )
2333                .expect("append decoder latency");
2334        }
2335        program
2336    }
2337
2338    #[test]
2339    fn isolated_t_frame_inversions_preserve_columns_and_change_probe_corrections() {
2340        let program = compiled_isolated_t(3, 0);
2341        let baseline = emit_isolated_t_attempts(&program, 0.0).unwrap();
2342        let probe_signs = |manifest: &IsolatedTAttemptManifest| {
2343            let x = manifest.frame_x.sign;
2344            let z = manifest.frame_z.sign;
2345            std::array::from_fn::<_, 3, _>(|axis| {
2346                manifest.exp_val_signs[axis] * if [z, x ^ z, x][axis] { -1 } else { 1 }
2347            })
2348        };
2349        for invert_x in [true, false] {
2350            let mut changed = program.clone();
2351            let frames = changed.output_frames();
2352            let frame = if invert_x { frames[0].x } else { frames[0].z };
2353            let Some(ClassicalNode::Compute { expr }) =
2354                changed.node_mut(frame).unwrap().try_classical_mut()
2355            else {
2356                panic!("output frame is a Compute");
2357            };
2358            *expr = ClassicalExpr::Not(Box::new(expr.clone()));
2359            changed.validate().unwrap();
2360            let artifacts = emit_isolated_t_attempts(&changed, 0.0).unwrap();
2361            let mut expected = baseline.clone();
2362            if invert_x {
2363                expected.manifest.frame_x.sign ^= true;
2364            } else {
2365                expected.manifest.frame_z.sign ^= true;
2366            }
2367            assert_eq!(artifacts, expected);
2368            let before = probe_signs(&baseline.manifest);
2369            let after = probe_signs(&artifacts.manifest);
2370            let multipliers = if invert_x { [1, -1, -1] } else { [-1, -1, 1] };
2371            assert_eq!(
2372                after,
2373                std::array::from_fn(|axis| before[axis] * multipliers[axis])
2374            );
2375        }
2376    }
2377
2378    #[test]
2379    fn isolated_t_attempt_emits_aligned_physical_and_causal_circuits() {
2380        let artifacts = emit_isolated_t_attempts(&compiled_isolated_t(3, 2), 1e-3)
2381            .expect("emit isolated T attempt");
2382        let detector_count = |text: &str| {
2383            text.lines()
2384                .filter(|line| line.starts_with("DETECTOR"))
2385                .count()
2386        };
2387        let exp_count = |text: &str| {
2388            text.lines()
2389                .filter(|line| line.starts_with("EXP_VAL "))
2390                .count()
2391        };
2392
2393        assert!(artifacts.physical_t.contains("T_DAG"));
2394        assert!(!artifacts.physical_s.contains("T_DAG"));
2395        assert!(artifacts.physical_s.contains("S_DAG"));
2396        assert_eq!(exp_count(&artifacts.physical_s), 3);
2397        assert_eq!(exp_count(&artifacts.physical_t), 3);
2398        assert_eq!(exp_count(&artifacts.companion), 0);
2399        assert!(!artifacts.physical_s.contains("OBSERVABLE_INCLUDE"));
2400        assert!(!artifacts.physical_t.contains("OBSERVABLE_INCLUDE"));
2401        assert!(artifacts.companion.contains("OBSERVABLE_INCLUDE"));
2402        let noise_count = |text: &str| {
2403            text.lines()
2404                .filter(|line| line.starts_with("DEPOLARIZE") || line.contains("_ERROR("))
2405                .count()
2406        };
2407        assert_eq!(
2408            noise_count(&artifacts.physical_s),
2409            noise_count(&artifacts.physical_t)
2410        );
2411        assert!(noise_count(&artifacts.physical_s) > noise_count(&artifacts.companion));
2412        assert!(noise_count(&artifacts.companion) > 0);
2413
2414        let expected_detectors = artifacts.manifest.detector_signs.len();
2415        assert_eq!(detector_count(&artifacts.physical_s), expected_detectors);
2416        assert_eq!(detector_count(&artifacts.physical_t), expected_detectors);
2417        assert_eq!(detector_count(&artifacts.companion), expected_detectors);
2418        let sheeted = format!("{}{}", artifacts.companion, artifacts.sheets);
2419        assert_eq!(detector_count(&sheeted), expected_detectors);
2420        assert_eq!(
2421            artifacts
2422                .companion
2423                .lines()
2424                .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2425                .count(),
2426            artifacts.manifest.postselection_detectors.len()
2427        );
2428        assert!(!artifacts.physical_s.contains("DETECTOR[POST-SELECTION]"));
2429        assert!(!artifacts.physical_t.contains("DETECTOR[POST-SELECTION]"));
2430        assert_eq!(
2431            sheeted
2432                .lines()
2433                .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2434                .count(),
2435            artifacts.manifest.postselection_detectors.len()
2436        );
2437        // The split is only sound if reassembly is exact: the suffix must add
2438        // observables and nothing else.
2439        assert!(
2440            artifacts
2441                .sheets
2442                .lines()
2443                .all(|line| line.starts_with("OBSERVABLE_INCLUDE(")),
2444            "sheets suffix carries only observables: {}",
2445            artifacts.sheets
2446        );
2447        assert!(!artifacts.manifest.postselection_detectors.is_empty());
2448        assert_eq!(artifacts.manifest.frontier_sheets.len(), 8);
2449        assert_eq!(artifacts.manifest.exp_val_signs, [1, 1, 1]);
2450        assert_eq!(
2451            artifacts.manifest.frame_x.gap_observable,
2452            artifacts.manifest.gap_z_observable
2453        );
2454        assert_eq!(
2455            artifacts.manifest.frame_z.gap_observable,
2456            artifacts.manifest.gap_x_observable
2457        );
2458    }
2459
2460    #[test]
2461    fn isolated_t_attempt_matches_d5_m10_causal_cut() {
2462        let artifacts = emit_isolated_t_attempts(&compiled_isolated_t(5, 10), 1e-3)
2463            .expect("emit d5/m10 isolated T attempt");
2464        let measurement_count = artifacts
2465            .physical_t
2466            .lines()
2467            .filter_map(|line| {
2468                let mut words = line.split_whitespace();
2469                let gate = words.next()?.split('(').next()?;
2470                matches!(gate, "M" | "MX" | "MY" | "MPP").then(|| words.count())
2471            })
2472            .sum::<usize>();
2473        let detector_count = artifacts
2474            .physical_t
2475            .lines()
2476            .filter(|line| line.starts_with("DETECTOR"))
2477            .count();
2478
2479        assert_eq!(measurement_count, 448);
2480        // 35 post-selection checks: the six first-round/temporal surface-band
2481        // parities are ordinary decoder detectors, not restart conditions.
2482        assert_eq!(detector_count, 424);
2483        assert_eq!(artifacts.manifest.detector_signs.len(), 424);
2484        assert_eq!(artifacts.manifest.postselection_detectors.len(), 35);
2485        assert_eq!(
2486            artifacts
2487                .companion
2488                .lines()
2489                .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2490                .count(),
2491            35
2492        );
2493        assert!(!artifacts.physical_s.contains("DETECTOR[POST-SELECTION]"));
2494        assert!(!artifacts.physical_t.contains("DETECTOR[POST-SELECTION]"));
2495        assert_eq!(artifacts.manifest.frontier_sheets.len(), 24);
2496        assert_ne!(
2497            artifacts.manifest.gap_z_observable,
2498            artifacts.manifest.gap_x_observable
2499        );
2500        assert_eq!(
2501            artifacts.manifest.frame_x.measurements,
2502            [46, 95, 97, 112, 114, 116, 118, 120, 122]
2503        );
2504        assert_eq!(
2505            artifacts.manifest.frame_z.measurements,
2506            [15, 17, 18, 124, 125, 126, 127, 128, 129]
2507        );
2508        assert!(
2509            artifacts
2510                .manifest
2511                .frontier_sheets
2512                .iter()
2513                .all(|sheet| !sheet.sign)
2514        );
2515    }
2516
2517    #[test]
2518    fn isolated_t_attempt_rejects_invalid_noise_probability() {
2519        let program = Bloq::new();
2520        for probability in [f64::NAN, f64::INFINITY, -0.1, 1.1] {
2521            assert_eq!(
2522                emit_isolated_t_attempts(&program, probability),
2523                Err(StimEmissionError::InvalidNoiseProbability)
2524            );
2525        }
2526    }
2527
2528    #[test]
2529    fn signed_logical_y_keeps_global_phase() {
2530        let coord = ivec2(0, 0);
2531        let x = PauliMap::from_unique_entries([(coord, Pauli::Z)]);
2532        let z = PauliMap::from_unique_entries([(coord, Pauli::X)]);
2533        let (y, sign) = signed_logical_y(&x, &z).expect("iZX is Hermitian");
2534
2535        assert_eq!(y.get(&coord), Some(&Pauli::Y));
2536        assert_eq!(sign, -1);
2537    }
2538
2539    #[test]
2540    fn emit_bloq_stim_rejects_repeat_until_success_by_name() {
2541        // A `RepeatUntilSuccess` is postselected retry the static backend cannot
2542        // emit, so it is rejected by name.
2543        let mut bloq = Bloq::new();
2544        let body = SubGraph::new();
2545        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2546            body,
2547            restart_condition: ClassicalExpr::Const(false),
2548            restart_source: None,
2549        }));
2550        assert_eq!(
2551            emit_bloq_stim(&bloq),
2552            Err(StimEmissionError::UnsupportedNode(
2553                "RepeatUntilSuccess (postselected retry)"
2554            ))
2555        );
2556    }
2557
2558    #[test]
2559    fn emit_bloq_stim_folds_observable_outputs_but_rejects_activation() {
2560        // A complete observable's static flip estimate is zero. Consumers of
2561        // its corrected output or structural composition therefore receive the
2562        // same measurement recipe; its flip output contributes no records.
2563        let mut graph = Bloq::new();
2564        let node = measured_program_node(
2565            &mut graph,
2566            ivec3(0, 0, 0),
2567            ivec2(0, 0),
2568            TemplateInstanceId(0),
2569        );
2570        let quantum = graph.add_node(node);
2571        let read_observable = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
2572            index: Some(0),
2573            measurements: vec![InstanceMeasurement {
2574                instance: TemplateInstanceId(0),
2575                measurement: 0,
2576            }],
2577            operators: vec![],
2578        }));
2579        graph.add_edge(quantum, read_observable, BloqEdge::Order);
2580        let corrected_read = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2581        graph.add_edge(read_observable, corrected_read, BloqEdge::value(0));
2582        let composed = graph.add_node(BloqNode::classical(ClassicalNode::observable(2)));
2583        graph.add_edge(read_observable, composed, BloqEdge::compose(0));
2584        let flipped = graph.add_node(BloqNode::classical(ClassicalNode::observable(3)));
2585        graph.add_edge(read_observable, flipped, BloqEdge::flip(0));
2586
2587        let text = emit_bloq_stim(&graph).expect("observable outputs reduce to records");
2588        for index in [0, 1, 2] {
2589            assert!(
2590                text.contains(&format!("OBSERVABLE_INCLUDE({index}) rec[-1]")),
2591                "observable output should fold to its physical record: {text}"
2592            );
2593        }
2594
2595        assert!(text.contains("OBSERVABLE_INCLUDE(3)\n"), "{text}");
2596        assert!(!text.contains("OBSERVABLE_INCLUDE(3) rec"), "{text}");
2597
2598        graph.node_mut(corrected_read).unwrap().activation = Some(1);
2599        graph.add_edge(read_observable, corrected_read, BloqEdge::value(1));
2600        assert!(!graph.has_conditional_membership());
2601        assert_eq!(
2602            emit_bloq_stim(&graph),
2603            Err(StimEmissionError::UnsupportedNode(
2604                "activation (conditional execution)"
2605            ))
2606        );
2607    }
2608
2609    #[test]
2610    fn complete_observable_emits_inline_records_and_owned_boundary_faces() {
2611        let qubit = ivec2(0, 0);
2612        let instance = TemplateInstanceId(0);
2613        let mut graph = Bloq::new();
2614        let quantum = measured_program_node(&mut graph, ivec3(0, 0, 0), qubit, instance);
2615        let quantum = graph.add_node(quantum);
2616        let operator = [(qubit, Pauli::Z)].into_iter().collect::<PauliMap>();
2617        let observable = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
2618            index: Some(3),
2619            measurements: vec![
2620                InstanceMeasurement {
2621                    instance,
2622                    measurement: 0
2623                };
2624                3
2625            ],
2626            operators: vec![
2627                InstanceBoundaryOperator {
2628                    instance,
2629                    face: BoundaryFace::Input,
2630                    operator: operator.clone(),
2631                },
2632                InstanceBoundaryOperator {
2633                    instance,
2634                    face: BoundaryFace::Output,
2635                    operator: operator.clone(),
2636                },
2637            ],
2638        }));
2639        graph.add_edge(quantum, observable, BloqEdge::Order);
2640
2641        assert_eq!(
2642            observable_boundary_basis(graph.top(), observable),
2643            Ok(Basis::Z)
2644        );
2645        for aligned in [false, true] {
2646            let text =
2647                emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(aligned))
2648                    .unwrap();
2649            let input = text.find("OBSERVABLE_INCLUDE(3) Z0").unwrap();
2650            let measurement = text.find("M 0").unwrap();
2651            let output = text.rfind("OBSERVABLE_INCLUDE(3) Z0").unwrap();
2652            assert!(input < measurement && measurement < output, "{text}");
2653            assert!(text.contains("OBSERVABLE_INCLUDE(3) rec[-1]"), "{text}");
2654        }
2655
2656        // A shared fragment and inline binding at the same owned face cancel.
2657        let include = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2658            vec![],
2659            vec![InstanceBoundaryOperator {
2660                instance,
2661                face: BoundaryFace::Output,
2662                operator,
2663            }],
2664        )));
2665        graph.add_edge(include, observable, BloqEdge::compose(0));
2666        let text = emit_bloq_stim(&graph).unwrap();
2667        assert_eq!(
2668            text.matches("OBSERVABLE_INCLUDE(3) Z0").count(),
2669            1,
2670            "{text}"
2671        );
2672        assert!(text.find("OBSERVABLE_INCLUDE(3) Z0").unwrap() < text.find("M 0").unwrap());
2673    }
2674
2675    #[test]
2676    fn emit_bloq_stim_rejects_discard_node() {
2677        // A `Discard` is shot postselection the static backend cannot express, so
2678        // it is rejected up front rather than silently dropped (IR spec §6).
2679        let mut bloq = Bloq::new();
2680        bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2681            condition: ClassicalExpr::Const(true),
2682        }));
2683        assert_eq!(
2684            emit_bloq_stim(&bloq),
2685            Err(StimEmissionError::UnsupportedNode(
2686                "Discard (shot postselection)"
2687            ))
2688        );
2689    }
2690
2691    #[test]
2692    fn emit_bloq_stim_skips_records_reducible_classical_node() {
2693        // A records-reducible classical node passes the feature gate (no producer
2694        // emits it yet) and must be skipped on the static path, not panic the
2695        // global pre-passes in prepare() by calling quantum() on it.
2696        let mut bloq = Bloq::new();
2697        let input = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2698            vec![],
2699            vec![],
2700        )));
2701        let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2702            expr: ClassicalExpr::In(0),
2703        }));
2704        bloq.add_edge(input, compute, BloqEdge::value(0));
2705        emit_bloq_stim(&bloq).unwrap();
2706    }
2707
2708    #[test]
2709    fn emit_bloq_stim_audits_only_when_requested() {
2710        let mut bloq = Bloq::new();
2711        bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2712            expr: ClassicalExpr::In(0),
2713        }));
2714
2715        // The invalid classical input is irrelevant to the circuit emitted
2716        // here, but the full T2 audit must still catch it when requested.
2717        emit_bloq_stim(&bloq).unwrap();
2718        emit_bloq_stim_segments(&bloq).unwrap();
2719        emit_bloq_stim_with(&bloq, &BloqStimOptions::new().with_align_moments(true)).unwrap();
2720
2721        assert!(matches!(
2722            emit_bloq_stim_with(
2723                &bloq,
2724                &BloqStimOptions::new().with_trust(InputTrust::Checked)
2725            ),
2726            Err(StimEmissionError::InvalidProgram(
2727                bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. }
2728            ))
2729        ));
2730        assert!(matches!(
2731            emit_bloq_stim_with(
2732                &bloq,
2733                &BloqStimOptions::new()
2734                    .with_trust(InputTrust::Checked)
2735                    .with_align_moments(true)
2736            ),
2737            Err(StimEmissionError::InvalidProgram(
2738                bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. }
2739            ))
2740        ));
2741    }
2742
2743    #[test]
2744    fn static_emission_rejects_unpinned_membership_before_building_plans() {
2745        let mut bloq = Bloq::new();
2746        let mut node = BloqNode::from_members(Vec::new());
2747        node.expect_quantum_mut()
2748            .guards
2749            .push(bloq_ir::QuantumGuard {
2750                input: 0,
2751                ..Default::default()
2752            });
2753        bloq.add_node(node);
2754
2755        assert_eq!(
2756            emit_bloq_stim(&bloq),
2757            Err(StimEmissionError::UnsupportedNode(
2758                "conditional component membership"
2759            ))
2760        );
2761        assert_eq!(
2762            emit_bloq_stim_segments(&bloq).unwrap_err(),
2763            StimEmissionError::UnsupportedNode("conditional component membership")
2764        );
2765    }
2766
2767    #[test]
2768    fn aligned_emission_rejects_noise_and_segments() {
2769        let bloq = Bloq::new();
2770        let noise = NoiseModel::uniform_depolarizing(1e-3);
2771        let options = BloqStimOptions::new().with_align_moments(true);
2772
2773        assert_eq!(
2774            emit_bloq_stim_with(&bloq, &options.with_noise(&noise)),
2775            Err(StimEmissionError::AlignMomentsWithNoise)
2776        );
2777        assert!(matches!(
2778            emit_bloq_stim_segments_with(&bloq, &options),
2779            Err(StimEmissionError::AlignMomentsWithSegments)
2780        ));
2781    }
2782
2783    #[test]
2784    fn attempt_emission_preserves_typed_flatten_resource_errors() {
2785        let mut circuit = CoordCircuit::new();
2786        let body = circuit.add_body(CircuitBody::from_ops(vec![bloq_circuit::Op::Tick]));
2787        circuit.push_repeat(body, u32::MAX);
2788        let mut program = Bloq::new();
2789        let template = program.add_template(BloqTemplate::new(circuit));
2790        let mut node = BloqNode::from_members(Vec::new());
2791        node.expect_quantum_mut()
2792            .instances
2793            .push(TemplateInstance::new(
2794                TemplateInstanceId(0),
2795                template,
2796                ivec2(0, 0),
2797            ));
2798        program.add_node(node);
2799
2800        let error = emit_isolated_t_attempts(&program, 0.0).unwrap_err();
2801        assert!(std::error::Error::source(&error).is_some());
2802        assert!(matches!(error, StimEmissionError::Flatten(error) if error.is_resource_limited()));
2803    }
2804
2805    #[test]
2806    fn emit_bloq_stim_validates_before_capability_checks() {
2807        let mut bloq = Bloq::new();
2808        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2809            restart_condition: ClassicalExpr::In(0),
2810            restart_source: None,
2811            body: SubGraph::new(),
2812        }));
2813
2814        assert!(matches!(
2815            emit_bloq_stim_with(
2816                &bloq,
2817                &BloqStimOptions::new().with_trust(InputTrust::Checked)
2818            ),
2819            Err(StimEmissionError::InvalidProgram(
2820                bloq_ir::BloqValidationError::MissingRegionSelectorInput { slot: 0, .. }
2821            ))
2822        ));
2823        assert!(matches!(
2824            emit_bloq_stim_segments_with(
2825                &bloq,
2826                &BloqStimOptions::new().with_trust(InputTrust::Checked)
2827            ),
2828            Err(StimEmissionError::InvalidProgram(
2829                bloq_ir::BloqValidationError::MissingRegionSelectorInput { slot: 0, .. }
2830            ))
2831        ));
2832    }
2833
2834    #[test]
2835    fn emit_bloq_stim_rejects_nonlinear_compute() {
2836        // A nonlinear (`And`/`Or`) feedback condition is not XOR-expressible in
2837        // `OBSERVABLE_INCLUDE`, so the static backend rejects it up front (it needs
2838        // conditional execution, Phase U6/U7). Linear conditions fold instead.
2839        let mut bloq = Bloq::new();
2840        let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2841            expr: ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2842        }));
2843        // Wire dummy inputs so the node is a genuine two-input `And`.
2844        let a = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2845            vec![],
2846            vec![],
2847        )));
2848        let b = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2849            vec![],
2850            vec![],
2851        )));
2852        bloq.add_edge(a, compute, BloqEdge::value(0));
2853        bloq.add_edge(b, compute, BloqEdge::value(1));
2854        assert_eq!(
2855            emit_bloq_stim(&bloq),
2856            Err(StimEmissionError::UnsupportedNode(
2857                "Compute (nonlinear And/Or condition requires conditional execution)"
2858            ))
2859        );
2860    }
2861
2862    #[test]
2863    fn shared_classical_dag_emits_canonical_record_parity() {
2864        let mut graph = Bloq::new();
2865        let node = measured_program_node(
2866            &mut graph,
2867            ivec3(0, 0, 0),
2868            ivec2(0, 0),
2869            TemplateInstanceId(0),
2870        );
2871        let quantum = graph.add_node(node);
2872        let measured = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2873            vec![InstanceMeasurement {
2874                instance: TemplateInstanceId(0),
2875                measurement: 0,
2876            }],
2877            vec![],
2878        )));
2879        graph.add_edge(quantum, measured, BloqEdge::Order);
2880        let mut previous = measured;
2881        for _ in 0..25 {
2882            let next = graph.add_node(BloqNode::classical(ClassicalNode::Compute {
2883                expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2884            }));
2885            graph.add_edge(previous, next, BloqEdge::value(0));
2886            graph.add_edge(previous, next, BloqEdge::value(1));
2887            previous = next;
2888        }
2889        let zero = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2890        graph.add_edge(previous, zero, BloqEdge::value(0));
2891        let nonzero = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2892        graph.add_edge(previous, nonzero, BloqEdge::value(0));
2893        graph.add_edge(measured, nonzero, BloqEdge::value(1));
2894        let text = emit_bloq_stim(&graph).unwrap();
2895        let observables = text
2896            .lines()
2897            .filter(|line| line.starts_with("OBSERVABLE_INCLUDE"))
2898            .collect::<Vec<_>>();
2899        assert_eq!(
2900            observables,
2901            ["OBSERVABLE_INCLUDE(0)", "OBSERVABLE_INCLUDE(1) rec[-1]"]
2902        );
2903    }
2904
2905    #[test]
2906    fn emit_bloq_stim_folds_linear_compute_into_observable() {
2907        // A linear feedback condition folds into the affected observable: the
2908        // `Not(In0)` `Compute` over a measurement fragment, routed into
2909        // observable 1, contributes that measurement's record to
2910        // `OBSERVABLE_INCLUDE(1)` (the `Not` sign is dropped — Stim is
2911        // sign-agnostic). This exercises the recursive fold through a `Compute`.
2912        let mut graph = Bloq::new();
2913        let parent_node = measured_program_node(
2914            &mut graph,
2915            ivec3(0, 0, 0),
2916            ivec2(0, 0),
2917            TemplateInstanceId(1),
2918        );
2919        let qubit = ivec2(0, 0);
2920        let mut template_circuit = CoordCircuit::new();
2921        template_circuit.measure(PauliBasis::Z, [qubit]);
2922        let template = graph.add_template(BloqTemplate::new(template_circuit));
2923        let mut node = BloqNode::from_members(vec![SourceBlockRef {
2924            pos: ivec3(0, 0, 0),
2925        }]);
2926        node.expect_quantum_mut()
2927            .instances
2928            .push(TemplateInstance::new(
2929                TemplateInstanceId(0),
2930                template,
2931                ivec2(1, 0),
2932            ));
2933        let parent = graph.add_node(parent_node);
2934        let child = graph.add_node(node);
2935        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
2936
2937        let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2938            vec![InstanceMeasurement {
2939                instance: TemplateInstanceId(0),
2940                measurement: 0,
2941            }],
2942            vec![],
2943        )));
2944        graph.add_edge(child, accumulate, BloqEdge::Order);
2945
2946        // The feedback condition `!m` lowers to a `Not` `Compute` over the
2947        // measurement, routed into a downstream observable it perturbs.
2948        let compute = graph.add_node(BloqNode::classical(ClassicalNode::Compute {
2949            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2950        }));
2951        graph.add_edge(accumulate, compute, BloqEdge::value(0));
2952        let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2953        graph.add_edge(compute, observable, BloqEdge::value(0));
2954
2955        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
2956        assert!(
2957            text.contains("OBSERVABLE_INCLUDE(1) rec[-1]"),
2958            "linear Compute condition should fold its measurement record into the observable: {text}"
2959        );
2960    }
2961
2962    /// A template-backed node whose single instance measures `qubit` in Z.
2963    ///
2964    /// Each call registers its own template and must be given a graph-unique
2965    /// `instance_id`; the template measures at the origin and the instance is
2966    /// offset to `qubit`.
2967    fn measured_program_node(
2968        graph: &mut Bloq,
2969        block_pos: glam::IVec3,
2970        qubit: glam::IVec2,
2971        instance_id: TemplateInstanceId,
2972    ) -> BloqNode {
2973        let mut circuit = CoordCircuit::new();
2974        circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
2975        template_node_with_circuit_at(graph, circuit, block_pos, instance_id, qubit)
2976    }
2977
2978    fn template_node_with_circuit(graph: &mut Bloq, circuit: CoordCircuit) -> BloqNode {
2979        template_node_with_circuit_at(
2980            graph,
2981            circuit,
2982            ivec3(0, 0, 0),
2983            TemplateInstanceId(0),
2984            IVec2::ZERO,
2985        )
2986    }
2987
2988    fn template_node_with_circuit_at(
2989        graph: &mut Bloq,
2990        circuit: CoordCircuit,
2991        block_pos: glam::IVec3,
2992        instance_id: TemplateInstanceId,
2993        offset: IVec2,
2994    ) -> BloqNode {
2995        let template = graph.add_template(BloqTemplate::new(circuit));
2996        let mut node = BloqNode::from_members(vec![SourceBlockRef { pos: block_pos }]);
2997        node.expect_quantum_mut()
2998            .instances
2999            .push(TemplateInstance::new(instance_id, template, offset));
3000        node
3001    }
3002
3003    fn aligned_round_circuit(interaction_slots: usize) -> CoordCircuit {
3004        let data = ivec2(0, 0);
3005        let ancilla = ivec2(1, 0);
3006        let mut circuit = CoordCircuit::new();
3007        circuit.do_gate(GateType::RZ, [data, ancilla]).unwrap();
3008        circuit.tick();
3009        for _ in 0..interaction_slots {
3010            circuit.do_gate(GateType::CX, [data, ancilla]).unwrap();
3011            circuit.tick();
3012        }
3013        circuit.measure(PauliBasis::Z, [data, ancilla]);
3014        circuit
3015    }
3016
3017    fn aligned_rotation_rounds(rounds: usize) -> CoordCircuit {
3018        let qubit = ivec2(0, 0);
3019        let mut circuit = CoordCircuit::new();
3020        for round in 0..rounds {
3021            circuit.do_gate(GateType::RZ, [qubit]).unwrap();
3022            circuit.tick();
3023            circuit.do_gate(GateType::H, [qubit]).unwrap();
3024            circuit.tick();
3025            circuit.measure(PauliBasis::Z, [qubit]);
3026            if round + 1 != rounds {
3027                circuit.tick();
3028            }
3029        }
3030        circuit
3031    }
3032
3033    #[test]
3034    fn aligned_emission_merges_same_layer_slots_and_keeps_record_lookbacks() {
3035        let mut graph = Bloq::new();
3036        let mut first_quantum = None;
3037        for (instance, interaction_slots, offset) in [
3038            (TemplateInstanceId(0), 4, ivec2(0, 0)),
3039            (TemplateInstanceId(1), 5, ivec2(10, 0)),
3040            (TemplateInstanceId(2), 6, ivec2(20, 0)),
3041        ] {
3042            let mut template = BloqTemplate::new(aligned_round_circuit(interaction_slots));
3043            template.detectors.push(TemplateDetector {
3044                scope: TemplateDetectorScope::TopLevel,
3045                parity: DetectorParity::from_measurements([0]),
3046                coords: None,
3047            });
3048            let template = graph.add_template(template);
3049            let mut node = BloqNode::from_members(vec![SourceBlockRef {
3050                pos: ivec3(offset.x, 0, 0),
3051            }]);
3052            node.expect_quantum_mut()
3053                .instances
3054                .push(TemplateInstance::new(instance, template, offset));
3055            first_quantum.get_or_insert(graph.add_node(node));
3056        }
3057        let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3058            vec![InstanceMeasurement {
3059                instance: TemplateInstanceId(0),
3060                measurement: 0,
3061            }],
3062            vec![],
3063        )));
3064        let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(7)));
3065        graph.add_edge(first_quantum.unwrap(), accumulate, BloqEdge::Order);
3066        graph.add_edge(accumulate, observable, BloqEdge::compose(0));
3067
3068        let text = emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3069            .expect("emit aligned Bloq Stim");
3070        let slots = text.split("TICK\n").collect::<Vec<_>>();
3071
3072        assert_eq!(
3073            slots.len() - 1,
3074            8,
3075            "four and five CX slots pad into six: {text}"
3076        );
3077        assert!(slots[0].contains("R 0 1"), "{text}");
3078        assert!(slots[0].contains("R 2 3"), "{text}");
3079        assert!(slots[0].contains("R 4 5"), "{text}");
3080        assert!(slots[7].contains("M 0 1"), "{text}");
3081        assert!(slots[7].contains("M 2 3"), "{text}");
3082        assert!(slots[7].contains("M 4 5"), "{text}");
3083        assert_eq!(text.matches("DETECTOR rec[-2]").count(), 3, "{text}");
3084        assert!(
3085            text.contains("OBSERVABLE_INCLUDE(7) rec[-6]"),
3086            "global frame must include every contributor: {text}"
3087        );
3088    }
3089
3090    #[test]
3091    fn aligned_emission_projects_timeline_rounds_into_source_layers() {
3092        let mut graph = Bloq::new();
3093        let mut tall = template_node_with_circuit_at(
3094            &mut graph,
3095            aligned_rotation_rounds(2),
3096            ivec3(0, 0, 2),
3097            TemplateInstanceId(0),
3098            ivec2(0, 0),
3099        );
3100        tall.expect_quantum_mut().timeline = Some(QuantumTimeline {
3101            layer_round_ends: vec![1, 2],
3102        });
3103        let tall = graph.add_node(tall);
3104        let upper = template_node_with_circuit_at(
3105            &mut graph,
3106            aligned_rotation_rounds(1),
3107            ivec3(1, 0, 3),
3108            TemplateInstanceId(1),
3109            ivec2(10, 0),
3110        );
3111        let upper = graph.add_node(upper);
3112
3113        let text = emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3114            .expect("emit timeline-aligned Bloq Stim");
3115        let slots = text.split("TICK\n").collect::<Vec<_>>();
3116
3117        assert_eq!(
3118            slots.len() - 1,
3119            6,
3120            "second tall round shares upper layer: {text}"
3121        );
3122        assert!(slots[0].contains(&format!("# node {}:", tall.0)), "{text}");
3123        assert!(slots[3].contains("R 0"), "{text}");
3124        assert!(slots[3].contains(&format!("# node {}:", upper.0)), "{text}");
3125        assert!(slots[3].contains("R 1"), "{text}");
3126    }
3127
3128    #[test]
3129    fn aligned_terminal_tick_keeps_detector_boundary_without_phantom_moment() {
3130        let mut circuit = CoordCircuit::new();
3131        circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3132        circuit.tick();
3133        let mut graph = Bloq::new();
3134        let mut template = BloqTemplate::new(circuit);
3135        template.detectors.push(TemplateDetector {
3136            scope: TemplateDetectorScope::TopLevel,
3137            parity: DetectorParity::from_measurements([0]),
3138            coords: None,
3139        });
3140        let template = graph.add_template(template);
3141        let mut node = BloqNode::from_members(vec![SourceBlockRef {
3142            pos: ivec3(0, 0, 0),
3143        }]);
3144        node.expect_quantum_mut()
3145            .instances
3146            .push(TemplateInstance::new(
3147                TemplateInstanceId(0),
3148                template,
3149                IVec2::ZERO,
3150            ));
3151        graph.add_node(node);
3152
3153        let ordinary = emit_bloq_stim(&graph).unwrap();
3154        let aligned =
3155            emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true)).unwrap();
3156
3157        assert_eq!(aligned, ordinary);
3158        assert_eq!(aligned.matches("TICK\n").count(), 2, "{aligned}");
3159        assert!(aligned.contains("M 0\nTICK\nDETECTOR rec[-1]\nTICK\n"));
3160    }
3161
3162    #[test]
3163    fn aligned_empty_circuit_is_silent_or_rejects_unplaceable_metadata() {
3164        let mut graph = Bloq::new();
3165        let node = template_node_with_circuit(&mut graph, CoordCircuit::new());
3166        let node = graph.add_node(node);
3167        let options = BloqStimOptions::new().with_align_moments(true);
3168
3169        assert_eq!(
3170            emit_bloq_stim_with(&graph, &options).unwrap(),
3171            emit_bloq_stim(&graph).unwrap()
3172        );
3173
3174        graph
3175            .node_mut(node)
3176            .unwrap()
3177            .expect_quantum_mut()
3178            .detectors
3179            .push(NodeDetector {
3180                parity: DetectorParity::<InstanceMeasurement>::default().with_sign(true),
3181                coords: None,
3182            });
3183        let error = emit_bloq_stim_with(&graph, &options).unwrap_err();
3184        assert_eq!(error, StimEmissionError::AlignMomentsWithEmptyMetadata);
3185    }
3186
3187    #[test]
3188    fn emit_bloq_stim_reports_unsupported_gate() {
3189        let qubit = ivec2(0, 0);
3190        let mut circuit = CoordCircuit::new();
3191        circuit.do_gate(GateType::T, [qubit]).unwrap();
3192        let mut graph = Bloq::new();
3193        let node = template_node_with_circuit(&mut graph, circuit);
3194        graph.add_node(node);
3195
3196        let error = emit_bloq_stim(&graph).unwrap_err();
3197
3198        assert_eq!(error, StimEmissionError::UnsupportedGate(GateType::T));
3199    }
3200
3201    #[test]
3202    fn unused_non_clifford_template_does_not_block_emission() {
3203        let qubit = ivec2(0, 0);
3204        let mut unused = CoordCircuit::new();
3205        unused.do_gate(GateType::T, [qubit]).unwrap();
3206        let mut graph = Bloq::new();
3207        graph.add_template(BloqTemplate::new(unused));
3208
3209        let mut used = CoordCircuit::new();
3210        used.do_gate(GateType::H, [qubit]).unwrap();
3211        let node = template_node_with_circuit(&mut graph, used);
3212        graph.add_node(node);
3213
3214        let text = emit_bloq_stim(&graph).expect("unused templates are not emitted");
3215        assert!(text.lines().any(|line| line == "H 0"), "{text}");
3216    }
3217
3218    #[test]
3219    fn unreachable_non_clifford_body_does_not_block_emission() {
3220        let qubit = ivec2(0, 0);
3221        let mut circuit = CoordCircuit::new();
3222        circuit.do_gate(GateType::H, [qubit]).unwrap();
3223        circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
3224            gate: GateType::T,
3225            qubits: vec![qubit],
3226        }]));
3227        let mut graph = Bloq::new();
3228        let node = template_node_with_circuit(&mut graph, circuit);
3229        graph.add_node(node);
3230
3231        let text = emit_bloq_stim(&graph).expect("unreachable bodies are not emitted");
3232        assert!(text.lines().any(|line| line == "H 0"), "{text}");
3233    }
3234
3235    #[test]
3236    fn include_boundary_face_fans_out_to_every_observable() {
3237        let qubit = ivec2(0, 0);
3238        let instance = TemplateInstanceId(0);
3239        for nested in [false, true] {
3240            let mut circuit = CoordCircuit::new();
3241            circuit.do_gate(GateType::X, [qubit]).unwrap();
3242            let mut graph = Bloq::new();
3243            let quantum = template_node_with_circuit(&mut graph, circuit);
3244            graph.add_node(quantum);
3245            let include = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3246                vec![],
3247                vec![InstanceBoundaryOperator {
3248                    instance,
3249                    face: BoundaryFace::Output,
3250                    operator: [(qubit, bloq_circuit::Pauli::Z)].into_iter().collect(),
3251                }],
3252            )));
3253            let producer = if nested {
3254                let recipe = graph.add_node(BloqNode::classical(
3255                    ClassicalNode::observable_fragment(vec![], vec![]),
3256                ));
3257                graph.add_edge(include, recipe, BloqEdge::compose(0));
3258                recipe
3259            } else {
3260                include
3261            };
3262            for index in [0, 1] {
3263                let observable =
3264                    graph.add_node(BloqNode::classical(ClassicalNode::observable(index)));
3265                graph.add_edge(producer, observable, BloqEdge::compose(0));
3266            }
3267
3268            let text = emit_bloq_stim(&graph).expect("shared fragment emits for both consumers");
3269            let aligned =
3270                emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3271                    .unwrap();
3272            for output in [&text, &aligned] {
3273                for index in [0, 1] {
3274                    assert!(
3275                        output.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3276                        "{output}"
3277                    );
3278                }
3279            }
3280            assert!(aligned.contains("X 0\nTICK\nOBSERVABLE_INCLUDE(0) Z0"));
3281            if nested {
3282                for enabled in [false, true] {
3283                    let mut guarded = graph.clone();
3284                    let gate = guarded.add_node(BloqNode::classical(ClassicalNode::Compute {
3285                        expr: ClassicalExpr::Const(enabled),
3286                    }));
3287                    guarded.node_mut(producer).unwrap().activation = Some(1);
3288                    guarded.add_edge(gate, producer, BloqEdge::value(1));
3289                    assert_eq!(
3290                        emit_bloq_stim(&guarded),
3291                        Err(StimEmissionError::UnsupportedNode(
3292                            "activation (conditional execution)"
3293                        ))
3294                    );
3295                }
3296            }
3297        }
3298    }
3299
3300    #[test]
3301    fn indexed_composition_includes_boundaries_but_value_ports_do_not() {
3302        let qubit = ivec2(0, 0);
3303        let mut circuit = CoordCircuit::new();
3304        circuit.do_gate(GateType::X, [qubit]).unwrap();
3305        let mut graph = Bloq::new();
3306        let quantum = template_node_with_circuit(&mut graph, circuit);
3307        let quantum = graph.add_node(quantum);
3308        let child = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
3309            index: Some(0),
3310            measurements: vec![],
3311            operators: vec![InstanceBoundaryOperator {
3312                instance: TemplateInstanceId(0),
3313                face: BoundaryFace::Output,
3314                operator: [(qubit, Pauli::Z)].into_iter().collect(),
3315            }],
3316        }));
3317        graph.add_edge(quantum, child, BloqEdge::Order);
3318        for (index, edge) in [
3319            (1, BloqEdge::compose(0)),
3320            (2, BloqEdge::value(0)),
3321            (3, BloqEdge::flip(0)),
3322        ] {
3323            let parent = graph.add_node(BloqNode::classical(ClassicalNode::observable(index)));
3324            graph.add_edge(child, parent, edge);
3325        }
3326        let text = emit_bloq_stim(&graph).unwrap();
3327        for index in [0, 1] {
3328            assert!(
3329                text.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3330                "{text}"
3331            );
3332        }
3333        for index in [2, 3] {
3334            assert!(
3335                !text.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3336                "{text}"
3337            );
3338        }
3339    }
3340
3341    #[test]
3342    fn repeated_recipe_paths_cancel_boundary_faces() {
3343        let qubit = ivec2(0, 0);
3344        let mut circuit = CoordCircuit::new();
3345        circuit.do_gate(GateType::X, [qubit]).unwrap();
3346        let mut graph = Bloq::new();
3347        let quantum = template_node_with_circuit(&mut graph, circuit);
3348        graph.add_node(quantum);
3349        let mut root = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3350            vec![],
3351            vec![InstanceBoundaryOperator {
3352                instance: TemplateInstanceId(0),
3353                face: BoundaryFace::Output,
3354                operator: [(qubit, bloq_circuit::Pauli::Z)].into_iter().collect(),
3355            }],
3356        )));
3357        for _ in 0..24 {
3358            let next = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3359                vec![],
3360                vec![],
3361            )));
3362            graph.add_edge(root, next, BloqEdge::compose(0));
3363            graph.add_edge(root, next, BloqEdge::compose(1));
3364            root = next;
3365        }
3366        let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3367        graph.add_edge(root, observable, BloqEdge::compose(0));
3368        let text = emit_bloq_stim(&graph).unwrap();
3369        assert!(!text.contains("OBSERVABLE_INCLUDE(0) Z0"), "{text}");
3370    }
3371
3372    #[test]
3373    fn emit_bloq_stim_instantiates_template_node_side_tables() {
3374        let mut graph = Bloq::new();
3375        let parent_node = measured_program_node(
3376            &mut graph,
3377            ivec3(0, 0, 0),
3378            ivec2(0, 0),
3379            TemplateInstanceId(1),
3380        );
3381        let qubit = ivec2(0, 0);
3382        let mut template_circuit = CoordCircuit::new();
3383        template_circuit.measure(PauliBasis::Z, [qubit]);
3384        let mut bloq_template = BloqTemplate::new(template_circuit);
3385        bloq_template.detectors.push(TemplateDetector {
3386            scope: TemplateDetectorScope::TopLevel,
3387            parity: DetectorParity::from_measurements([0]),
3388            coords: Some(bloq_circuit::DetectorCoords::from_slice(&[3.0, 4.0])),
3389        });
3390        let template = graph.add_template(bloq_template);
3391        let mut node = BloqNode::from_members(vec![SourceBlockRef {
3392            pos: ivec3(0, 0, 0),
3393        }]);
3394        node.expect_quantum_mut()
3395            .instances
3396            .push(TemplateInstance::new(
3397                TemplateInstanceId(0),
3398                template,
3399                ivec2(1, 0),
3400            ));
3401        let parent = graph.add_node(parent_node);
3402        let child = graph.add_node(node);
3403        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3404
3405        // The observable composes a measurement fragment, with a
3406        // read-after-measure Order edge from the producing node (IR spec §4/§5).
3407        let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3408            vec![InstanceMeasurement {
3409                instance: TemplateInstanceId(0),
3410                measurement: 0,
3411            }],
3412            vec![],
3413        )));
3414        let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3415        graph.add_edge(child, accumulate, BloqEdge::Order);
3416        graph.add_edge(accumulate, observable, BloqEdge::compose(0));
3417
3418        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3419
3420        insta::assert_snapshot!(text, @"
3421        QUBIT_COORDS(0, 0) 0
3422        QUBIT_COORDS(1, 0) 1
3423        # node 0: quantum blocks (0,0,0)
3424        M 0
3425        TICK
3426        # node 1: quantum blocks (0,0,0)
3427        M 1
3428        DETECTOR(4, 4) rec[-1]
3429        TICK
3430        # node 3: observable 0 synthetic
3431        OBSERVABLE_INCLUDE(0) rec[-1]
3432        ");
3433    }
3434
3435    #[test]
3436    fn idle_noise_expands_repeat_annotations_and_preserves_segment_records() {
3437        let (a, b) = (ivec2(0, 0), ivec2(1, 0));
3438        let mut circuit = CoordCircuit::new();
3439        circuit.do_gate(GateType::RZ, [a]).unwrap();
3440        let measurement = circuit.reserve_measurement_id(b);
3441        let body = circuit.add_body(CircuitBody::from_ops(vec![
3442            Op::Measure {
3443                basis: PauliBasis::Z,
3444                qubits: vec![b],
3445                measurements: vec![measurement],
3446                flip_probability: 0.0,
3447            },
3448            Op::Tick,
3449        ]));
3450        circuit.push_repeat(body, 3);
3451        let mut template = BloqTemplate::new(circuit);
3452        template.detectors.push(TemplateDetector {
3453            scope: TemplateDetectorScope::RepeatBody { body },
3454            parity: DetectorParity::from_terms([
3455                DetectorTerm::Measurement(measurement),
3456                DetectorTerm::LoopState(LoopStateId(0)),
3457            ]),
3458            coords: None,
3459        });
3460        template.repeat_states.push(TemplateRepeatState {
3461            body,
3462            state: LoopStateId(0),
3463            initial: DetectorParity::default(),
3464            next: DetectorParity::from_measurements([measurement]),
3465        });
3466        template.restarts.push(bloq_ir::lowering::TemplateRestart {
3467            parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
3468        });
3469        let mut without_restart = template.clone();
3470        without_restart.restarts.clear();
3471        let mut program = Bloq::new();
3472        let template_id = program.add_template(template);
3473        let mut node = BloqNode::from_members(vec![]);
3474        node.expect_quantum_mut()
3475            .instances
3476            .push(TemplateInstance::new(
3477                TemplateInstanceId(0),
3478                template_id,
3479                IVec2::ZERO,
3480            ));
3481        let noise = NoiseModel {
3482            p_idle: 0.125,
3483            ..NoiseModel::uniform_depolarizing(0.0)
3484        };
3485        let plan = node
3486            .emission_plan_with_options(program.templates(), InstantiationOptions::noisy(&noise))
3487            .unwrap();
3488        let normalized = &plan.templates(program.templates())[template_id];
3489        assert_eq!(normalized.detectors.len(), 3);
3490        assert!(normalized.repeat_states.is_empty());
3491        assert_eq!(
3492            normalized.restarts[0].parity,
3493            DetectorParity::from_measurements([measurement])
3494        );
3495        assert_eq!(
3496            plan.circuit.expanded_measurement_columns().unwrap().count(),
3497            3
3498        );
3499
3500        // Static Stim emission excludes RUS-only restart annotations; their
3501        // expansion was checked above on the same normalized source template.
3502        node.expect_quantum_mut().instances[0].template_id = program.add_template(without_restart);
3503        let producer = program.add_node(node);
3504        let record = program.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3505            vec![InstanceMeasurement {
3506                instance: TemplateInstanceId(0),
3507                measurement,
3508            }],
3509            vec![],
3510        )));
3511        let observable = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3512        program.add_edge(producer, record, BloqEdge::Order);
3513        program.add_edge(record, observable, BloqEdge::compose(0));
3514        let (clean, noisy) = emit_bloq_stim_segments_pair(&program, &noise).unwrap();
3515        assert_eq!(clean.num_measurements(), 3);
3516        assert_eq!(noisy.num_measurements(), 3);
3517        assert_eq!(
3518            noisy.to_text().matches("DEPOLARIZE1(0.125) 0").count(),
3519            2,
3520            "the leading reset occupies a's first moment, but a idles on later iterations"
3521        );
3522        assert_eq!(noisy.to_text().matches("DETECTOR").count(), 3);
3523        for text in [clean.to_text(), noisy.to_text()] {
3524            assert!(
3525                text.contains("OBSERVABLE_INCLUDE(0) rec[-1]"),
3526                "the original measurement id still names the final loop record: {text}"
3527            );
3528        }
3529        let mut flat = program.clone();
3530        flat.flatten().unwrap();
3531        let expected =
3532            emit_bloq_stim_with(&flat, &BloqStimOptions::new().with_noise(&noise)).unwrap();
3533        assert_eq!(noisy.to_text(), expected);
3534        #[cfg(feature = "verify")]
3535        for text in [clean.to_text(), noisy.to_text()] {
3536            let circuit: stim::Circuit = text.parse().unwrap();
3537            assert_eq!(circuit.num_measurements(), 3);
3538            assert_eq!(circuit.num_detectors(), 3);
3539        }
3540    }
3541
3542    #[test]
3543    fn emit_bloq_stim_instantiates_template_repeat_side_tables() {
3544        let qubit = ivec2(0, 0);
3545        let mut template_circuit = CoordCircuit::new();
3546        let body = template_circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
3547            basis: PauliBasis::Z,
3548            qubits: vec![qubit],
3549            measurements: vec![0],
3550            flip_probability: 0.0,
3551        }]));
3552        template_circuit.register_measurement_id(0, qubit);
3553        template_circuit.push_repeat(body, 2);
3554        let mut bloq_template = BloqTemplate::new(template_circuit);
3555        bloq_template.detectors.push(TemplateDetector {
3556            scope: TemplateDetectorScope::RepeatBody { body },
3557            parity: DetectorParity::from_measurements([0]),
3558            coords: None,
3559        });
3560        bloq_template.repeat_states.push(TemplateRepeatState {
3561            body,
3562            state: LoopStateId(0),
3563            initial: DetectorParity::default(),
3564            next: DetectorParity::from_measurements([0]),
3565        });
3566        let mut graph = Bloq::new();
3567        let template = graph.add_template(bloq_template);
3568        let mut node = BloqNode::from_members(vec![SourceBlockRef {
3569            pos: ivec3(0, 0, 0),
3570        }]);
3571        node.expect_quantum_mut()
3572            .instances
3573            .push(TemplateInstance::new(
3574                TemplateInstanceId(0),
3575                template,
3576                qubit,
3577            ));
3578        graph.add_node(node);
3579
3580        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3581
3582        insta::assert_snapshot!(text, @"
3583        QUBIT_COORDS(0, 0) 0
3584        # node 0: quantum blocks (0,0,0)
3585        REPEAT 2 {
3586            M 0
3587            DETECTOR rec[-1]
3588        }
3589        ");
3590    }
3591
3592    #[test]
3593    fn emit_bloq_stim_emits_node_circuits_in_topological_order() {
3594        let mut graph = Bloq::new();
3595        let parent_node = measured_program_node(
3596            &mut graph,
3597            ivec3(0, 0, 0),
3598            ivec2(0, 0),
3599            TemplateInstanceId(0),
3600        );
3601        let child_node = measured_program_node(
3602            &mut graph,
3603            ivec3(0, 0, 1),
3604            ivec2(1, 0),
3605            TemplateInstanceId(1),
3606        );
3607        // Insert opposite to dependency order so storage order cannot pass.
3608        let child = graph.add_node(child_node);
3609        let parent = graph.add_node(parent_node);
3610        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3611
3612        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3613
3614        let measurement_lines = text
3615            .lines()
3616            .filter(|line| line.starts_with("M "))
3617            .collect::<Vec<_>>();
3618        assert_eq!(measurement_lines, vec!["M 0", "M 1"], "{text}");
3619    }
3620
3621    #[test]
3622    fn emit_bloq_stim_resolves_cross_node_detector_refs() {
3623        let mut graph = Bloq::new();
3624        let mut parent_circuit = CoordCircuit::new();
3625        parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3626        let parent_node = template_node_with_circuit_at(
3627            &mut graph,
3628            parent_circuit,
3629            ivec3(0, 0, 0),
3630            TemplateInstanceId(0),
3631            IVec2::ZERO,
3632        );
3633        let mut child_circuit = CoordCircuit::new();
3634        child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
3635        let mut child_node = template_node_with_circuit_at(
3636            &mut graph,
3637            child_circuit,
3638            ivec3(0, 0, 1),
3639            TemplateInstanceId(1),
3640            IVec2::ZERO,
3641        );
3642        child_node
3643            .expect_quantum_mut()
3644            .detectors
3645            .push(NodeDetector {
3646                parity: DetectorParity::from_measurements([
3647                    InstanceMeasurement {
3648                        instance: TemplateInstanceId(0),
3649                        measurement: 0,
3650                    },
3651                    InstanceMeasurement {
3652                        instance: TemplateInstanceId(1),
3653                        measurement: 0,
3654                    },
3655                ]),
3656                coords: Some(bloq_circuit::DetectorCoords::from_slice(&[5.0, 6.0])),
3657            });
3658
3659        let parent = graph.add_node(parent_node);
3660        let child = graph.add_node(child_node);
3661        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3662
3663        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3664
3665        assert!(text.contains("DETECTOR(5, 6) rec[-2] rec[-1]"), "{text}");
3666    }
3667
3668    #[test]
3669    fn emit_bloq_stim_resolves_shared_cross_node_detector() {
3670        use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
3671
3672        let mut graph = Bloq::new();
3673        let mut parent_circuit = CoordCircuit::new();
3674        parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3675        let parent_node = template_node_with_circuit_at(
3676            &mut graph,
3677            parent_circuit,
3678            ivec3(0, 0, 0),
3679            TemplateInstanceId(0),
3680            IVec2::ZERO,
3681        );
3682        let mut child_circuit = CoordCircuit::new();
3683        child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
3684        let mut child_node = template_node_with_circuit_at(
3685            &mut graph,
3686            child_circuit,
3687            ivec3(0, 0, 1),
3688            TemplateInstanceId(1),
3689            IVec2::ZERO,
3690        );
3691        let owner_templates = vec![
3692            parent_node.expect_quantum().instances[0].template_id,
3693            child_node.expect_quantum().instances[0].template_id,
3694        ];
3695        let bundle = graph.add_detector_bundle(DetectorBundle::new(
3696            owner_templates,
3697            vec![BundleDetector {
3698                parity: bloq_circuit::DetectorParity::from_measurements([
3699                    BundleMeasurement {
3700                        owner: 0,
3701                        measurement: 0,
3702                    },
3703                    BundleMeasurement {
3704                        owner: 1,
3705                        measurement: 0,
3706                    },
3707                ]),
3708                coords: Some(bloq_circuit::DetectorCoords::from_slice(&[1.0, 2.0])),
3709            }],
3710        ));
3711        child_node
3712            .expect_quantum_mut()
3713            .detector_bundles
3714            .push(DetectorBundleUse {
3715                bundle,
3716                instances: vec![TemplateInstanceId(0), TemplateInstanceId(1)],
3717                offset: IVec2::new(4, 4),
3718            });
3719
3720        let parent = graph.add_node(parent_node);
3721        let child = graph.add_node(child_node);
3722        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3723
3724        let text = emit_bloq_stim(&graph).expect("emit shared detector");
3725        assert!(text.contains("DETECTOR(5, 6) rec[-2] rec[-1]"), "{text}");
3726
3727        let wrong_bundle = graph.add_detector_bundle(DetectorBundle::new(
3728            vec![graph[child].expect_quantum().instances[0].template_id; 2],
3729            vec![],
3730        ));
3731        graph
3732            .node_mut(child)
3733            .unwrap()
3734            .expect_quantum_mut()
3735            .detector_bundles[0]
3736            .bundle = wrong_bundle;
3737        assert!(matches!(
3738            emit_bloq_stim(&graph),
3739            Err(StimEmissionError::DetectorBundle(
3740                bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
3741            ))
3742        ));
3743    }
3744
3745    #[test]
3746    fn ordinary_emission_rejects_malformed_bundle_parity() {
3747        use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
3748
3749        for term in [
3750            DetectorTerm::Measurement(BundleMeasurement {
3751                owner: 0,
3752                measurement: 99,
3753            }),
3754            DetectorTerm::LoopState(LoopStateId(7)),
3755        ] {
3756            let mut graph = Bloq::new();
3757            let mut circuit = CoordCircuit::new();
3758            circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
3759            let mut node = template_node_with_circuit_at(
3760                &mut graph,
3761                circuit,
3762                ivec3(0, 0, 0),
3763                TemplateInstanceId(0),
3764                IVec2::ZERO,
3765            );
3766            let template = node.expect_quantum().instances[0].template_id;
3767            let bundle = graph.add_detector_bundle(DetectorBundle::new(
3768                vec![template],
3769                vec![BundleDetector {
3770                    parity: DetectorParity::from_terms([term]),
3771                    coords: None,
3772                }],
3773            ));
3774            node.expect_quantum_mut()
3775                .detector_bundles
3776                .push(DetectorBundleUse {
3777                    bundle,
3778                    instances: vec![TemplateInstanceId(0)],
3779                    offset: IVec2::ZERO,
3780                });
3781            graph.add_node(node);
3782
3783            let error = emit_bloq_stim(&graph).expect_err("malformed annotation must fail");
3784            match term {
3785                DetectorTerm::Measurement(_) => assert_eq!(
3786                    error,
3787                    StimEmissionError::UnknownInstanceMeasurement(InstanceMeasurement {
3788                        instance: TemplateInstanceId(0),
3789                        measurement: 99,
3790                    }),
3791                ),
3792                DetectorTerm::LoopState(state) => {
3793                    assert_eq!(error, StimEmissionError::NodeLoopStateUnsupported(state),)
3794                }
3795            }
3796        }
3797    }
3798
3799    #[test]
3800    fn emit_bloq_stim_maps_feedforward_control_to_its_instance_record() {
3801        use bloq_circuit::ConditionalCorrection;
3802        let mut graph = Bloq::new();
3803        let mut parent_circuit = CoordCircuit::new();
3804        parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3805        let parent_node = template_node_with_circuit_at(
3806            &mut graph,
3807            parent_circuit,
3808            ivec3(0, 0, 0),
3809            TemplateInstanceId(0),
3810            IVec2::ZERO,
3811        );
3812
3813        let mut child_circuit = CoordCircuit::new();
3814        let control = child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)])[0];
3815        child_circuit
3816            .body_mut(child_circuit.entry_body())
3817            .expect("entry body exists")
3818            .ops_mut()
3819            .push(Op::ConditionalPauli(vec![ConditionalCorrection {
3820                pauli: PauliBasis::X,
3821                control,
3822                target: ivec2(2, 0),
3823            }]));
3824        child_circuit
3825            .do_gate(bloq_circuit::GateType::RZ, [ivec2(2, 0)])
3826            .unwrap();
3827        let child_node = template_node_with_circuit_at(
3828            &mut graph,
3829            child_circuit,
3830            ivec3(0, 0, 1),
3831            TemplateInstanceId(1),
3832            IVec2::ZERO,
3833        );
3834
3835        let parent = graph.add_node(parent_node);
3836        let child = graph.add_node(child_node);
3837        graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3838
3839        for aligned in [false, true] {
3840            let text =
3841                emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(aligned))
3842                    .expect("emit measurement feed-forward");
3843            assert!(text.contains("M 1\nCX rec[-1] 2"), "{text}");
3844            assert!(!text.contains("CX rec[-2] 2"), "{text}");
3845            assert!(
3846                text.find("CX rec[-1] 2").unwrap() < text.find("R 2").unwrap(),
3847                "{text}"
3848            );
3849            #[cfg(feature = "verify")]
3850            text.parse::<stim::Circuit>()
3851                .expect("program feed-forward should parse as Stim");
3852        }
3853    }
3854
3855    #[test]
3856    fn emit_bloq_stim_summarizes_nested_large_repeat_frames() {
3857        // A large inner repeat under a summarized outer one must not expand
3858        // per-iteration accounting: the frame only needs the final inner
3859        // iteration's occurrences, wherever the loop nests.
3860        let qubit = ivec2(0, 0);
3861        let mut circuit = CoordCircuit::new();
3862        let measurement = circuit.reserve_measurement_id(qubit);
3863        let inner = circuit.add_body(bloq_circuit::CircuitBody::from_ops(vec![Op::Measure {
3864            basis: PauliBasis::Z,
3865            qubits: vec![qubit],
3866            measurements: vec![measurement],
3867            flip_probability: 0.0,
3868        }]));
3869        let outer = circuit.add_body(bloq_circuit::CircuitBody::from_ops(vec![Op::Repeat {
3870            body: inner,
3871            repetitions: 1_000_000,
3872        }]));
3873        circuit.push_repeat(outer, 2);
3874        let mut graph = Bloq::new();
3875        let mut node = template_node_with_circuit(&mut graph, circuit);
3876        node.expect_quantum_mut().detectors.push(NodeDetector {
3877            parity: DetectorParity::from_measurements([InstanceMeasurement {
3878                instance: TemplateInstanceId(0),
3879                measurement,
3880            }]),
3881            coords: None,
3882        });
3883        graph.add_node(node);
3884
3885        let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3886
3887        assert!(text.contains("REPEAT 2 {"), "{text}");
3888        assert!(text.contains("REPEAT 1000000 {"), "{text}");
3889        assert_eq!(
3890            text.lines().filter(|line| line.trim() == "M 0").count(),
3891            1,
3892            "{text}"
3893        );
3894        assert!(text.contains("DETECTOR rec[-1]"), "{text}");
3895    }
3896}