Skip to main content

bloq_vm/
program.rs

1//! Execute a whole compiled [`Bloq`] program without adding noise on the shared
2//! [`Simulator`] engine. This is the *one* walk that owns SEM-ORD ordering,
3//! region control flow, and measurement-record binding; [`run_bloq`],
4//! [`run_bloq_with_hook`], and [`run_bloq_with_io`] are its entry points. (The
5//! single-template runner lives with the verifier in [`crate::verify`].)
6//!
7//! # Whole-program model (`run_bloq`)
8//!
9//! Measurement ids are allocated contiguously across every instance measurement
10//! at every nesting level (`InstanceMeasurement{instance, measurement}` → global
11//! id). Nodes are executed in `deterministic_emit_order` onto ONE shared
12//! [`Simulator`] over the program's global coordinate layout; a quantum
13//! node's circuit comes from `emission_plan` (instances offset-translated), with
14//! its local measurement ids remapped to the global ids.
15//!
16//! ## Region control flow (dynamic, per-shot — the point of a procedural engine)
17//!
18//! Region bodies are nested [`SubGraph`](bloq_ir::SubGraph)s over the same
19//! template pool; the walk recurses into them on the same shared simulator:
20//! * `RepeatUntilSuccess` — execute sandboxed body attempts until both the
21//!   post-selection restart parities and the IR restart predicate accept, up to
22//!   [`DEFAULT_RUS_ATTEMPT_CAP`] attempts. Rejected attempts restore the
23//!   pre-body simulator and records (SEM-ATTEMPT); the accepted attempt alone
24//!   composes upward.
25//!
26//! Quantum membership guards select active instances and their record aliases;
27//! activation slots suppress inactive classical recipes and region bodies.
28//! Classical nodes fold as in a flat program (observable recipes/linear `Compute`;
29//! `Discard` rejects the shot and stops its execution). An `Observable` is a
30//! complete corrected-parity producer, folding local records and bit-carrying
31//! inputs. This no-op executor uses a zero decoder flip; composition
32//! therefore has the same bit here. Boundaries contribute only to reports.
33//! The per-shot `ObservableReport` is measured on the final state
34//! ([`Program::observable_value`]) — the record/feedback fold plus any
35//! composed boundary-Pauli face. Detectors (template-local + node
36//! instance-space, at every level) are verified per shot with the usual
37//! `LoopState`/`RepeatBody` policy.
38//!
39//! # Observable contract
40//!
41//! An `Observable` is its raw record parity plus live `Output`-face boundary
42//! Paulis. Feedforward byproducts therefore remain visible; callers combine the
43//! raw value with the terminal frame sign when they need a deterministic logical
44//! value. `Input` faces are source endpoints already closed by composition and
45//! are not folded. Because output faces are read from the terminal simulator
46//! state, preparation rejects a later quantum node that consumes one.
47
48use std::cell::RefCell;
49
50use bloq_ir::circuit::{DetectorParity, DetectorTerm, PauliMap};
51#[cfg(test)]
52use bloq_ir::lowering::TemplateDetectorScope;
53use bloq_ir::lowering::{InstanceBoundaryOperator, InstanceMeasurement, TemplateInstanceId};
54use bloq_ir::{
55    Bloq, BloqNodeId, BloqNodeKind, BodySelector, ClassicalNode, LevelPath, NodeDetectorAddress,
56    ObservableOutput, RegionNode, SubGraph,
57};
58use glam::{IVec2, IVec3};
59use rustc_hash::{FxHashMap, FxHashSet};
60
61use super::circuit::ShotRecord;
62use super::prepared::PreparedProgramCore;
63use super::{ExecError, MAX_PHYSICAL_BOUNDARY_BINDINGS};
64use crate::backend::{Pauli, PauliString, Simulator};
65use crate::verify::{FramePairReport, ObservableReport, VerifyReport, detector_report};
66
67// ============================================================
68// Whole program
69// ============================================================
70
71/// Retry bound for a `RepeatUntilSuccess` region: a shot still restarting after
72/// this many attempts is discarded rather than looping forever.
73pub const DEFAULT_RUS_ATTEMPT_CAP: u32 = 100;
74
75/// How far `|⟨P⟩|` may sit below 1 and still count as a definite face value.
76/// Stabilizer expectations land exactly on ±1; the slack is for the amplitude
77/// map a `T` introduces.
78const DETERMINISTIC_EXPECTATION_TOLERANCE: f64 = 1e-9;
79
80/// Immutable, shot-independent program data.
81struct Program {
82    core: PreparedProgramCore,
83    inputs: Vec<InputPatch>,
84    /// Resolved restart parities keyed by enclosing RUS body scope; each entry
85    /// retains its nested owner scope for executed-path applicability.
86    rus_restarts: FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>,
87    /// `Output` faces a later node consumes, read at their cut instead of on the
88    /// terminal state. Empty for every program whose outputs stay live to the
89    /// end, which is most of them.
90    early_reads: Vec<super::closure::EarlyOutputRead>,
91    /// Possible first consumers, aligned with logical-output metadata.
92    output_cuts: Vec<Vec<(LevelPath, BloqNodeId)>>,
93    /// Consumed logical outputs to swap onto private ancillas at their cuts.
94    /// Empty unless the caller requested captured outputs.
95    early_output_captures: Vec<EarlyOutputCapture>,
96    /// Reused dense workspace for observables that fold multiple dynamic
97    /// boundary bindings. Whole-program shots execute sequentially.
98    boundary_scratch: RefCell<PauliString>,
99}
100
101struct EarlyOutputCapture {
102    before: Vec<(LevelPath, BloqNodeId)>,
103    instance: TemplateInstanceId,
104    port: IVec3,
105    operators: Vec<EarlyOutputPaulis>,
106}
107
108struct EarlyOutputPaulis {
109    logical_x: PauliString,
110    logical_z: PauliString,
111    ancilla_x: PauliString,
112    ancilla_z: PauliString,
113}
114
115#[derive(Clone, Copy)]
116enum RunMode {
117    Plain,
118    PrepareInputs,
119    CaptureOutputs,
120}
121
122/// Shot-independent data needed to validate terminal folds and build reports.
123struct ProgramReportLayout {
124    detectors: Vec<(LevelPath, ResolvedDetector)>,
125    observable_nodes: Vec<BloqNodeId>,
126    outputs: Vec<OutputLogical>,
127    frames: Vec<bloq_ir::FramePair>,
128}
129
130type StagedInputSeeds = FxHashMap<TemplateInstanceId, Vec<(usize, usize)>>;
131
132#[derive(Clone)]
133struct DeferredCcz {
134    seeds: [usize; 3],
135    prepare_before: [Vec<TemplateInstanceId>; 3],
136    prepared: bool,
137}
138
139#[derive(Debug, Clone)]
140struct InputPatch {
141    input_instance: TemplateInstanceId,
142    prepare_before: Vec<TemplateInstanceId>,
143    port: IVec3,
144    data_qubits: Vec<usize>,
145    stabilizers: Vec<PauliString>,
146    logical_x: PauliString,
147    logical_z: PauliString,
148}
149
150impl InputPatch {
151    fn prepare_injection_scaffold(&self, sim: &mut Simulator) -> Result<usize, ExecError> {
152        let mut intersection = None;
153        for &qubit in &self.data_qubits {
154            let on_x = self.logical_x.xbit(qubit);
155            if on_x {
156                sim.reset_x(qubit)?;
157            } else {
158                sim.reset(qubit)?;
159            }
160            if on_x && self.logical_z.zbit(qubit) && intersection.replace(qubit).is_some() {
161                return Err(ExecError::InvalidInjectionPatch { port: self.port });
162            }
163        }
164        intersection.ok_or(ExecError::InvalidInjectionPatch { port: self.port })
165    }
166
167    fn postselect_injection_patch(&self, sim: &mut Simulator) -> Result<(), ExecError> {
168        for stabilizer in &self.stabilizers {
169            sim.postselect_observable(stabilizer, false)?;
170        }
171        Ok(())
172    }
173}
174
175/// One private seed qubit prepared before execution and injected at its input
176/// Port's safe schedule cut.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct InputSeed {
179    /// The source input port.
180    pub port: IVec3,
181    /// Private engine qubit initialized to `|+>`.
182    pub qubit: usize,
183}
184
185/// Per-shot private-seed context for [`run_bloq_with_io`].
186#[derive(Debug)]
187pub struct PreparationContext<'a> {
188    /// Zero-based shot number.
189    pub shot: usize,
190    /// One private seed per logical input, sorted by source port.
191    pub inputs: &'a [InputSeed],
192    deferred_ccz: &'a RefCell<Vec<[IVec3; 3]>>,
193}
194
195impl PreparationContext<'_> {
196    /// Queue an ideal logical CCZ resource on three input seeds. The executor
197    /// applies the gate immediately before the first quantum component that
198    /// consumes any leg, so earlier block execution cannot overwrite it.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`ExecError::InvalidCczPreparation`] when any two ports are equal.
203    pub fn prepare_ccz(&self, ports: [IVec3; 3]) -> Result<(), ExecError> {
204        if ports[0] == ports[1] || ports[0] == ports[2] || ports[1] == ports[2] {
205            return Err(ExecError::InvalidCczPreparation);
206        }
207        self.deferred_ccz.borrow_mut().push(ports);
208        Ok(())
209    }
210}
211
212/// Per-shot mutable state.
213struct Shot {
214    sim: Simulator,
215    staged_inputs: StagedInputSeeds,
216    injected_inputs: Vec<bool>,
217    deferred_ccz: Vec<DeferredCcz>,
218    record: ShotRecord,
219    discarded: bool,
220    /// Peak stabilizer rank reached during the shot (a `T` grows it before a
221    /// post-selection measurement can collapse it back).
222    ///
223    /// Sampled once per *instruction*, which is finer than the once-per-`Op`
224    /// sampling this used before the batch rewiring: an `Op` carrying several
225    /// `T` targets is one op but several rank-moving steps, and the old reading
226    /// could only see where that op ended. The rank timeline itself is
227    /// unchanged — Clifford instructions never touch the amplitude map — so
228    /// this only ever reports a peak the shot genuinely passed through. It runs
229    /// about 2x higher on the non-Clifford gallery fixtures as a result.
230    peak_rank: usize,
231    /// Instances excluded by membership guards or inactive regions this shot.
232    /// Their measurement terms are dropped; a missing record for any other
233    /// instance is a hard error.
234    excluded_instances: rustc_hash::FxHashSet<TemplateInstanceId>,
235    /// Region-body scopes entered on the terminal execution path. Detector
236    /// ownership also gates detectors inside inactive regions.
237    executed_scopes: rustc_hash::FxHashSet<LevelPath>,
238    active_membership_inputs: rustc_hash::FxHashSet<(LevelPath, BloqNodeId, u32)>,
239    /// Branch selector outcomes this shot, in evaluation order.
240    branch_choices: Vec<bool>,
241    /// Values of `Program::early_reads`, taken at their cuts, in that order.
242    /// `None` where the consuming node has not run (or never runs, its arm not
243    /// being taken) and the face is still live for the terminal read.
244    early_faces: Vec<Option<bool>>,
245    /// Evaluated values remain available to gates in ancestor scopes.
246    classical_values: FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
247    /// Engine indices holding logical outputs swapped away before reuse.
248    saved_outputs: Vec<Option<usize>>,
249    /// Seed used to construct this shot, for independent retry RNG streams.
250    seed: u64,
251}
252
253struct CompletedShot<'a> {
254    state: Shot,
255    top_values: FxHashMap<u32, Option<bool>>,
256    top_bindings: FxHashMap<u32, BoundOperators<'a>>,
257}
258
259enum BoundOperators<'a> {
260    /// Physical reads retain Output faces; Input faces stay in the IR.
261    Owned(Vec<&'a InstanceBoundaryOperator>),
262    Recipe(Vec<&'a InstanceBoundaryOperator>, Box<[BloqNodeId]>),
263}
264
265impl BoundOperators<'_> {
266    fn has_output(&self) -> bool {
267        match self {
268            Self::Owned(operators) => !operators.is_empty(),
269            Self::Recipe(operators, children) => !operators.is_empty() || !children.is_empty(),
270        }
271    }
272}
273
274fn collect_bound_operators<'a>(
275    bindings: &FxHashMap<u32, BoundOperators<'a>>,
276    producer: BloqNodeId,
277    out: &mut Vec<&'a InstanceBoundaryOperator>,
278    expanded: &mut usize,
279) -> Result<(), ExecError> {
280    let mut stack = vec![(producer, false, false)];
281    let mut active = FxHashSet::default();
282    while let Some((producer, exit, nested_recipe)) = stack.pop() {
283        if exit {
284            active.remove(&producer);
285            continue;
286        }
287        if nested_recipe {
288            *expanded = expanded.saturating_add(1);
289            if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
290                return Err(ExecError::BoundaryBindingExpansionLimit {
291                    limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
292                });
293            }
294        }
295        if !active.insert(producer) {
296            return Err(ExecError::MalformedGraph("cyclic binding recipe"));
297        }
298        match bindings.get(&producer.0) {
299            Some(BoundOperators::Owned(operators)) => {
300                if nested_recipe {
301                    *expanded = expanded.saturating_add(operators.len());
302                    if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
303                        return Err(ExecError::BoundaryBindingExpansionLimit {
304                            limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
305                        });
306                    }
307                }
308                out.extend(operators);
309                active.remove(&producer);
310            }
311            Some(BoundOperators::Recipe(operators, children)) => {
312                *expanded = expanded.saturating_add(operators.len());
313                if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
314                    return Err(ExecError::BoundaryBindingExpansionLimit {
315                        limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
316                    });
317                }
318                out.extend(operators);
319                stack.push((producer, true, nested_recipe));
320                stack.extend(children.iter().rev().map(|child| (*child, false, true)));
321            }
322            None => {
323                active.remove(&producer);
324            }
325        }
326    }
327    Ok(())
328}
329
330/// Pre-body state reused by every retry. `clone_from` preserves the live
331/// simulator/hash-map allocations instead of reallocating them each attempt.
332struct ShotSnapshot {
333    sim: Simulator,
334    injected_inputs: Vec<bool>,
335    deferred_ccz: Vec<DeferredCcz>,
336    record: ShotRecord,
337    discarded: bool,
338    excluded_instances: rustc_hash::FxHashSet<TemplateInstanceId>,
339    executed_scopes: rustc_hash::FxHashSet<LevelPath>,
340    active_membership_inputs: rustc_hash::FxHashSet<(LevelPath, BloqNodeId, u32)>,
341    branch_choices: Vec<bool>,
342    early_faces: Vec<Option<bool>>,
343    /// Evaluated values remain available to gates in ancestor scopes.
344    classical_values: FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
345    saved_outputs: Vec<Option<usize>>,
346}
347
348impl ShotSnapshot {
349    fn capture(state: &Shot) -> Self {
350        Self {
351            sim: state.sim.clone(),
352            injected_inputs: state.injected_inputs.clone(),
353            deferred_ccz: state.deferred_ccz.clone(),
354            record: state.record.clone(),
355            discarded: state.discarded,
356            excluded_instances: state.excluded_instances.clone(),
357            executed_scopes: state.executed_scopes.clone(),
358            active_membership_inputs: state.active_membership_inputs.clone(),
359            branch_choices: state.branch_choices.clone(),
360            early_faces: state.early_faces.clone(),
361            classical_values: state.classical_values.clone(),
362            saved_outputs: state.saved_outputs.clone(),
363        }
364    }
365
366    fn restore(&self, state: &mut Shot) {
367        state.sim.clone_from(&self.sim);
368        state.injected_inputs.clone_from(&self.injected_inputs);
369        state.deferred_ccz.clone_from(&self.deferred_ccz);
370        state.record.clone_from(&self.record);
371        state.discarded = self.discarded;
372        state
373            .excluded_instances
374            .clone_from(&self.excluded_instances);
375        state.executed_scopes.clone_from(&self.executed_scopes);
376        state
377            .active_membership_inputs
378            .clone_from(&self.active_membership_inputs);
379        state.branch_choices.clone_from(&self.branch_choices);
380        state.early_faces.clone_from(&self.early_faces);
381        state.classical_values.clone_from(&self.classical_values);
382        state.saved_outputs.clone_from(&self.saved_outputs);
383    }
384}
385
386/// Logical `X` and `Z` operators for one program output.
387///
388/// These engine-index [`PauliString`]s are ready to measure at the end of a
389/// shot. [`run_bloq_with_hook`] builds them once and lends them to every shot's
390/// hook via [`ShotContext`].
391///
392/// Both maps come directly from [`Bloq::logical_outputs`], so execution needs
393/// no compiler geometry.
394#[derive(Debug, Clone)]
395pub struct OutputLogical {
396    /// The source output port.
397    pub port: IVec3,
398    /// Logical `X` over engine indices.
399    pub logical_x: PauliString,
400    /// Logical `Z` over engine indices.
401    pub logical_z: PauliString,
402    /// Whether a later node can consume this output's patch. Its operators
403    /// then refer to a later worldline on the terminal state; hooks must use
404    /// [`ShotContext::saved_outputs`] to read outputs captured at an earlier cut.
405    pub consumed: bool,
406}
407
408/// One output's terminal Pauli-frame value for the current shot.
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct ShotFrame {
411    /// The source output port this frame corrects.
412    pub port: IVec3,
413    /// Whether an `X` correction is required, or `None` if unevaluable.
414    pub x: Option<bool>,
415    /// Whether a `Z` correction is required, or `None` if unevaluable.
416    pub z: Option<bool>,
417}
418
419/// A logical output swapped onto a private engine qubit before its physical
420/// patch was reused later in the program.
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct SavedOutput {
423    /// The source output port.
424    pub port: IVec3,
425    /// Engine index holding the logical state before terminal frame correction.
426    pub qubit: usize,
427}
428
429/// Per-shot context handed to a [`run_bloq_with_hook`] hook, evaluated after the
430/// program finishes but before the shot's simulator is dropped.
431///
432/// The hook receives the pristine post-program simulator (the output patches are
433/// still live — an ideal noiseless boundary). A hook that mutates the simulator
434/// (e.g. a destructive measurement) therefore invalidates *this shot's* observable and
435/// frame reports, but never the record-based detectors.
436#[derive(Debug)]
437pub struct ShotContext<'a> {
438    /// Zero-based shot number (discarded shots never reach this hook).
439    pub shot: usize,
440    /// This shot's measurement outcomes.
441    pub record: &'a ShotRecord,
442    /// This shot's stamped selector outcomes, in evaluation order — the
443    /// feedforward action bits driving the terminal Pauli frame.
444    pub branch_selectors: &'a [bool],
445    /// Structural and selective choices, keyed by their source selector names.
446    pub named_branch_selectors: &'a [(&'a str, bool)],
447    /// Every program output's logical operators (shot-independent).
448    pub outputs: &'a [OutputLogical],
449    /// Evaluated terminal Pauli frames for this shot, keyed by output port.
450    pub frames: &'a [ShotFrame],
451    /// Outputs saved at an earlier cut because their physical patch was reused.
452    pub saved_outputs: &'a [SavedOutput],
453    /// Live engine width. A fresh ancilla lives at this index (the engine grows
454    /// on demand for the gates/measurements a hook performs).
455    pub qubit_count: usize,
456}
457
458/// Verify a whole [`Bloq`] program over a seeded shot batch.
459///
460/// The program is flattened, then `shots` shots run on a shared simulator with
461/// full region control flow. Every detector must be constant across applicable,
462/// non-discarded shots; each `Observable` node's per-shot value is reported.
463///
464/// # Errors
465/// See [`ExecError`].
466pub fn run_bloq(bloq: &Bloq, shots: usize, seed: u64) -> Result<VerifyReport, ExecError> {
467    run_bloq_internal(
468        bloq,
469        shots,
470        seed,
471        RunMode::Plain,
472        &mut |_, _| Ok(()),
473        &mut |_, _| Ok(()),
474    )
475}
476
477/// Run [`run_bloq`] with a per-shot hook.
478///
479/// The hook receives the pristine post-program simulator after the program
480/// finishes but before the shot's state is dropped or output boundaries are
481/// measured. It is called only for non-discarded shots, with a [`ShotContext`]
482/// carrying the shot's records, stamped selectors, and [`OutputLogical`]s.
483///
484/// This is the entry point for out-of-band per-shot analysis a raw record fold
485/// cannot express — chiefly logical-state fidelity of a non-Clifford output
486/// (a `|T⟩` factory). A hook that mutates the simulator invalidates that shot's
487/// observable/frame reports (but not its detectors); the no-op hook of
488/// [`run_bloq`] leaves the state untouched, so its full report stays valid.
489///
490/// # Errors
491/// See [`ExecError`]; also propagates any error the `hook` returns.
492pub fn run_bloq_with_hook<F>(
493    bloq: &Bloq,
494    shots: usize,
495    seed: u64,
496    mut hook: F,
497) -> Result<VerifyReport, ExecError>
498where
499    F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
500{
501    run_bloq_internal(
502        bloq,
503        shots,
504        seed,
505        RunMode::Plain,
506        &mut |_, _| Ok(()),
507        &mut hook,
508    )
509}
510
511/// [`run_bloq`] with private input seeds and a post-shot output hook.
512///
513/// Every seed starts in `|+>`. The preparation hook may entangle seeds. Each is
514/// injected before its temporal input Port, or before the first wait/cube after
515/// an ordinary spatial input's virtual Port. Time-separated inputs may reuse
516/// one patch. Multiplex inputs inject before the virtual Port so its split sees them.
517///
518/// # Errors
519/// See [`ExecError`]; also propagates errors returned by either hook.
520pub fn run_bloq_with_io<P, F>(
521    bloq: &Bloq,
522    shots: usize,
523    seed: u64,
524    mut prepare: P,
525    mut peek: F,
526) -> Result<VerifyReport, ExecError>
527where
528    P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
529    F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
530{
531    run_bloq_internal(
532        bloq,
533        shots,
534        seed,
535        RunMode::PrepareInputs,
536        &mut prepare,
537        &mut peek,
538    )
539}
540
541/// Run [`run_bloq_with_hook`] while preserving reused output patches.
542///
543/// Each reused patch is swapped onto a private ancilla at its output cut. The
544/// hook reads those ancillas through [`ShotContext::saved_outputs`], allowing a
545/// joint-state oracle to include outputs no longer live at program termination.
546///
547/// As with any mutating hook, only the record-based detector portion of the
548/// returned report remains meaningful.
549///
550/// # Errors
551/// See [`ExecError`]; also propagates any error the `hook` returns.
552pub fn run_bloq_with_captured_outputs<F>(
553    bloq: &Bloq,
554    shots: usize,
555    seed: u64,
556    mut hook: F,
557) -> Result<VerifyReport, ExecError>
558where
559    F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
560{
561    run_bloq_internal(
562        bloq,
563        shots,
564        seed,
565        RunMode::CaptureOutputs,
566        &mut |_, _| Ok(()),
567        &mut hook,
568    )
569}
570
571/// The shared whole-program walk behind [`run_bloq`] and [`run_bloq_with_hook`].
572/// It calls one post-program `hook` per non-discarded shot (out-of-band
573/// analysis); the plain [`run_bloq`] path passes a no-op.
574fn run_bloq_internal<P, F>(
575    bloq: &Bloq,
576    shots: usize,
577    seed: u64,
578    mode: RunMode,
579    prepare: &mut P,
580    hook: &mut F,
581) -> Result<VerifyReport, ExecError>
582where
583    P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
584    F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
585{
586    let program = Program::prepare(bloq, mode)?;
587    let layout = program.report_layout()?;
588
589    let mut detector_shots: Vec<Vec<bool>> = vec![Vec::new(); layout.detectors.len()];
590    let mut observable_shots: Vec<Vec<bool>> = vec![Vec::new(); layout.observable_nodes.len()];
591    let mut frame_x: Vec<Vec<Option<bool>>> = vec![Vec::new(); layout.frames.len()];
592    let mut frame_z: Vec<Vec<Option<bool>>> = vec![Vec::new(); layout.frames.len()];
593    let mut discarded = 0usize;
594    let mut max_rank = 0usize;
595    let mut branch_selectors: Vec<bool> = Vec::new();
596    for shot in 0..shots {
597        let mut completed =
598            program.execute_shot(shot, seed, matches!(mode, RunMode::PrepareInputs), prepare)?;
599        max_rank = max_rank.max(completed.state.peak_rank);
600        branch_selectors.extend(completed.state.branch_choices.iter().copied());
601        if completed.state.discarded {
602            discarded += 1;
603            continue;
604        }
605        if program.core.bloq.logical_outputs().iter().any(|output| {
606            completed
607                .state
608                .excluded_instances
609                .contains(&output.instance)
610        }) {
611            return Err(ExecError::MalformedGraph(
612                "logical output owner is not selected",
613            ));
614        }
615
616        // Per-shot hook on the pristine post-program state (output patches still
617        // live). Runs before the observable/frame folds below so a mutating hook
618        // sees an undisturbed boundary; a mutating hook thus voids this shot's
619        // observable/frame reports but not its record-based detectors.
620        {
621            let shot_frames = layout
622                .frames
623                .iter()
624                .map(|frame| ShotFrame {
625                    port: frame.port,
626                    x: completed.top_values.get(&frame.x.0).copied().flatten(),
627                    z: completed.top_values.get(&frame.z.0).copied().flatten(),
628                })
629                .collect::<Vec<_>>();
630            let saved_outputs = program
631                .early_output_captures
632                .iter()
633                .zip(&completed.state.saved_outputs)
634                .filter_map(|(capture, &qubit)| {
635                    qubit.map(|qubit| SavedOutput {
636                        port: capture.port,
637                        qubit,
638                    })
639                })
640                .collect::<Vec<_>>();
641            let named_branch_selectors = program
642                .core
643                .bloq
644                .nodes()
645                .filter_map(|(id, node)| {
646                    let bloq_ir::NodeProvenance::BranchSelector { name } = &node.provenance else {
647                        return None;
648                    };
649                    let value = completed
650                        .state
651                        .classical_values
652                        .get(&(LevelPath::default(), id))
653                        .copied()
654                        .flatten()?;
655                    Some((name.as_str(), value))
656                })
657                .collect::<Vec<_>>();
658            let ctx = ShotContext {
659                shot,
660                record: &completed.state.record,
661                branch_selectors: &completed.state.branch_choices,
662                named_branch_selectors: &named_branch_selectors,
663                outputs: &layout.outputs,
664                frames: &shot_frames,
665                saved_outputs: &saved_outputs,
666                qubit_count: completed.state.sim.num_qubits(),
667            };
668            hook(&mut completed.state.sim, &ctx)?;
669        }
670
671        // Detectors owned by inactive memberships or regions are skipped.
672        // Any other missing record is a hard error.
673        for ((scope, parity), out) in layout.detectors.iter().zip(&mut detector_shots) {
674            if !parity.applies(scope, &completed.state) {
675                continue;
676            }
677            let value = xor_records(parity, scope, &completed.state)?;
678            out.push(value);
679        }
680        for (node_id, out) in layout.observable_nodes.iter().zip(&mut observable_shots) {
681            if let Some(value) = program.observable_value(
682                *node_id,
683                &completed.top_values,
684                &completed.top_bindings,
685                &mut completed.state,
686            )? {
687                out.push(value);
688            }
689        }
690        for (i, frame) in layout.frames.iter().enumerate() {
691            frame_x[i].push(completed.top_values.get(&frame.x.0).copied().flatten());
692            frame_z[i].push(completed.top_values.get(&frame.z.0).copied().flatten());
693        }
694    }
695
696    Ok(VerifyReport {
697        shots,
698        discarded,
699        detectors: detector_shots.into_iter().map(detector_report).collect(),
700        observables: observable_shots
701            .into_iter()
702            .map(|per_shot| ObservableReport { per_shot })
703            .collect(),
704        frame_pairs: layout
705            .frames
706            .iter()
707            .zip(frame_x)
708            .zip(frame_z)
709            .map(|((frame, x_bits), z_bits)| FramePairReport {
710                port: frame.port,
711                x_bits,
712                z_bits,
713            })
714            .collect(),
715        max_rank,
716        branch_selectors,
717    })
718}
719
720impl Program {
721    fn prepare(bloq: &Bloq, mode: RunMode) -> Result<Self, ExecError> {
722        let core = PreparedProgramCore::build(bloq)?;
723        let rus_restarts = collect_rus_restarts(&core.bloq, &core.global_meas)?;
724        let early_reads = super::closure::plan_early_output_reads(&core.bloq)?;
725        let output_cuts = super::closure::logical_output_cuts(&core.bloq)?;
726        let mut program = Self {
727            boundary_scratch: RefCell::new(PauliString::new(core.coord_index.len())),
728            core,
729            inputs: Vec::new(),
730            rus_restarts,
731            early_reads,
732            output_cuts,
733            early_output_captures: Vec::new(),
734        };
735        if matches!(mode, RunMode::PrepareInputs) {
736            program.inputs = program.input_patches()?;
737        }
738        if matches!(mode, RunMode::CaptureOutputs) {
739            program.early_output_captures = program.plan_early_output_captures()?;
740        }
741        if program.early_output_captures.is_empty() {
742            program.validate_early_read_compatibility()?;
743        }
744        Ok(program)
745    }
746
747    fn validate_early_read_compatibility(&self) -> Result<(), ExecError> {
748        let operators = self
749            .early_reads
750            .iter()
751            .map(|read| {
752                let mut product = PauliString::new(self.core.coord_index.len());
753                for binding in &read.bindings {
754                    product = &product
755                        * self
756                            .core
757                            .boundary_operator(&binding.operator(&self.core.bloq).operator)?;
758                }
759                Ok(product)
760            })
761            .collect::<Result<Vec<_>, ExecError>>()?;
762        for (index, first) in self.early_reads.iter().enumerate() {
763            for (other, second) in self.early_reads[..index].iter().enumerate() {
764                if first.before == second.before
765                    && (first.incomplete || second.incomplete)
766                    && !operators[index].commutes_with(&operators[other])
767                {
768                    return Err(ExecError::IncompatibleEarlyOutputReads {
769                        first: first.observable.0,
770                        second: second.observable.0,
771                        before: first.before.1.0,
772                    });
773                }
774            }
775        }
776        Ok(())
777    }
778
779    fn report_layout(&self) -> Result<ProgramReportLayout, ExecError> {
780        let mut detectors = collect_detectors(
781            &self.core.bloq,
782            self.core.bloq.top(),
783            &self.core.global_meas,
784        )?;
785        // Only ordinary spatial inputs inject after their temporal Port.
786        detectors.retain(|(_, detector)| {
787            !self.inputs.iter().any(|input| {
788                !input.prepare_before.contains(&input.input_instance)
789                    && detector
790                        .terms
791                        .iter()
792                        .any(|(instance, _)| *instance == input.input_instance)
793                    && detector
794                        .terms
795                        .iter()
796                        .any(|(instance, _)| input.prepare_before.contains(instance))
797            })
798        });
799        let observable_nodes = collect_observables(&self.core.bloq);
800        super::closure::guard_decode_observable_closure(&self.core.bloq)?;
801        let outputs = self.output_logicals()?;
802        let frames = self.core.bloq.output_frames();
803        for output in self.core.bloq.logical_outputs() {
804            if !frames.iter().any(|frame| frame.port == output.port) {
805                return Err(ExecError::MalformedGraph(
806                    "logical output has no complete frame pair",
807                ));
808            }
809        }
810        Ok(ProgramReportLayout {
811            detectors,
812            observable_nodes,
813            outputs,
814            frames,
815        })
816    }
817
818    fn execute_shot<P>(
819        &self,
820        shot: usize,
821        seed: u64,
822        stage_inputs: bool,
823        prepare: &mut P,
824    ) -> Result<CompletedShot<'_>, ExecError>
825    where
826        P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
827    {
828        let shot_seed = seed ^ shot as u64;
829        let base_qubits = self.core.coord_index.len();
830        let mut state = Shot {
831            sim: Simulator::with_seed(base_qubits, shot_seed),
832            staged_inputs: StagedInputSeeds::default(),
833            injected_inputs: vec![false; self.inputs.len()],
834            deferred_ccz: Vec::new(),
835            record: ShotRecord::with_len(self.core.record_count),
836            discarded: false,
837            peak_rank: 1,
838            excluded_instances: rustc_hash::FxHashSet::default(),
839            executed_scopes: rustc_hash::FxHashSet::default(),
840            active_membership_inputs: rustc_hash::FxHashSet::default(),
841            branch_choices: Vec::new(),
842            early_faces: vec![None; self.early_reads.len()],
843            classical_values: FxHashMap::default(),
844            saved_outputs: vec![None; self.early_output_captures.len()],
845            seed: shot_seed,
846        };
847        if stage_inputs {
848            let mut seeds = Vec::with_capacity(self.inputs.len());
849            for input in &self.inputs {
850                let qubit = state.sim.num_qubits();
851                state.sim.reset_x(qubit)?;
852                seeds.push(InputSeed {
853                    port: input.port,
854                    qubit,
855                });
856            }
857            let deferred_ccz = RefCell::new(Vec::new());
858            prepare(
859                &mut state.sim,
860                &PreparationContext {
861                    shot,
862                    inputs: &seeds,
863                    deferred_ccz: &deferred_ccz,
864                },
865            )?;
866            for ports in deferred_ccz.into_inner() {
867                let mut prepared = DeferredCcz {
868                    seeds: [0; 3],
869                    prepare_before: std::array::from_fn(|_| Vec::new()),
870                    prepared: false,
871                };
872                for (index, port) in ports.into_iter().enumerate() {
873                    let input_index = self
874                        .inputs
875                        .iter()
876                        .position(|input| input.port == port)
877                        .ok_or(ExecError::UnknownPreparationInput { port })?;
878                    prepared.seeds[index] = seeds[input_index].qubit;
879                    prepared.prepare_before[index] =
880                        self.inputs[input_index].prepare_before.clone();
881                }
882                state.deferred_ccz.push(prepared);
883            }
884            for (index, (input, seed)) in self.inputs.iter().zip(seeds).enumerate() {
885                for &target in &input.prepare_before {
886                    state
887                        .staged_inputs
888                        .entry(target)
889                        .or_default()
890                        .push((index, seed.qubit));
891                }
892            }
893        }
894        state.peak_rank = state.peak_rank.max(state.sim.rank());
895        let mut top_values = FxHashMap::default();
896        let mut top_bindings = FxHashMap::default();
897        self.execute_level(
898            self.core.bloq.top(),
899            &LevelPath::default(),
900            &mut state,
901            &mut top_values,
902            &mut top_bindings,
903        )?;
904        Ok(CompletedShot {
905            state,
906            top_values,
907            top_bindings,
908        })
909    }
910
911    fn restart_signaled(&self, scope: &LevelPath, state: &Shot) -> Result<bool, ExecError> {
912        for (owner, parity) in self.rus_restarts.get(scope).into_iter().flatten() {
913            if parity.applies(owner, state) && xor_records(parity, owner, state)? {
914                return Ok(true);
915            }
916        }
917        Ok(false)
918    }
919
920    /// Execute one [`SubGraph`] level in emit order on the shared simulator.
921    /// `scope` is the enclosing region chain (empty at the top level); `values`
922    /// holds this level's classical node values.
923    fn execute_level<'a>(
924        &'a self,
925        level: &'a SubGraph,
926        scope: &LevelPath,
927        state: &mut Shot,
928        values: &mut FxHashMap<u32, Option<bool>>,
929        bindings: &mut FxHashMap<u32, BoundOperators<'a>>,
930    ) -> Result<(), ExecError> {
931        state.executed_scopes.insert(scope.clone());
932        for &id in self.core.emit_order(scope)? {
933            let node = level.node(id).expect("emit order yields real ids");
934            if let Some(slot) = node.activation {
935                let inputs = self.slot_inputs(level, id, values);
936                let active = inputs.get(&slot).copied().ok_or(ExecError::MalformedGraph(
937                    "classical activation input is unavailable",
938                ))?;
939                if !active {
940                    if let Some(region) = node.try_region() {
941                        for (_, body) in region.bodies() {
942                            collect_instances(body, &mut state.excluded_instances);
943                        }
944                    } else if let Some(quantum) = node.try_quantum() {
945                        state
946                            .excluded_instances
947                            .extend(quantum.instances.iter().map(|instance| instance.id));
948                    }
949                    values.insert(id.0, Some(false));
950                    state
951                        .classical_values
952                        .insert((scope.clone(), id), Some(false));
953                    continue;
954                }
955            }
956            if !self.early_output_captures.is_empty() {
957                self.capture_early_outputs(scope, id, state)?;
958            } else if !self.early_reads.is_empty() {
959                self.read_early_faces(scope, id, state)?;
960            }
961            match (node.try_classical(), &node.kind) {
962                (_, BloqNodeKind::Quantum(quantum)) => {
963                    if quantum.guards.is_empty() {
964                        self.inject_input_seeds(quantum, state)?;
965                        self.run_quantum_node(id, scope, state)?;
966                    } else {
967                        let inputs = self.slot_inputs(level, id, values);
968                        let selected = self.core.selected_ops(scope, id, node, &inputs)?;
969                        for guard in &quantum.guards {
970                            if inputs[&guard.input] {
971                                state.active_membership_inputs.insert((
972                                    scope.clone(),
973                                    id,
974                                    guard.input,
975                                ));
976                            } else {
977                                state.excluded_instances.extend(&guard.instances);
978                            }
979                        }
980                        state.record.select_aliases(&selected.aliases);
981                        self.inject_input_seeds(&selected.quantum, state)?;
982                        let rank = selected.ops.run(&mut state.sim, &mut state.record)?;
983                        state.peak_rank = state.peak_rank.max(rank);
984                    }
985                }
986                (Some(ClassicalNode::Discard { condition }), _) => {
987                    let inputs = self.slot_inputs(level, id, values);
988                    state.discarded = condition
989                        .eval(&mut |slot| inputs.get(&slot).copied())
990                        .ok_or(ExecError::MalformedGraph(
991                            "discard condition is unavailable",
992                        ))?;
993                }
994                (_, BloqNodeKind::Classical(classical)) => {
995                    let classical = classical.as_ref();
996                    // Frame-content recipes fold over the shot record.
997                    let value = self.classical_value(
998                        level,
999                        id,
1000                        classical,
1001                        values,
1002                        &state.record,
1003                        &state.excluded_instances,
1004                    )?;
1005                    values.insert(id.0, value);
1006                    if matches!(
1007                        node.provenance,
1008                        bloq_ir::NodeProvenance::BranchSelector { .. }
1009                    ) {
1010                        state
1011                            .branch_choices
1012                            .push(value.ok_or(ExecError::MalformedGraph(
1013                                "branch selector has no value",
1014                            ))?);
1015                        state.classical_values.insert((scope.clone(), id), value);
1016                    }
1017                    if let ClassicalNode::Observable { operators, .. } = classical {
1018                        let children = level
1019                            .data_inputs(id)
1020                            .filter(|input| input.output.is_none())
1021                            .map(|input| input.producer)
1022                            .filter(|child| {
1023                                bindings
1024                                    .get(&child.0)
1025                                    .is_some_and(BoundOperators::has_output)
1026                            })
1027                            .collect();
1028                        bindings.insert(
1029                            id.0,
1030                            BoundOperators::Recipe(
1031                                operators
1032                                    .iter()
1033                                    .filter(|operator| {
1034                                        operator.face == bloq_ir::BoundaryFace::Output
1035                                    })
1036                                    .collect(),
1037                                children,
1038                            ),
1039                        );
1040                    }
1041                }
1042                (_, BloqNodeKind::Region(region)) => {
1043                    self.execute_region(level, id, region, scope, state, values, bindings)?
1044                }
1045            }
1046            if !self.early_reads.is_empty()
1047                && let Some(&value) = values.get(&id.0)
1048            {
1049                state.classical_values.insert((scope.clone(), id), value);
1050            }
1051            if state.discarded {
1052                // SEM-DISCARD: no later scheduled node executes in this shot.
1053                break;
1054            }
1055        }
1056        Ok(())
1057    }
1058
1059    /// Region control flow (see the [module docs](self)).
1060    // Threaded walk state (level/scope/state/values) alongside the node params —
1061    // a tree walk, not a public API surface.
1062    #[expect(
1063        clippy::too_many_arguments,
1064        reason = "recursive execution threads one region-walk state"
1065    )]
1066    fn execute_region<'a>(
1067        &'a self,
1068        level: &'a SubGraph,
1069        id: BloqNodeId,
1070        region: &'a RegionNode,
1071        scope: &LevelPath,
1072        state: &mut Shot,
1073        values: &mut FxHashMap<u32, Option<bool>>,
1074        bindings: &mut FxHashMap<u32, BoundOperators<'a>>,
1075    ) -> Result<(), ExecError> {
1076        let RegionNode::RepeatUntilSuccess {
1077            body,
1078            restart_condition,
1079            restart_source,
1080        } = region;
1081        let child = scope.child(id, BodySelector::Body);
1082        let snapshot = ShotSnapshot::capture(state);
1083        let mut attempt = 0;
1084        loop {
1085            if attempt != 0 {
1086                snapshot.restore(state);
1087            }
1088            state
1089                .sim
1090                .reseed_rng(rus_attempt_seed(state.seed, &child, attempt));
1091
1092            let mut body_values = FxHashMap::default();
1093            let mut body_bindings = FxHashMap::default();
1094            self.execute_level(body, &child, state, &mut body_values, &mut body_bindings)?;
1095            // Both mandatory restart signals are evaluated unconditionally
1096            // (no short-circuit): an unevaluable restart condition is an
1097            // error even on an attempt a restart parity already rejected.
1098            let restart_detector = self.restart_signaled(&child, state)?;
1099            let open = restart_source.and_then(|source| {
1100                zero_decoder_output(
1101                    body_values.get(&source.node.0).copied().flatten(),
1102                    Some(source.output),
1103                )
1104            });
1105            let restart_predicate = restart_condition
1106                .eval(&mut |slot| {
1107                    let input = level.value_inputs(id).find(|input| input.slot == slot);
1108                    match input {
1109                        Some(input) => zero_decoder_output(
1110                            values.get(&input.producer.0).copied().flatten(),
1111                            input.output,
1112                        ),
1113                        None => open,
1114                    }
1115                })
1116                .ok_or(ExecError::MalformedGraph(
1117                    "RUS restart condition is not evaluable",
1118                ))?;
1119
1120            if !(restart_detector || restart_predicate) {
1121                // Attempt-local draws must not perturb the parent RNG stream.
1122                state.sim.restore_rng_from(&snapshot.sim);
1123                values.insert(id.0, bound_body_value(body, &body_values));
1124                bindings.insert(
1125                    id.0,
1126                    BoundOperators::Owned(bound_body_bindings(body, &body_bindings)?),
1127                );
1128                break;
1129            }
1130            attempt += 1;
1131            if attempt >= DEFAULT_RUS_ATTEMPT_CAP {
1132                state.discarded = true;
1133                break;
1134            }
1135        }
1136        Ok(())
1137    }
1138
1139    fn inject_input_seeds(
1140        &self,
1141        quantum: &bloq_ir::QuantumNode,
1142        state: &mut Shot,
1143    ) -> Result<(), ExecError> {
1144        for preparation in &mut state.deferred_ccz {
1145            if !preparation.prepared
1146                && preparation
1147                    .prepare_before
1148                    .iter()
1149                    .flatten()
1150                    .any(|cut| quantum.instances.iter().any(|instance| instance.id == *cut))
1151            {
1152                let [a, b, c] = preparation.seeds;
1153                state.sim.ccz(a, b, c)?;
1154                preparation.prepared = true;
1155            }
1156        }
1157        for instance in &quantum.instances {
1158            for &(input_index, seed) in state.staged_inputs.get(&instance.id).into_iter().flatten()
1159            {
1160                if state.injected_inputs[input_index] {
1161                    continue;
1162                }
1163                let input = &self.inputs[input_index];
1164                let corner = input.prepare_injection_scaffold(&mut state.sim)?;
1165                state.sim.swap(seed, corner);
1166                input.postselect_injection_patch(&mut state.sim)?;
1167                state.injected_inputs[input_index] = true;
1168            }
1169        }
1170        Ok(())
1171    }
1172
1173    /// Replay a quantum node's prepared instruction stream.
1174    fn run_quantum_node(
1175        &self,
1176        id: BloqNodeId,
1177        scope: &LevelPath,
1178        state: &mut Shot,
1179    ) -> Result<(), ExecError> {
1180        let max_rank = self
1181            .core
1182            .prepared_ops(scope, id)?
1183            .run(&mut state.sim, &mut state.record)?;
1184        state.peak_rank = state.peak_rank.max(max_rank);
1185        Ok(())
1186    }
1187
1188    /// The `Value` output of a record-based classical producer in one shot.
1189    fn classical_value(
1190        &self,
1191        level: &SubGraph,
1192        id: BloqNodeId,
1193        classical: &ClassicalNode,
1194        values: &FxHashMap<u32, Option<bool>>,
1195        record: &ShotRecord,
1196        excluded: &rustc_hash::FxHashSet<TemplateInstanceId>,
1197    ) -> Result<Option<bool>, ExecError> {
1198        let mut record_value = false;
1199        for im in classical.measurements() {
1200            if excluded.contains(&im.instance) {
1201                continue;
1202            }
1203            let gid = *self
1204                .core
1205                .global_meas
1206                .get(im)
1207                .ok_or(ExecError::MalformedGraph(
1208                    "observable names an unknown measurement",
1209                ))?;
1210            record_value ^= record.require(gid)?;
1211        }
1212        match classical {
1213            ClassicalNode::Compute { expr } => {
1214                let inputs = self.slot_inputs(level, id, values);
1215                Ok(expr.eval(&mut |slot| inputs.get(&slot).copied()))
1216            }
1217            // Recipes XOR records and data inputs. Output boundary operators
1218            // fold at report time ([`Self::observable_value`]); an unavailable
1219            // data input keeps the recipe unavailable.
1220            ClassicalNode::Observable { .. } => {
1221                let mut value = record_value;
1222                for input in level.data_inputs(id) {
1223                    match zero_decoder_output(
1224                        values.get(&input.producer.0).copied().flatten(),
1225                        input.output,
1226                    ) {
1227                        Some(bit) => value ^= bit,
1228                        None => return Ok(None),
1229                    }
1230                }
1231                Ok(Some(value))
1232            }
1233            ClassicalNode::Discard { .. } => Ok(None),
1234        }
1235    }
1236
1237    /// This level's `Value` inputs of `id`, resolved to `slot → bit` from
1238    /// already-computed producer values.
1239    fn slot_inputs(
1240        &self,
1241        level: &SubGraph,
1242        id: BloqNodeId,
1243        values: &FxHashMap<u32, Option<bool>>,
1244    ) -> FxHashMap<u32, bool> {
1245        let mut inputs = FxHashMap::default();
1246        for input in level.value_inputs(id) {
1247            if let Some(bit) = zero_decoder_output(
1248                values.get(&input.producer.0).copied().flatten(),
1249                input.output,
1250            ) {
1251                inputs.insert(input.slot, bit);
1252            }
1253        }
1254        inputs
1255    }
1256
1257    /// An `Observable` node's value: the XOR of its record producers plus any
1258    /// live `Output`-face boundary Pauli measured on the final state.
1259    ///
1260    /// `Input` faces contribute **nothing** to the physical fold. An
1261    /// input boundary operator is a *source endpoint* (e.g. a T block's
1262    /// magic-state anchor), closed internally by the cultivation seam when the
1263    /// templates compose. Its MPP-boundary
1264    /// realization is the *static* verifier's perfect-port stand-in; `run_bloq`
1265    /// executes the composed circuit, where that closure is already carried by the
1266    /// folded cultivation records, and the input face no longer
1267    /// exists as a live boundary. Measuring it on the terminal state would read
1268    /// the wrong operator at the wrong time (the patch has since been
1269    /// merged/consumed), so we skip it. Only `Output` faces are live terminal
1270    /// boundaries and are measured here. [`super::closure::plan_early_output_reads`]
1271    /// moves a consumed face's read ahead of the node that reuses its support.
1272    fn observable_value(
1273        &self,
1274        obs: BloqNodeId,
1275        top_values: &FxHashMap<u32, Option<bool>>,
1276        top_bindings: &FxHashMap<u32, BoundOperators<'_>>,
1277        state: &mut Shot,
1278    ) -> Result<Option<bool>, ExecError> {
1279        let guard = self.core.bloq.top()[obs].activation;
1280        if let Some(slot) = guard {
1281            let inputs = self.slot_inputs(self.core.bloq.top(), obs, top_values);
1282            if inputs.get(&slot) != Some(&true) {
1283                return Ok(None);
1284            }
1285        }
1286        let Some(mut value) = top_values.get(&obs.0).copied().flatten() else {
1287            return Ok(None);
1288        };
1289        let mut terminal_operator: Option<&PauliString> = None;
1290        let mut combined = false;
1291        let mut boundary_scratch = self.boundary_scratch.borrow_mut();
1292        let mut output_operators = self.core.bloq.top()[obs]
1293            .try_classical()
1294            .expect("observable is classical")
1295            .operators()
1296            .iter()
1297            .filter(|operator| operator.face == bloq_ir::BoundaryFace::Output)
1298            .collect::<Vec<_>>();
1299        let mut expanded = 0;
1300        for input in self
1301            .core
1302            .bloq
1303            .top()
1304            .data_inputs(obs)
1305            .filter(|input| input.output.is_none())
1306        {
1307            collect_bound_operators(
1308                top_bindings,
1309                input.producer,
1310                &mut output_operators,
1311                &mut expanded,
1312            )?;
1313        }
1314        let mut cached_bindings = Vec::new();
1315        for (read, bit) in self.early_reads.iter().zip(&state.early_faces) {
1316            if let Some(bit) = bit
1317                && read.observable == obs
1318                && read.bindings.iter().all(|binding| {
1319                    output_operators
1320                        .iter()
1321                        .any(|operator| std::ptr::eq(binding.operator(&self.core.bloq), *operator))
1322                })
1323            {
1324                value ^= bit;
1325                cached_bindings.extend(&read.bindings);
1326            }
1327        }
1328        for operator in output_operators {
1329            if !cached_bindings
1330                .iter()
1331                .any(|binding| std::ptr::eq(binding.operator(&self.core.bloq), operator))
1332            {
1333                let prepared = self.core.boundary_operator(&operator.operator)?;
1334                if let Some(first) = terminal_operator {
1335                    if !combined {
1336                        boundary_scratch.clone_from(first);
1337                        combined = true;
1338                    }
1339                    // Keep the exact phase: XX * ZZ = -YY.
1340                    *boundary_scratch = &*boundary_scratch * prepared;
1341                } else {
1342                    terminal_operator = Some(prepared);
1343                }
1344            }
1345        }
1346        if let Some(first) = terminal_operator {
1347            let pauli = if combined { &boundary_scratch } else { first };
1348            value ^= Self::measure_boundary_pauli(pauli, &mut state.sim)?;
1349        }
1350        Ok(Some(value))
1351    }
1352
1353    fn plan_early_output_captures(&self) -> Result<Vec<EarlyOutputCapture>, ExecError> {
1354        let captures = self
1355            .core
1356            .bloq
1357            .logical_outputs()
1358            .iter()
1359            .zip(&self.output_cuts)
1360            .filter(|(_, cuts)| !cuts.is_empty())
1361            .map(|(output, cuts)| {
1362                Ok((
1363                    cuts.clone(),
1364                    output.instance,
1365                    output.port,
1366                    self.joint_pauli(&output.x)?,
1367                    self.joint_pauli(&output.z)?,
1368                ))
1369            })
1370            .collect::<Result<Vec<_>, ExecError>>()?;
1371        let base_width = self.core.coord_index.len();
1372        let variant_count = captures.len();
1373        // ponytail: C² width variants keep the shot path allocation-free;
1374        // replace them if captured-output counts grow beyond small verifier fixtures.
1375        Ok(captures
1376            .into_iter()
1377            .map(
1378                |(before, instance, port, logical_x, logical_z)| EarlyOutputCapture {
1379                    before,
1380                    instance,
1381                    port,
1382                    operators: (0..variant_count)
1383                        .map(|extra| {
1384                            let qubit = base_width + extra;
1385                            let width = qubit + 1;
1386                            EarlyOutputPaulis {
1387                                logical_x: super::circuit::widen_pauli(&logical_x, width),
1388                                logical_z: super::circuit::widen_pauli(&logical_z, width),
1389                                ancilla_x: PauliString::single(width, qubit, Pauli::X),
1390                                ancilla_z: PauliString::single(width, qubit, Pauli::Z),
1391                            }
1392                        })
1393                        .collect(),
1394                },
1395            )
1396            .collect())
1397    }
1398
1399    /// Swap a logical output away immediately before another node reuses its
1400    /// patch. The program's Pauli frame remains for the terminal hook to apply.
1401    fn capture_early_outputs(
1402        &self,
1403        scope: &LevelPath,
1404        id: BloqNodeId,
1405        state: &mut Shot,
1406    ) -> Result<(), ExecError> {
1407        for (slot, capture) in self.early_output_captures.iter().enumerate() {
1408            if !capture
1409                .before
1410                .iter()
1411                .any(|(cut_scope, cut_id)| *cut_id == id && cut_scope == scope)
1412                || state.saved_outputs[slot].is_some()
1413                || state.excluded_instances.contains(&capture.instance)
1414            {
1415                continue;
1416            }
1417            let qubit = state.sim.num_qubits();
1418            let operators = capture
1419                .operators
1420                .get(qubit.saturating_sub(self.core.coord_index.len()))
1421                .ok_or(ExecError::MalformedGraph(
1422                    "early-output capture has no prepared width",
1423                ))?;
1424            state.sim.reset(qubit)?;
1425            state
1426                .sim
1427                .controlled_pauli(&operators.logical_z, &operators.ancilla_x)?;
1428            state
1429                .sim
1430                .controlled_pauli(&operators.ancilla_z, &operators.logical_x)?;
1431            state
1432                .sim
1433                .controlled_pauli(&operators.logical_z, &operators.ancilla_x)?;
1434            state.saved_outputs[slot] = Some(qubit);
1435        }
1436        Ok(())
1437    }
1438
1439    /// Resolve pure classical dataflow at a cut, including computations whose
1440    /// inputs are ready but whose nodes have not yet been scheduled.
1441    fn available_value(
1442        &self,
1443        scope: &LevelPath,
1444        id: BloqNodeId,
1445        state: &Shot,
1446        memo: &mut FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
1447    ) -> Result<Option<bool>, ExecError> {
1448        let key = (scope.clone(), id);
1449        if let Some(&value) = state.classical_values.get(&key).or_else(|| memo.get(&key)) {
1450            return Ok(value);
1451        }
1452        let level = self
1453            .core
1454            .bloq
1455            .level_at(scope)
1456            .expect("binding scope exists");
1457        let Some(classical) = level[id].try_classical() else {
1458            return Ok(None);
1459        };
1460        if let Some(slot) = level[id].activation {
1461            let input = level
1462                .value_inputs(id)
1463                .find(|input| input.slot == slot)
1464                .ok_or(ExecError::MalformedGraph(
1465                    "classical guard has no value input",
1466                ))?;
1467            match zero_decoder_output(
1468                self.available_value(scope, input.producer, state, memo)?,
1469                input.output,
1470            ) {
1471                Some(true) => {}
1472                Some(false) => {
1473                    memo.insert(key, Some(false));
1474                    return Ok(Some(false));
1475                }
1476                None => return Ok(None),
1477            }
1478        }
1479        let mut values = FxHashMap::default();
1480        for input in level.data_inputs(id) {
1481            values.insert(
1482                input.producer.0,
1483                self.available_value(scope, input.producer, state, memo)?,
1484            );
1485        }
1486        let value = match self.classical_value(
1487            level,
1488            id,
1489            classical,
1490            &values,
1491            &state.record,
1492            &state.excluded_instances,
1493        ) {
1494            Err(ExecError::MissingRecord(_)) => Ok(None),
1495            result => result,
1496        }?;
1497        memo.insert(key, value);
1498        Ok(value)
1499    }
1500
1501    fn binding_active(
1502        &self,
1503        binding: &super::closure::OutputBinding,
1504        state: &Shot,
1505        memo: &mut FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
1506    ) -> Result<Option<bool>, ExecError> {
1507        let mut active = Some(true);
1508        for (scope, guard) in &binding.guards {
1509            match zero_decoder_output(
1510                self.available_value(scope, guard.node, state, memo)?,
1511                Some(guard.output),
1512            ) {
1513                Some(false) => return Ok(Some(false)),
1514                None => active = None,
1515                Some(true) => {}
1516            }
1517        }
1518        Ok(active)
1519    }
1520
1521    /// Take the planned reads of any `Output` face node `id` is about to destroy
1522    /// (see [`super::closure::plan_early_output_reads`]).
1523    fn read_early_faces(
1524        &self,
1525        scope: &LevelPath,
1526        id: BloqNodeId,
1527        state: &mut Shot,
1528    ) -> Result<(), ExecError> {
1529        // LIM-022: decide every gate at this cut before projecting any face.
1530        // Unknown values are cached only here: a later cut may have their records.
1531        let mut memo = FxHashMap::default();
1532        let reads = self
1533            .early_reads
1534            .iter()
1535            .enumerate()
1536            .filter(|(slot, read)| {
1537                read.before == (scope.clone(), id) && state.early_faces[*slot].is_none()
1538            })
1539            .map(|(slot, read)| {
1540                let active = self
1541                    .binding_active(&read.bindings[0], state, &mut memo)?
1542                    .ok_or(ExecError::EarlyOutputGateUnavailable {
1543                        observable: read.observable.0,
1544                        before: id.0,
1545                    })?;
1546                Ok((slot, read, active))
1547            })
1548            .collect::<Result<Vec<_>, ExecError>>()?;
1549        for (slot, read, active) in reads {
1550            if !active {
1551                continue;
1552            }
1553            let operators = read
1554                .bindings
1555                .iter()
1556                .filter(|binding| {
1557                    !self
1558                        .early_reads
1559                        .iter()
1560                        .zip(&state.early_faces)
1561                        .any(|(earlier, bit)| {
1562                            earlier.observable == read.observable
1563                                && bit.is_some()
1564                                && earlier.bindings.contains(binding)
1565                        })
1566                })
1567                .map(|binding| binding.operator(&self.core.bloq))
1568                .collect::<Vec<_>>();
1569            if operators.is_empty() {
1570                continue;
1571            }
1572            let bit = if let [operator] = operators.as_slice() {
1573                self.measure_boundary(operator, &mut state.sim)?
1574            } else {
1575                let mut pauli = PauliString::new(self.core.coord_index.len());
1576                for operator in operators {
1577                    pauli = &pauli * self.core.boundary_operator(&operator.operator)?;
1578                }
1579                Self::measure_boundary_pauli(&pauli, &mut state.sim)?
1580            };
1581            state.early_faces[slot] = Some(bit);
1582        }
1583        Ok(())
1584    }
1585
1586    /// Read a boundary-operator Pauli (a logical face). The operator's
1587    /// coordinates are already instance-global (translated at lowering — see
1588    /// [`bloq_ir::InstanceBoundaryOperator`]), so they resolve directly; the
1589    /// instance lookup only validates that the graph names a known instance.
1590    ///
1591    /// A live logical face is deterministic, so the non-collapsing expectation
1592    /// reads it without disturbing the state — which is what makes an early read
1593    /// at a cut safe, with the rest of the program still to run. A face with no
1594    /// definite value (`⟨P⟩ = 0`, or a fractional Clifford+T expectation) has
1595    /// nothing to read non-destructively, so it is projected, as the terminal
1596    /// read has always done.
1597    fn measure_boundary(
1598        &self,
1599        operator: &bloq_ir::lowering::InstanceBoundaryOperator,
1600        sim: &mut Simulator,
1601    ) -> Result<bool, ExecError> {
1602        if !self.core.instance_offset.contains_key(&operator.instance) {
1603            return Err(ExecError::MalformedGraph(
1604                "boundary operator names an unknown instance",
1605            ));
1606        }
1607        if operator.operator.is_empty() {
1608            return Err(ExecError::EmptyMppProduct);
1609        }
1610        let pauli = self.core.boundary_operator(&operator.operator)?;
1611        Self::measure_boundary_pauli(pauli, sim)
1612    }
1613
1614    fn measure_boundary_pauli(pauli: &PauliString, sim: &mut Simulator) -> Result<bool, ExecError> {
1615        let expectation = sim.peek_observable_expectation(pauli)?;
1616        if (1.0 - expectation.abs()) < DETERMINISTIC_EXPECTATION_TOLERANCE {
1617            return Ok(expectation < 0.0);
1618        }
1619        Ok(sim.measure_observable(pauli)?.outcome)
1620    }
1621
1622    /// Build the joint Pauli of a `PauliMap` in global engine indices. The map's
1623    /// coordinates must already be instance-global — boundary operators are
1624    /// translated by their instance offset at lowering (see
1625    /// [`bloq_ir::InstanceBoundaryOperator`]) — so the layout carries no offset;
1626    /// a caller holding patch-local coordinates translates them first.
1627    fn joint_pauli(&self, map: &PauliMap) -> Result<PauliString, ExecError> {
1628        super::circuit::product_pauli(
1629            map,
1630            &super::circuit::LayoutCtx {
1631                coord_to_index: &self.core.coord_index,
1632                offset: IVec2::ZERO,
1633                qubit_count: self.core.coord_index.len(),
1634            },
1635        )
1636    }
1637
1638    /// Physical patches and logical operators for every compiler-authored
1639    /// program input.
1640    fn input_patches(&self) -> Result<Vec<InputPatch>, ExecError> {
1641        let mut placements = FxHashMap::default();
1642        let mut spatial_cubes: FxHashMap<_, Vec<_>> = FxHashMap::default();
1643        let mut spatial_waits = FxHashMap::default();
1644        self.core.bloq.walk(|cx| {
1645            if let Some(quantum) = cx.node.try_quantum() {
1646                placements.extend(
1647                    quantum
1648                        .instances
1649                        .iter()
1650                        .map(|instance| (instance.id, (instance.template_id, instance.offset))),
1651                );
1652                for instance in &quantum.instances {
1653                    if matches!(
1654                        instance.provenance,
1655                        bloq_ir::InstanceProvenance::SpatialPortSubstitution {
1656                            role: bloq_ir::PortRole::Input,
1657                            part: bloq_ir::SpatialPortPart::TemporalPort,
1658                            ..
1659                        }
1660                    ) {
1661                        // Inject before held rounds; a later reset erases their syndrome history.
1662                        let wait = cx.level.outgoing(cx.id).find_map(|edge| {
1663                            if !matches!(edge.edge, bloq_ir::BloqEdge::Quantum(_)) {
1664                                return None;
1665                            }
1666                            let node = &cx.level[edge.target];
1667                            node.memory_rounds()?;
1668                            node.try_quantum()?
1669                                .instances
1670                                .iter()
1671                                .find(|wait| wait.offset == instance.offset)
1672                        });
1673                        if let Some(wait) = wait {
1674                            spatial_waits.insert(instance.id, wait.id);
1675                        }
1676                    }
1677                    if let bloq_ir::InstanceProvenance::SpatialPortSubstitution {
1678                        source,
1679                        role,
1680                        part: bloq_ir::SpatialPortPart::Cube,
1681                    } = instance.provenance
1682                        && role == bloq_ir::PortRole::Input
1683                    {
1684                        spatial_cubes.entry(source).or_default().push(instance.id);
1685                    }
1686                }
1687            }
1688            bloq_ir::WalkControl::Continue
1689        });
1690        self.core
1691            .bloq
1692            .logical_inputs()
1693            .iter()
1694            .map(|input| {
1695                let &(template_id, offset) =
1696                    placements
1697                        .get(&input.instance)
1698                        .ok_or(ExecError::MalformedGraph(
1699                            "logical input names an unknown instance",
1700                        ))?;
1701                let template = &self.core.bloq.templates()[template_id];
1702                let maps = template
1703                    .boundary_flows
1704                    .iter()
1705                    .filter(|flow| flow.start.is_empty() && !flow.end.is_empty())
1706                    .map(|flow| {
1707                        if flow.sign {
1708                            return Err(ExecError::MalformedGraph(
1709                                "input Port has a signed create stabilizer",
1710                            ));
1711                        }
1712                        Ok(flow.end.try_translated(offset)?)
1713                    })
1714                    .collect::<Result<Vec<_>, ExecError>>()?;
1715                let mut data = rustc_hash::FxHashSet::default();
1716                for map in &maps {
1717                    data.extend(map.iter().map(|(coordinate, _)| *coordinate));
1718                }
1719                let mut data = data.into_iter().collect::<Vec<_>>();
1720                data.sort_by_key(glam::IVec2::to_array);
1721                let data_qubits = data
1722                    .into_iter()
1723                    .map(|coordinate| {
1724                        self.core
1725                            .coord_index
1726                            .get(&coordinate)
1727                            .map(|&index| index as usize)
1728                            .ok_or(ExecError::UnknownCoord(coordinate))
1729                    })
1730                    .collect::<Result<Vec<_>, _>>()?;
1731                let stabilizers = maps
1732                    .iter()
1733                    .map(|map| self.joint_pauli(map))
1734                    .collect::<Result<Vec<_>, _>>()?;
1735                Ok(InputPatch {
1736                    input_instance: input.instance,
1737                    prepare_before: spatial_waits
1738                        .get(&input.instance)
1739                        .copied()
1740                        .map(|instance| vec![instance])
1741                        .or_else(|| spatial_cubes.get(&input.port).cloned())
1742                        .unwrap_or_else(|| vec![input.instance]),
1743                    port: input.port,
1744                    data_qubits,
1745                    stabilizers,
1746                    logical_x: self.joint_pauli(&input.x)?,
1747                    logical_z: self.joint_pauli(&input.z)?,
1748                })
1749            })
1750            .collect()
1751    }
1752
1753    /// The logical operators of every program output, for the per-shot hook.
1754    ///
1755    /// The IR stores both operators in layout-global coordinates; this method
1756    /// only resolves those coordinates to engine indices.
1757    fn output_logicals(&self) -> Result<Vec<OutputLogical>, ExecError> {
1758        let outputs = self.core.bloq.logical_outputs();
1759        outputs
1760            .iter()
1761            .zip(&self.output_cuts)
1762            .map(|(output, cuts)| {
1763                Ok(OutputLogical {
1764                    port: output.port,
1765                    logical_x: self.joint_pauli(&output.x)?,
1766                    logical_z: self.joint_pauli(&output.z)?,
1767                    consumed: !cuts.is_empty(),
1768                })
1769            })
1770            .collect()
1771    }
1772}
1773
1774// ============================================================
1775// Static collection (detectors, observables)
1776// ============================================================
1777
1778pub(super) fn collect_rus_restarts(
1779    bloq: &Bloq,
1780    global_meas: &FxHashMap<InstanceMeasurement, u32>,
1781) -> Result<FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>, ExecError> {
1782    fn visit_regions(
1783        bloq: &Bloq,
1784        level: &SubGraph,
1785        scope: &LevelPath,
1786        global_meas: &FxHashMap<InstanceMeasurement, u32>,
1787        out: &mut FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>,
1788    ) -> Result<(), ExecError> {
1789        for (id, node) in level.nodes() {
1790            let Some(region) = node.try_region() else {
1791                continue;
1792            };
1793            for (selector, body) in region.bodies() {
1794                let child = scope.child(id, selector);
1795                if matches!(region, RegionNode::RepeatUntilSuccess { .. }) {
1796                    let mut parities = Vec::new();
1797                    collect_restart_parities(bloq, body, &child, global_meas, &mut parities)?;
1798                    out.insert(child.clone(), parities);
1799                }
1800                visit_regions(bloq, body, &child, global_meas, out)?;
1801            }
1802        }
1803        Ok(())
1804    }
1805
1806    fn collect_restart_parities(
1807        bloq: &Bloq,
1808        level: &SubGraph,
1809        scope: &LevelPath,
1810        global_meas: &FxHashMap<InstanceMeasurement, u32>,
1811        out: &mut Vec<(LevelPath, ResolvedDetector)>,
1812    ) -> Result<(), ExecError> {
1813        for (id, node) in level.nodes() {
1814            if let Some(quantum) = node.try_quantum() {
1815                for instance in &quantum.instances {
1816                    let template = bloq.templates().get(instance.template_id).ok_or(
1817                        ExecError::MalformedGraph("instance references a missing template"),
1818                    )?;
1819                    for restart in &template.restarts {
1820                        out.push((
1821                            scope.clone(),
1822                            resolve_template_parity(&restart.parity, instance.id, global_meas)?
1823                                .guarded(
1824                                    id,
1825                                    quantum
1826                                        .guards
1827                                        .iter()
1828                                        .find(|guard| guard.instances.contains(&instance.id))
1829                                        .map(|guard| guard.input),
1830                                ),
1831                        ));
1832                    }
1833                }
1834                for (index, restart) in quantum.restarts.iter().enumerate() {
1835                    let restart_index =
1836                        u32::try_from(index).map_err(|_| ExecError::IdOverflow("restart"))?;
1837                    out.push((
1838                        scope.clone(),
1839                        resolve_node_parity(&restart.parity, global_meas)?
1840                            .with_contributions(
1841                                id,
1842                                quantum.guards.iter().flat_map(|guard| {
1843                                    guard
1844                                        .restart_parities
1845                                        .iter()
1846                                        .filter(move |(row, _)| *row as usize == index)
1847                                        .map(move |(_, parity)| {
1848                                            (guard.input, resolve_node_parity(parity, global_meas))
1849                                        })
1850                                }),
1851                            )?
1852                            .guarded(
1853                                id,
1854                                quantum
1855                                    .guards
1856                                    .iter()
1857                                    .find(|guard| guard.restarts.contains(&restart_index))
1858                                    .map(|guard| guard.input),
1859                            ),
1860                    ));
1861                }
1862            }
1863            if let Some(region) = node.try_region()
1864                && !matches!(region, RegionNode::RepeatUntilSuccess { .. })
1865            {
1866                for (selector, body) in region.bodies() {
1867                    let child = scope.child(id, selector);
1868                    collect_restart_parities(bloq, body, &child, global_meas, out)?;
1869                }
1870            }
1871        }
1872        Ok(())
1873    }
1874
1875    let mut out = FxHashMap::default();
1876    visit_regions(
1877        bloq,
1878        bloq.top(),
1879        &LevelPath::default(),
1880        global_meas,
1881        &mut out,
1882    )?;
1883    Ok(out)
1884}
1885
1886/// Collect all template-local + node detectors with their owning execution
1887/// scope, resolved to global record-id parities.
1888fn collect_detectors(
1889    bloq: &Bloq,
1890    level: &SubGraph,
1891    global_meas: &FxHashMap<InstanceMeasurement, u32>,
1892) -> Result<Vec<(LevelPath, ResolvedDetector)>, ExecError> {
1893    let mut detectors = Vec::new();
1894    collect_detectors_into(
1895        bloq,
1896        level,
1897        &LevelPath::default(),
1898        global_meas,
1899        &mut detectors,
1900    )?;
1901    Ok(detectors)
1902}
1903
1904fn collect_detectors_into(
1905    bloq: &Bloq,
1906    level: &SubGraph,
1907    scope: &LevelPath,
1908    global_meas: &FxHashMap<InstanceMeasurement, u32>,
1909    out: &mut Vec<(LevelPath, ResolvedDetector)>,
1910) -> Result<(), ExecError> {
1911    for (id, node) in level.nodes() {
1912        if let Some(quantum) = node.try_quantum() {
1913            for instance in &quantum.instances {
1914                let template =
1915                    bloq.templates()
1916                        .get(instance.template_id)
1917                        .ok_or(ExecError::MalformedGraph(
1918                            "instance references a missing template",
1919                        ))?;
1920                for detector in &template.detectors {
1921                    out.push((
1922                        scope.clone(),
1923                        resolve_template_parity(&detector.parity, instance.id, global_meas)?
1924                            .guarded(
1925                                id,
1926                                quantum
1927                                    .guards
1928                                    .iter()
1929                                    .find(|guard| guard.instances.contains(&instance.id))
1930                                    .map(|guard| guard.input),
1931                            ),
1932                    ));
1933                }
1934            }
1935            for detector in bloq.node_detectors(quantum)? {
1936                let parity = resolve_node_parity(&detector.parity(), global_meas)?;
1937                let (membership, parity) = match detector.address() {
1938                    NodeDetectorAddress::Inline(index) => {
1939                        let membership = quantum
1940                            .guards
1941                            .iter()
1942                            .find(|guard| guard.detectors.contains(&index))
1943                            .map(|guard| guard.input);
1944                        let parity = parity.with_contributions(
1945                            id,
1946                            quantum.guards.iter().flat_map(|guard| {
1947                                guard
1948                                    .detector_parities
1949                                    .iter()
1950                                    .filter(move |(row, _)| *row == index)
1951                                    .map(move |(_, parity)| {
1952                                        (guard.input, resolve_node_parity(parity, global_meas))
1953                                    })
1954                            }),
1955                        )?;
1956                        (membership, parity)
1957                    }
1958                    NodeDetectorAddress::Bundle { use_index, .. } => {
1959                        let membership = quantum
1960                            .guards
1961                            .iter()
1962                            .find(|guard| guard.detector_bundles.contains(&use_index))
1963                            .map(|guard| guard.input);
1964                        (membership, parity)
1965                    }
1966                };
1967                out.push((scope.clone(), parity.guarded(id, membership)));
1968            }
1969        }
1970        if let Some(region) = node.try_region() {
1971            for (selector, body) in region.bodies() {
1972                let child = scope.child(id, selector);
1973                collect_detectors_into(bloq, body, &child, global_meas, out)?;
1974            }
1975        }
1976    }
1977    Ok(())
1978}
1979
1980/// Top-level `Observable` nodes in node order.
1981fn collect_observables(bloq: &Bloq) -> Vec<BloqNodeId> {
1982    bloq.top()
1983        .nodes()
1984        .filter_map(|(id, node)| match node.try_classical() {
1985            Some(ClassicalNode::Observable { index: Some(_), .. }) => Some(id),
1986            _ => None,
1987        })
1988        .collect()
1989}
1990
1991/// A detector's sign and `(owning instance, global record id)` terms, so
1992/// per-shot evaluation can normalize its expected value and distinguish
1993/// deselected instances' records from genuine ones.
1994#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1995pub(super) struct ResolvedDetector {
1996    pub sign: bool,
1997    pub terms: Vec<(TemplateInstanceId, u32)>,
1998    membership: Option<(BloqNodeId, u32)>,
1999    conditioned: Vec<(BloqNodeId, u32, ResolvedDetector)>,
2000}
2001
2002impl ResolvedDetector {
2003    fn with_contributions(
2004        mut self,
2005        node: BloqNodeId,
2006        contributions: impl Iterator<Item = (u32, Result<ResolvedDetector, ExecError>)>,
2007    ) -> Result<Self, ExecError> {
2008        for (input, parity) in contributions {
2009            let parity = parity?;
2010            self.conditioned.push((node, input, parity));
2011        }
2012        Ok(self)
2013    }
2014    fn guarded(mut self, node: BloqNodeId, input: Option<u32>) -> Self {
2015        self.membership = input.map(|slot| (node, slot));
2016        self
2017    }
2018
2019    fn applies(&self, scope: &LevelPath, state: &Shot) -> bool {
2020        state.executed_scopes.contains(scope)
2021            && self.membership.is_none_or(|(node, slot)| {
2022                state
2023                    .active_membership_inputs
2024                    .contains(&(scope.clone(), node, slot))
2025            })
2026    }
2027}
2028
2029/// Resolve a parity's measurement terms to `(owning instance, global record)`.
2030/// `owner` maps a term to the `InstanceMeasurement` that keys `global_meas` —
2031/// template parities synthesise it from their fixed instance, node parities
2032/// carry it — which is the only way the two callers differ.
2033fn resolve_parity<T: Copy + Ord>(
2034    parity: &DetectorParity<T>,
2035    global_meas: &FxHashMap<InstanceMeasurement, u32>,
2036    owner: impl Fn(T) -> InstanceMeasurement,
2037    unknown: &'static str,
2038) -> Result<ResolvedDetector, ExecError> {
2039    let mut ids = Vec::with_capacity(parity.terms().len());
2040    for term in parity.terms() {
2041        match term {
2042            DetectorTerm::Measurement(m) => {
2043                let key = owner(*m);
2044                let global = *global_meas
2045                    .get(&key)
2046                    .ok_or(ExecError::MalformedGraph(unknown))?;
2047                ids.push((key.instance, global));
2048            }
2049            DetectorTerm::LoopState(state) => return Err(ExecError::UnresolvedLoopState(state.0)),
2050        }
2051    }
2052    Ok(ResolvedDetector {
2053        sign: parity.sign(),
2054        terms: ids,
2055        membership: None,
2056        conditioned: Vec::new(),
2057    })
2058}
2059
2060fn resolve_template_parity(
2061    parity: &DetectorParity<u32>,
2062    instance: TemplateInstanceId,
2063    global_meas: &FxHashMap<InstanceMeasurement, u32>,
2064) -> Result<ResolvedDetector, ExecError> {
2065    resolve_parity(
2066        parity,
2067        global_meas,
2068        |measurement| InstanceMeasurement {
2069            instance,
2070            measurement,
2071        },
2072        "template detector names an unknown measurement",
2073    )
2074}
2075
2076fn resolve_node_parity(
2077    parity: &DetectorParity<InstanceMeasurement>,
2078    global_meas: &FxHashMap<InstanceMeasurement, u32>,
2079) -> Result<ResolvedDetector, ExecError> {
2080    resolve_parity(
2081        parity,
2082        global_meas,
2083        |im| im,
2084        "node detector names an unknown measurement",
2085    )
2086}
2087
2088/// Every instance id living anywhere in `level` (recursing region bodies).
2089pub(crate) fn collect_instances(
2090    level: &SubGraph,
2091    out: &mut rustc_hash::FxHashSet<TemplateInstanceId>,
2092) {
2093    for (_, node) in level.nodes() {
2094        if let Some(quantum) = node.try_quantum() {
2095            out.extend(quantum.instances.iter().map(|i| i.id));
2096        }
2097        if let Some(region) = node.try_region() {
2098            for (_, body) in region.bodies() {
2099                collect_instances(body, out);
2100            }
2101        }
2102    }
2103}
2104
2105/// The region's declared bit result (SEM-RVAL), or constant false. A selected
2106/// producer with unavailable inputs remains unavailable.
2107fn bound_body_value(body: &SubGraph, body_values: &FxHashMap<u32, Option<bool>>) -> Option<bool> {
2108    match body.value_output() {
2109        Some(producer) => zero_decoder_output(
2110            body_values.get(&producer.node.0).copied().flatten(),
2111            Some(producer.output),
2112        ),
2113        None => Some(false),
2114    }
2115}
2116
2117/// Without decoder errors, Flip is false but unavailable producers stay unavailable.
2118fn zero_decoder_output(value: Option<bool>, output: Option<ObservableOutput>) -> Option<bool> {
2119    value.map(|bit| bit && output != Some(ObservableOutput::Flip))
2120}
2121
2122/// Declared boundary outputs, independently of the body's bit result.
2123fn bound_body_bindings<'a>(
2124    body: &'a SubGraph,
2125    body_bindings: &FxHashMap<u32, BoundOperators<'a>>,
2126) -> Result<Vec<&'a InstanceBoundaryOperator>, ExecError> {
2127    let mut operators = Vec::new();
2128    let mut expanded = 0;
2129    for &producer in body.boundary_outputs() {
2130        collect_bound_operators(body_bindings, producer, &mut operators, &mut expanded)?;
2131    }
2132    Ok(operators)
2133}
2134
2135/// Evaluate a detector whose owner scope ran this shot. Terms naming instances
2136/// excluded from this shot are dropped. Owner scope and membership guards,
2137/// not record terms, decide detector applicability.
2138/// Any other missing record is a hard [`ExecError::MissingRecord`].
2139fn xor_records(
2140    parity: &ResolvedDetector,
2141    scope: &LevelPath,
2142    state: &Shot,
2143) -> Result<bool, ExecError> {
2144    let mut value = parity.sign;
2145    for &(instance, id) in &parity.terms {
2146        if state.excluded_instances.contains(&instance) {
2147            continue;
2148        }
2149        value ^= state.record.require(id)?;
2150    }
2151    for (node, input, contribution) in &parity.conditioned {
2152        if !state
2153            .active_membership_inputs
2154            .contains(&(scope.clone(), *node, *input))
2155        {
2156            continue;
2157        }
2158        value ^= contribution.sign;
2159        for &(instance, id) in &contribution.terms {
2160            if !state.excluded_instances.contains(&instance) {
2161                value ^= state.record.require(id)?;
2162            }
2163        }
2164    }
2165    Ok(value)
2166}
2167
2168fn rus_attempt_seed(seed: u64, scope: &LevelPath, attempt: u32) -> u64 {
2169    let mut state = splitmix64(seed ^ 0x5255_535F_5254_5259);
2170    for segment in scope.segments() {
2171        state = splitmix64(state ^ u64::from(segment.region.0));
2172        state = splitmix64(state);
2173    }
2174    splitmix64(state ^ u64::from(attempt))
2175}
2176
2177#[inline]
2178const fn splitmix64(mut value: u64) -> u64 {
2179    value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
2180    value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2181    value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2182    value ^ (value >> 31)
2183}
2184
2185#[cfg(test)]
2186mod tests {
2187    use super::*;
2188    use crate::test_support::{guarded_node, one_qubit_template, quantum_node};
2189    use bloq_ir::BoundaryFace;
2190    use bloq_ir::circuit::{CoordCircuit, GateType, Pauli as CircuitPauli, PauliBasis};
2191    use bloq_ir::lowering::{BloqTemplate, NodeRestart};
2192    use bloq_ir::{BloqEdge, BloqNode, ClassicalExpr, NodeDetector, TemplateDetector};
2193
2194    #[test]
2195    fn early_cut_activation_uses_the_selected_observable_port() {
2196        for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
2197            let mut bloq = Bloq::new();
2198            let bit = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2199                expr: ClassicalExpr::Const(true),
2200            }));
2201            let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2202            bloq.add_edge(bit, observable, BloqEdge::value(0));
2203            let mut guarded = BloqNode::classical(ClassicalNode::Compute {
2204                expr: ClassicalExpr::Const(true),
2205            });
2206            guarded.activation = Some(0);
2207            let guarded = bloq.add_node(guarded);
2208            bloq.add_edge(
2209                observable,
2210                guarded,
2211                BloqEdge::Value {
2212                    output,
2213                    slot: 0,
2214                    role: bloq_ir::ValueRole::Data,
2215                },
2216            );
2217            bloq.validate().unwrap();
2218            let program = Program::prepare(&bloq, RunMode::Plain).unwrap();
2219            let mut completed = program
2220                .execute_shot(0, 0, false, &mut |_, _| Ok(()))
2221                .unwrap();
2222            let expected = Some(output == ObservableOutput::Corrected);
2223            assert_eq!(completed.top_values[&guarded.0], expected);
2224
2225            completed.state.classical_values.clear();
2226            let scope = LevelPath::default();
2227            let mut memo = FxHashMap::default();
2228            assert_eq!(
2229                program
2230                    .available_value(&scope, guarded, &completed.state, &mut memo)
2231                    .unwrap(),
2232                expected
2233            );
2234            completed
2235                .state
2236                .classical_values
2237                .insert((scope.clone(), observable), None);
2238            memo.clear();
2239            assert_eq!(
2240                program
2241                    .available_value(&scope, guarded, &completed.state, &mut memo)
2242                    .unwrap(),
2243                None
2244            );
2245        }
2246    }
2247
2248    #[test]
2249    fn unavailable_discard_condition_errors_when_executed() {
2250        let mut bloq = Bloq::new();
2251        bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2252            condition: ClassicalExpr::In(0),
2253        }));
2254
2255        assert!(matches!(
2256            bloq.validate(),
2257            Err(bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. })
2258        ));
2259        assert!(matches!(
2260            run_bloq(&bloq, 1, 0),
2261            Err(ExecError::MalformedGraph(
2262                "discard condition is unavailable"
2263            ))
2264        ));
2265    }
2266
2267    #[test]
2268    fn conditional_members_execute_once_with_selected_record_aliases() {
2269        let mut bloq = Bloq::new();
2270        let mut coin = CoordCircuit::new();
2271        coin.do_gate(GateType::H, [IVec2::X]).unwrap();
2272        let bit = coin.measure(PauliBasis::Z, [IVec2::X])[0];
2273        let coin_template = bloq.add_template(BloqTemplate::new(coin));
2274        let coin = bloq.add_node(quantum_instance(TemplateInstanceId(0), coin_template));
2275        let selector = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2276            vec![InstanceMeasurement {
2277                instance: TemplateInstanceId(0),
2278                measurement: bit,
2279            }],
2280            Vec::new(),
2281        )));
2282        bloq.add_edge(coin, selector, BloqEdge::Order);
2283        let opposite = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2284            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2285        }));
2286        bloq.add_edge(selector, opposite, BloqEdge::value(0));
2287
2288        // The optional duplicate precedes the common record in instance order,
2289        // so its selection changes the common record's physical alias.
2290        let mut measure = CoordCircuit::new();
2291        let measurement = measure.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2292        let template = bloq.add_template(BloqTemplate::new(measure));
2293        let mut registry = quantum_instance(TemplateInstanceId(1), template);
2294        registry.expect_quantum_mut().instances.push(
2295            quantum_instance(TemplateInstanceId(2), template)
2296                .expect_quantum()
2297                .instances[0],
2298        );
2299        registry
2300            .expect_quantum_mut()
2301            .guards
2302            .push(bloq_ir::QuantumGuard {
2303                input: 0,
2304                instances: vec![TemplateInstanceId(1)],
2305                detectors: vec![0],
2306                restarts: vec![],
2307                ..Default::default()
2308            });
2309        registry.expect_quantum_mut().detectors.push(NodeDetector {
2310            parity: DetectorParity::default().with_sign(true),
2311            coords: None,
2312        });
2313        let registry = bloq.add_node(registry);
2314        bloq.add_edge(selector, registry, BloqEdge::value(0));
2315        let parity = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2316            vec![InstanceMeasurement {
2317                instance: TemplateInstanceId(2),
2318                measurement,
2319            }],
2320            Vec::new(),
2321        )));
2322        bloq.add_edge(registry, parity, BloqEdge::Order);
2323        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2324        bloq.add_edge(parity, observable, BloqEdge::value(0));
2325        let choice = bloq.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2326        bloq.add_edge(selector, choice, BloqEdge::value(0));
2327        let report = run_bloq(&bloq, 64, 0).unwrap();
2328        assert_eq!(report.observables[0].per_shot, [false; 64]);
2329        let enabled = report.observables[1]
2330            .per_shot
2331            .iter()
2332            .filter(|&&bit| bit)
2333            .count();
2334        assert!(enabled > 0 && enabled < 64);
2335        assert_eq!(report.detectors[0].per_shot, vec![true; enabled]);
2336    }
2337
2338    #[test]
2339    fn retry_result_is_independent_of_restart_and_internal_values() {
2340        let program = Bloq::from_text(
2341            "BLOQIR 1
2342graph {
2343 n0 rus in0 source n0 {
2344   body {
2345     n0 compute 0
2346     n1 compute 1
2347     n2 compute !in0
2348     n3 compute 0
2349     n1 -> n2 value 0
2350     result n1
2351   }
2352 }
2353 n1 observable 0
2354 n0 -> n1 value 0
2355}",
2356        )
2357        .unwrap();
2358        for mut program in [
2359            program.clone(),
2360            Bloq::from_binary(&program.to_binary()).unwrap(),
2361        ] {
2362            program.optimize().expect("acyclic test program");
2363            let report = run_bloq(&program, 8, 0).unwrap();
2364            assert_eq!(report.observables[0].per_shot, [true; 8]);
2365        }
2366    }
2367
2368    #[test]
2369    fn region_exports_its_declared_observable_measurement() {
2370        for bit in [false, true] {
2371            let mut circuit = CoordCircuit::new();
2372            if bit {
2373                circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2374            }
2375            let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2376            let mut bloq = Bloq::new();
2377            let template = bloq.add_template(BloqTemplate::new(circuit));
2378            let mut body = SubGraph::new();
2379            let quantum = body.add_node(quantum_instance(TemplateInstanceId(0), template));
2380            let parity = body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2381                vec![InstanceMeasurement {
2382                    instance: TemplateInstanceId(0),
2383                    measurement,
2384                }],
2385                Vec::new(),
2386            )));
2387            let observable = body.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2388            body.add_edge(quantum, parity, BloqEdge::Order);
2389            body.add_edge(parity, observable, BloqEdge::value(0));
2390            body.set_value_output(Some(observable.into()));
2391            let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2392                restart_condition: ClassicalExpr::Const(false),
2393                restart_source: None,
2394                body,
2395            }));
2396            let output = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2397            bloq.add_edge(region, output, BloqEdge::value(0));
2398            let report = run_bloq(&bloq, 1, 0).unwrap();
2399            assert_eq!(report.observables[0].per_shot, [bit]);
2400        }
2401    }
2402
2403    #[test]
2404    fn discarded_shot_keeps_evaluated_branch_selector() {
2405        let mut bloq = Bloq::new();
2406        let branch = bloq.add_node(selector_node(ClassicalExpr::Const(true)));
2407        let discard = bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2408            condition: ClassicalExpr::Const(true),
2409        }));
2410        bloq.add_edge(branch, discard, BloqEdge::Order);
2411
2412        let report = run_bloq(&bloq, 1, 0).expect("valid program executes");
2413        assert_eq!(report.discarded, 1);
2414        assert_eq!(report.branch_selectors, [true]);
2415    }
2416
2417    fn selector_node(expr: ClassicalExpr) -> BloqNode {
2418        BloqNode::classical(ClassicalNode::Compute { expr }).with_provenance(
2419            bloq_ir::NodeProvenance::BranchSelector {
2420                name: "choice".into(),
2421            },
2422        )
2423    }
2424
2425    fn quantum_instance(instance: TemplateInstanceId, template: bloq_ir::TemplateId) -> BloqNode {
2426        quantum_node(template, instance.0, IVec2::ZERO)
2427    }
2428
2429    fn guarded_detector_program(take_arm: bool) -> Bloq {
2430        let mut prefix_circuit = CoordCircuit::new();
2431        let prefix_measurement = prefix_circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2432        let mut bloq = Bloq::new();
2433        let prefix_template = bloq.add_template(BloqTemplate::new(prefix_circuit));
2434        let prefix_instance = TemplateInstanceId(0);
2435        let prefix = bloq.add_node(quantum_instance(prefix_instance, prefix_template));
2436
2437        let mut arm_circuit = CoordCircuit::new();
2438        arm_circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
2439        let mut arm_template = BloqTemplate::new(arm_circuit);
2440        arm_template.detectors.push(TemplateDetector {
2441            scope: TemplateDetectorScope::TopLevel,
2442            parity: DetectorParity::default().with_sign(true),
2443            coords: None,
2444        });
2445        let arm_template = bloq.add_template(arm_template);
2446        let mut arm_node = quantum_instance(TemplateInstanceId(1), arm_template);
2447        arm_node.expect_quantum_mut().detectors.push(NodeDetector {
2448            parity: DetectorParity::from_measurements([InstanceMeasurement {
2449                instance: prefix_instance,
2450                measurement: prefix_measurement,
2451            }])
2452            .with_sign(true),
2453            coords: None,
2454        });
2455        let selector = bloq.add_node(selector_node(ClassicalExpr::Const(take_arm)));
2456        let selected = bloq.add_node(guarded_node(arm_node, 0));
2457        bloq.add_edge(prefix, selected, BloqEdge::Order);
2458        bloq.add_edge(selector, selected, BloqEdge::value(0));
2459        bloq
2460    }
2461
2462    fn selected_detector_contribution_program(take_arm: bool) -> Bloq {
2463        let mut bloq = Bloq::new();
2464        let selector = bloq.add_node(selector_node(ClassicalExpr::Const(take_arm)));
2465        let opposite = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2466            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2467        }));
2468        bloq.add_edge(selector, opposite, BloqEdge::value(0));
2469        let mut downstream = BloqNode::from_members(Vec::new());
2470        downstream
2471            .expect_quantum_mut()
2472            .detectors
2473            .push(NodeDetector {
2474                parity: DetectorParity::default(),
2475                coords: None,
2476            });
2477        let mut owners = Vec::new();
2478        for (instance, active) in [(0, opposite), (1, selector)] {
2479            let mut circuit = CoordCircuit::new();
2480            if instance == 1 {
2481                circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2482            }
2483            let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2484            let template = bloq.add_template(BloqTemplate::new(circuit));
2485            let selected = bloq.add_node(guarded_node(
2486                quantum_node(template, instance, IVec2::ZERO),
2487                0,
2488            ));
2489            bloq.add_edge(active, selected, BloqEdge::value(0));
2490            owners.push(selected);
2491            downstream
2492                .expect_quantum_mut()
2493                .guards
2494                .push(bloq_ir::QuantumGuard {
2495                    input: instance,
2496                    detector_parities: vec![(
2497                        0,
2498                        DetectorParity::from_measurements([InstanceMeasurement {
2499                            instance: TemplateInstanceId(instance),
2500                            measurement,
2501                        }]),
2502                    )],
2503                    ..Default::default()
2504                });
2505        }
2506        let downstream = bloq.add_node(downstream);
2507        bloq.add_edge(opposite, downstream, BloqEdge::value(0));
2508        bloq.add_edge(selector, downstream, BloqEdge::value(1));
2509        for owner in owners {
2510            bloq.add_edge(owner, downstream, BloqEdge::Order);
2511        }
2512        bloq
2513    }
2514
2515    fn signed_detector_node() -> BloqNode {
2516        let mut node = BloqNode::from_members(Vec::new());
2517        node.expect_quantum_mut().detectors.push(NodeDetector {
2518            parity: DetectorParity::default().with_sign(true),
2519            coords: None,
2520        });
2521        node
2522    }
2523
2524    fn signed_restart_node() -> BloqNode {
2525        let mut node = BloqNode::from_members(Vec::new());
2526        node.expect_quantum_mut().restarts.push(NodeRestart {
2527            parity: DetectorParity::default().with_sign(true),
2528        });
2529        node
2530    }
2531
2532    #[test]
2533    fn guarded_detectors_only_report_when_selected() {
2534        let skipped = run_bloq(&guarded_detector_program(false), 3, 0).unwrap();
2535        assert_eq!(skipped.detectors.len(), 2);
2536        assert!(skipped.detectors.iter().all(|detector| {
2537            detector.constant && detector.value.is_none() && detector.per_shot.is_empty()
2538        }));
2539
2540        let taken = run_bloq(&guarded_detector_program(true), 3, 0).unwrap();
2541        assert_eq!(taken.detectors.len(), 2);
2542        assert!(
2543            taken
2544                .detectors
2545                .iter()
2546                .all(|detector| detector.per_shot == [true; 3])
2547        );
2548    }
2549
2550    #[test]
2551    fn guarded_shared_detector_reads_two_owner_instances() {
2552        use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
2553
2554        for enabled in [false, true] {
2555            let mut bloq = Bloq::new();
2556            let mut circuit = CoordCircuit::new();
2557            let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2558            let template = bloq.add_template(BloqTemplate::new(circuit));
2559            let bundle = bloq.add_detector_bundle(DetectorBundle::new(
2560                vec![template, template],
2561                vec![BundleDetector {
2562                    parity: DetectorParity::from_measurements([
2563                        BundleMeasurement {
2564                            owner: 0,
2565                            measurement,
2566                        },
2567                        BundleMeasurement {
2568                            owner: 1,
2569                            measurement,
2570                        },
2571                    ])
2572                    .with_sign(true),
2573                    coords: None,
2574                }],
2575            ));
2576            let mut node = BloqNode::from_members(Vec::new());
2577            let quantum = node.expect_quantum_mut();
2578            quantum.instances = vec![
2579                bloq_ir::lowering::TemplateInstance::new(
2580                    TemplateInstanceId(0),
2581                    template,
2582                    IVec2::ZERO,
2583                ),
2584                bloq_ir::lowering::TemplateInstance::new(TemplateInstanceId(1), template, IVec2::X),
2585            ];
2586            quantum.detector_bundles.push(DetectorBundleUse {
2587                bundle,
2588                instances: vec![TemplateInstanceId(0), TemplateInstanceId(1)],
2589                offset: IVec2::ZERO,
2590            });
2591            quantum.guards.push(bloq_ir::QuantumGuard {
2592                input: 0,
2593                detector_bundles: vec![0],
2594                ..Default::default()
2595            });
2596            let selector = bloq.add_node(selector_node(ClassicalExpr::Const(enabled)));
2597            let node = bloq.add_node(node);
2598            bloq.add_edge(selector, node, BloqEdge::value(0));
2599
2600            bloq.validate().expect("shared detector binding validates");
2601            let report = run_bloq(&bloq, 2, 0).expect("program executes");
2602            assert_eq!(report.detectors.len(), 1);
2603            assert_eq!(
2604                report.detectors[0].per_shot,
2605                if enabled { vec![true; 2] } else { vec![] }
2606            );
2607
2608            let other = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2609            let wrong_bundle =
2610                bloq.add_detector_bundle(DetectorBundle::new(vec![other, template], vec![]));
2611            bloq.node_mut(node)
2612                .unwrap()
2613                .expect_quantum_mut()
2614                .detector_bundles[0]
2615                .bundle = wrong_bundle;
2616            assert!(matches!(
2617                run_bloq(&bloq, 1, 0),
2618                Err(ExecError::DetectorBundle(
2619                    bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
2620                ))
2621            ));
2622            assert!(matches!(
2623                crate::lower::lower(&bloq, &crate::lower::LoweringConfig::default()),
2624                Err(crate::lower::LowerError::DetectorBundle(
2625                    bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
2626                ))
2627            ));
2628        }
2629    }
2630
2631    #[test]
2632    fn downstream_detector_folds_only_selected_contribution() {
2633        for chosen in [false, true] {
2634            let bloq = selected_detector_contribution_program(chosen);
2635            bloq.validate()
2636                .expect("conditional detector program is valid");
2637
2638            let report = run_bloq(&bloq, 1, 0).expect("guarded program executes");
2639            assert_eq!(report.detectors[0].per_shot, [chosen]);
2640        }
2641    }
2642
2643    /// Rejected attempts must roll back membership state so their detectors
2644    /// never leak into the accepted shot.
2645    #[test]
2646    fn rejected_rus_attempt_does_not_leave_active_detectors() {
2647        let mut circuit = CoordCircuit::new();
2648        circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2649        let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2650        let mut bloq = Bloq::new();
2651        let template = bloq.add_template(BloqTemplate::new(circuit));
2652        let mut body = SubGraph::new();
2653        let measure = body.add_node(quantum_instance(TemplateInstanceId(0), template));
2654        let selector = body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2655            vec![InstanceMeasurement {
2656                instance: TemplateInstanceId(0),
2657                measurement,
2658            }],
2659            Vec::new(),
2660        )));
2661        let opposite = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2662            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2663        }));
2664        body.add_edge(measure, selector, BloqEdge::Order);
2665        body.add_edge(selector, opposite, BloqEdge::value(0));
2666        for (active, restarting) in [(opposite, true), (selector, false)] {
2667            let mut quantum = signed_detector_node();
2668            if restarting {
2669                quantum.expect_quantum_mut().restarts.push(NodeRestart {
2670                    parity: DetectorParity::default().with_sign(true),
2671                });
2672            }
2673            let node = guarded_node(quantum, 0);
2674            let selected = body.add_node(node);
2675            body.add_edge(active, selected, BloqEdge::value(0));
2676        }
2677        let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2678            expr: ClassicalExpr::Const(false),
2679        }));
2680        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2681            body,
2682            restart_condition: ClassicalExpr::Const(false),
2683            restart_source: Some(restart_source.into()),
2684        }));
2685        let report = run_bloq(&bloq, 8, 0).unwrap();
2686        assert_eq!(report.discarded, 0);
2687        assert!(report.detectors[0].per_shot.is_empty());
2688        assert_eq!(report.detectors[1].per_shot, [true; 8]);
2689    }
2690
2691    #[test]
2692    fn guarded_restart_only_applies_when_selected() {
2693        let program = |take_restart_arm| {
2694            let mut body = SubGraph::new();
2695            let selector = body.add_node(selector_node(ClassicalExpr::Const(take_restart_arm)));
2696            let restart = body.add_node(guarded_node(signed_restart_node(), 0));
2697            body.add_edge(selector, restart, BloqEdge::value(0));
2698            let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2699                expr: ClassicalExpr::Const(false),
2700            }));
2701            let mut bloq = Bloq::new();
2702            bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2703                body,
2704                restart_condition: ClassicalExpr::Const(false),
2705                restart_source: Some(restart_source.into()),
2706            }));
2707            bloq
2708        };
2709        let kept = run_bloq(&program(false), 1, 0).unwrap();
2710        assert_eq!(kept.discarded, 0);
2711        let rejected = run_bloq(&program(true), 1, 0).unwrap();
2712        assert_eq!(rejected.discarded, 1);
2713    }
2714
2715    /// A RUS region's attempts draw from their own reseeded RNG stream and hand
2716    /// the parent stream back untouched (`restore_rng_from`), so a retrying RUS
2717    /// prefix must not shift the randomness of anything scheduled after it. The
2718    /// prefix here retries a shot-dependent number of times; its downstream
2719    /// selectors must still match the program without the prefix.
2720    #[test]
2721    fn transparent_rus_retry_preserves_downstream_randomness() {
2722        // H + Z-measure: one random bit per instance.
2723        let coin = |coord: IVec2| {
2724            let mut circuit = CoordCircuit::new();
2725            circuit.do_gate(GateType::H, [coord]).expect("H is valid");
2726            let measurement = circuit.measure(PauliBasis::Z, [coord])[0];
2727            (BloqTemplate::new(circuit), measurement)
2728        };
2729
2730        let program = |with_prefix: bool| {
2731            let mut bloq = Bloq::new();
2732
2733            // A RUS body that keeps retrying until its own coin lands `true`.
2734            let prefix = with_prefix.then(|| {
2735                let (template, measurement) = coin(IVec2::new(2, 0));
2736                let template = bloq.add_template(template);
2737                let instance = TemplateInstanceId(1);
2738                let mut body = SubGraph::new();
2739                let flip = body.add_node(quantum_instance(instance, template));
2740                let restart_source =
2741                    body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2742                        vec![InstanceMeasurement {
2743                            instance,
2744                            measurement,
2745                        }],
2746                        Vec::new(),
2747                    )));
2748                body.add_edge(flip, restart_source, BloqEdge::Order);
2749                bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2750                    body,
2751                    restart_condition: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2752                    restart_source: Some(restart_source.into()),
2753                }))
2754            });
2755
2756            let (template, measurement) = coin(IVec2::ZERO);
2757            let template = bloq.add_template(template);
2758            let instance = TemplateInstanceId(0);
2759            let quantum = bloq.add_node(quantum_instance(instance, template));
2760            let selector = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2761                vec![InstanceMeasurement {
2762                    instance,
2763                    measurement,
2764                }],
2765                Vec::new(),
2766            )));
2767            let branch = bloq.add_node(selector_node(ClassicalExpr::In(0)));
2768            if let Some(prefix) = prefix {
2769                bloq.add_edge(prefix, quantum, BloqEdge::Order);
2770            }
2771            bloq.add_edge(quantum, selector, BloqEdge::Order);
2772            bloq.add_edge(selector, branch, BloqEdge::value(0));
2773            bloq
2774        };
2775
2776        let plain = run_bloq(&program(false), 64, 17).expect("prefix-free program executes");
2777        let retried = run_bloq(&program(true), 64, 17).expect("retrying prefix executes");
2778
2779        assert_eq!(plain.branch_selectors.len(), 64);
2780        assert_eq!(retried.discarded, 0);
2781        assert_eq!(retried.branch_selectors, plain.branch_selectors);
2782    }
2783
2784    /// A boundary operator's coordinates are already instance-global, so
2785    /// [`Program::measure_boundary`] must resolve them without re-adding the
2786    /// instance offset. This pins the frame at a non-zero offset, where a stray
2787    /// second offset (the historical double-offset) would resolve coordinate
2788    /// `(48, 0)` — absent from `coord_index`, surfacing as `UnknownCoord` — while
2789    /// the correct global coordinate `(24, 0)` reads the prepared qubit.
2790    #[test]
2791    fn measure_boundary_does_not_double_apply_offset() {
2792        let coord = IVec2::new(24, 0);
2793        let instance = TemplateInstanceId(0);
2794
2795        let mut bloq = Bloq::new();
2796        let template_id = one_qubit_template(&mut bloq, GateType::X);
2797        bloq.add_node(quantum_node(template_id, instance.0, coord));
2798
2799        // A single-qubit Z face on the global data qubit, in instance-global
2800        // coordinates exactly as lowering stores it.
2801        let operator = InstanceBoundaryOperator {
2802            instance,
2803            face: BoundaryFace::Output,
2804            operator: [(coord, CircuitPauli::Z)].into_iter().collect(),
2805        };
2806        bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2807            Vec::new(),
2808            vec![operator.clone()],
2809        )));
2810        let program = Program::prepare(&bloq, RunMode::Plain).unwrap();
2811
2812        // Prepare |1> so Z reads the −1 eigenvalue (outcome `true`) deterministically.
2813        let mut sim = Simulator::with_seed(1, 0);
2814        sim.x(0);
2815        let outcome = program
2816            .measure_boundary(&operator, &mut sim)
2817            .expect("boundary resolves at the true global coordinate (24, 0)");
2818        assert!(outcome, "Z on |1> is the −1 eigenvalue");
2819    }
2820
2821    fn nested_boundary_program(depth: usize) -> Bloq {
2822        let coord = IVec2::ZERO;
2823        let instance = TemplateInstanceId(0);
2824        let mut bloq = Bloq::new();
2825        let template = one_qubit_template(&mut bloq, GateType::X);
2826        let quantum = bloq.add_node(quantum_node(template, instance.0, IVec2::ZERO));
2827        let operator = InstanceBoundaryOperator {
2828            instance,
2829            face: BoundaryFace::Output,
2830            operator: [(coord, CircuitPauli::Z)].into_iter().collect(),
2831        };
2832        let mut body = SubGraph::new();
2833        body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2834            Vec::new(),
2835            vec![operator],
2836        )));
2837        let internal = body.add_node(body[BloqNodeId(0)].clone());
2838        let local = body.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2839        body.add_edge(BloqNodeId(0), local, BloqEdge::value(0));
2840        body.add_edge(internal, local, BloqEdge::value(1));
2841        body.set_boundary_outputs(vec![BloqNodeId(0)]);
2842        let mut region = RegionNode::RepeatUntilSuccess {
2843            restart_condition: ClassicalExpr::Const(false),
2844            restart_source: None,
2845            body,
2846        };
2847        for _ in 1..depth {
2848            let mut body = SubGraph::new();
2849            let nested = body.add_node(BloqNode::region(region));
2850            body.set_boundary_outputs(vec![nested]);
2851            let consume = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2852                expr: ClassicalExpr::In(0),
2853            }));
2854            body.add_edge(nested, consume, BloqEdge::value(0));
2855            region = RegionNode::RepeatUntilSuccess {
2856                restart_condition: ClassicalExpr::Const(false),
2857                restart_source: None,
2858                body,
2859            };
2860        }
2861        let region = bloq.add_node(BloqNode::region(region));
2862        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2863        bloq.add_edge(quantum, region, BloqEdge::Order);
2864        bloq.add_edge(region, observable, BloqEdge::compose(0));
2865        bloq
2866    }
2867
2868    #[test]
2869    fn nested_rus_include_bindings_reach_observable() {
2870        for depth in [1, 2, 3] {
2871            let mut bloq = nested_boundary_program(depth);
2872            bloq.optimize().expect("acyclic test program");
2873            let report = run_bloq(&bloq, 1, 0).expect("nested boundary program executes");
2874            assert_eq!(
2875                report.observables[0].per_shot,
2876                [true],
2877                "the selected body's Z binding reads the prepared |1>"
2878            );
2879        }
2880    }
2881
2882    #[test]
2883    fn early_output_reads_preserve_joint_bell_observables() {
2884        for reuse in [false, true] {
2885            let mut bloq = Bloq::new();
2886            let mut circuit = CoordCircuit::new();
2887            circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2888            circuit
2889                .do_gate(GateType::CX, [IVec2::ZERO, IVec2::X])
2890                .unwrap();
2891            let template = bloq.add_template(BloqTemplate::new(circuit));
2892            let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
2893            for (index, basis) in [CircuitPauli::X, CircuitPauli::Z].into_iter().enumerate() {
2894                let operators = [IVec2::ZERO, IVec2::X]
2895                    .into_iter()
2896                    .map(|coord| InstanceBoundaryOperator {
2897                        instance: TemplateInstanceId(0),
2898                        face: BoundaryFace::Output,
2899                        operator: [(coord, basis)].into_iter().collect(),
2900                    })
2901                    .collect();
2902                let include = bloq.add_node(BloqNode::classical(
2903                    ClassicalNode::observable_fragment(Vec::new(), operators),
2904                ));
2905                let observable =
2906                    bloq.add_node(BloqNode::classical(ClassicalNode::observable(index as u32)));
2907                bloq.add_edge(owner, include, BloqEdge::Order);
2908                bloq.add_edge(include, observable, BloqEdge::compose(0));
2909            }
2910            if reuse {
2911                let template = one_qubit_template(&mut bloq, GateType::RZ);
2912                let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
2913                bloq.add_edge(owner, reset, BloqEdge::Order);
2914            }
2915            let report = run_bloq(&bloq, 64, 0).unwrap();
2916            for observable in report.observables {
2917                assert_eq!(
2918                    observable.per_shot, [false; 64],
2919                    "both Bell stabilizers remain +1 (reuse={reuse})"
2920                );
2921            }
2922        }
2923    }
2924
2925    #[test]
2926    fn repeated_output_captures_use_owner_cuts_without_observables() {
2927        let mut bloq = Bloq::new();
2928        let mut previous = None;
2929        let mut outputs = Vec::new();
2930        for z in 0..3 {
2931            let mut circuit = CoordCircuit::new();
2932            circuit.do_gate(GateType::RZ, [IVec2::ZERO]).unwrap();
2933            circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2934            let template = bloq.add_template(BloqTemplate::new(circuit));
2935            let instance = TemplateInstanceId(z as u32);
2936            let owner = bloq.add_node(quantum_node(template, instance.0, IVec2::ZERO));
2937            if let Some(previous) = previous {
2938                bloq.add_edge(previous, owner, BloqEdge::Order);
2939            }
2940            previous = Some(owner);
2941            let port = IVec3::new(0, 0, z);
2942            outputs.push(bloq_ir::LogicalOutput {
2943                port,
2944                instance,
2945                x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
2946                z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
2947            });
2948            for basis in [bloq_ir::Basis::X, bloq_ir::Basis::Z] {
2949                bloq.add_node(
2950                    BloqNode::classical(ClassicalNode::Compute {
2951                        expr: ClassicalExpr::Const(false),
2952                    })
2953                    .with_provenance(bloq_ir::NodeProvenance::OutputFrame { port, basis }),
2954                );
2955            }
2956        }
2957        bloq.set_logical_outputs(outputs);
2958        // Saved IR must retain the identity needed for each output's cut.
2959        let bloq = Bloq::from_binary(&bloq.to_binary()).unwrap();
2960        let bloq = Bloq::from_text(&bloq.to_text()).unwrap();
2961        run_bloq_with_captured_outputs(&bloq, 1, 0, |sim, context| {
2962            assert_eq!(context.saved_outputs.len(), 2);
2963            for saved in context.saved_outputs {
2964                let z = PauliString::single(sim.num_qubits(), saved.qubit, Pauli::Z);
2965                assert_eq!(
2966                    sim.peek_observable_expectation(&z)?,
2967                    -1.0,
2968                    "output {} was prepared in |1>",
2969                    saved.port.z
2970                );
2971            }
2972            Ok(())
2973        })
2974        .unwrap();
2975    }
2976
2977    #[test]
2978    fn report_requires_a_complete_frame_pair_for_each_logical_output() {
2979        let mut bloq = Bloq::new();
2980        let template = one_qubit_template(&mut bloq, GateType::RZ);
2981        bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
2982        let port = IVec3::ZERO;
2983        bloq.set_logical_outputs(vec![bloq_ir::LogicalOutput {
2984            port,
2985            instance: TemplateInstanceId(0),
2986            x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
2987            z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
2988        }]);
2989        bloq.add_node(
2990            BloqNode::classical(ClassicalNode::Compute {
2991                expr: ClassicalExpr::Const(false),
2992            })
2993            .with_provenance(bloq_ir::NodeProvenance::OutputFrame {
2994                port,
2995                basis: bloq_ir::Basis::X,
2996            }),
2997        );
2998        assert!(matches!(
2999            run_bloq(&bloq, 1, 0),
3000            Err(ExecError::MalformedGraph(
3001                "logical output has no complete frame pair"
3002            ))
3003        ));
3004    }
3005
3006    #[test]
3007    fn output_capture_requires_a_selected_owner() {
3008        for take_other_arm in [false, true] {
3009            let mut bloq = Bloq::new();
3010            let prepared = one_qubit_template(&mut bloq, GateType::X);
3011            let reset = one_qubit_template(&mut bloq, GateType::RZ);
3012            let consumer = bloq.add_node(quantum_node(reset, 2, IVec2::ZERO));
3013            for (instance, template, selected) in
3014                [(0, prepared, !take_other_arm), (1, reset, take_other_arm)]
3015            {
3016                let selector = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3017                    expr: ClassicalExpr::Const(selected),
3018                }));
3019                let owner = bloq.add_node(guarded_node(
3020                    quantum_node(template, instance, IVec2::ZERO),
3021                    0,
3022                ));
3023                bloq.add_edge(selector, owner, BloqEdge::value(0));
3024                bloq.add_edge(owner, consumer, BloqEdge::Order);
3025            }
3026            let port = IVec3::ZERO;
3027            bloq.set_logical_outputs(vec![bloq_ir::LogicalOutput {
3028                port,
3029                instance: TemplateInstanceId(u32::from(take_other_arm)),
3030                x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
3031                z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
3032            }]);
3033            for basis in [bloq_ir::Basis::X, bloq_ir::Basis::Z] {
3034                bloq.add_node(
3035                    BloqNode::classical(ClassicalNode::Compute {
3036                        expr: ClassicalExpr::Const(false),
3037                    })
3038                    .with_provenance(bloq_ir::NodeProvenance::OutputFrame { port, basis }),
3039                );
3040            }
3041            run_bloq_with_captured_outputs(&bloq, 1, 0, |sim, context| {
3042                assert_eq!(context.saved_outputs.len(), 1);
3043                let saved = &context.saved_outputs[0];
3044                let z = PauliString::single(sim.num_qubits(), saved.qubit, Pauli::Z);
3045                assert_eq!(
3046                    sim.peek_observable_expectation(&z)?,
3047                    if take_other_arm { 1.0 } else { -1.0 },
3048                );
3049                Ok(())
3050            })
3051            .unwrap();
3052
3053            // WF-7: external output metadata cannot advertise an absent owner,
3054            // even when another branch uses the same physical coordinates.
3055            let mut outputs = bloq.logical_outputs().to_vec();
3056            outputs[0].instance = TemplateInstanceId(u32::from(!take_other_arm));
3057            bloq.set_logical_outputs(outputs);
3058            assert!(matches!(
3059                bloq.validate(),
3060                Err(
3061                    bloq_ir::BloqValidationError::InvalidInstanceMergeStructure {
3062                        source:
3063                            bloq_ir::lowering::NodeTemplateInstanceMergeError::InvalidMembership(_),
3064                        ..
3065                    }
3066                )
3067            ));
3068            assert!(matches!(
3069                run_bloq_with_captured_outputs(&bloq, 1, 0, |_, _| unreachable!()),
3070                Err(ExecError::MalformedGraph(
3071                    "logical output owner is not selected"
3072                ))
3073            ));
3074        }
3075    }
3076
3077    #[test]
3078    fn early_output_reads_keep_identical_activated_bindings_distinct() {
3079        let mut bloq = Bloq::new();
3080        let template = one_qubit_template(&mut bloq, GateType::X);
3081        let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3082        let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3083        let template = one_qubit_template(&mut bloq, GateType::RZ);
3084        let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3085        for (slot, selected) in [false, true].into_iter().enumerate() {
3086            let selector = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3087                expr: ClassicalExpr::Const(selected),
3088            }));
3089            let mut fragment = BloqNode::classical(ClassicalNode::observable_fragment(
3090                Vec::new(),
3091                vec![InstanceBoundaryOperator {
3092                    instance: TemplateInstanceId(0),
3093                    face: BoundaryFace::Output,
3094                    operator: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
3095                }],
3096            ));
3097            fragment.activation = Some(0);
3098            let region = bloq.add_node(fragment);
3099            bloq.add_edge(owner, region, BloqEdge::Order);
3100            bloq.add_edge(selector, region, BloqEdge::value(0));
3101            bloq.add_edge(region, reset, BloqEdge::Order);
3102            bloq.add_edge(region, observable, BloqEdge::compose(slot as u32));
3103        }
3104        bloq.add_edge(reset, observable, BloqEdge::Order);
3105        let report = run_bloq(&bloq, 8, 0).unwrap();
3106        assert_eq!(report.observables[0].per_shot, [true; 8]);
3107    }
3108
3109    #[test]
3110    fn anticommuting_partial_output_reads_fail_before_projection() {
3111        let mut bloq = Bloq::new();
3112        let template = one_qubit_template(&mut bloq, GateType::H);
3113        let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3114        let template = one_qubit_template(&mut bloq, GateType::RZ);
3115        let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3116        let future = bloq.add_node(quantum_node(template, 2, IVec2::X));
3117        bloq.add_edge(owner, reset, BloqEdge::Order);
3118        bloq.add_edge(reset, future, BloqEdge::Order);
3119        for (index, basis) in [CircuitPauli::X, CircuitPauli::Z].into_iter().enumerate() {
3120            let include = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3121                Vec::new(),
3122                [(0, IVec2::ZERO), (2, IVec2::X)]
3123                    .into_iter()
3124                    .map(|(instance, coord)| InstanceBoundaryOperator {
3125                        instance: TemplateInstanceId(instance),
3126                        face: BoundaryFace::Output,
3127                        operator: [(coord, basis)].into_iter().collect(),
3128                    })
3129                    .collect(),
3130            )));
3131            let observable =
3132                bloq.add_node(BloqNode::classical(ClassicalNode::observable(index as u32)));
3133            bloq.add_edge(future, include, BloqEdge::Order);
3134            bloq.add_edge(include, observable, BloqEdge::compose(0));
3135        }
3136        for result in [
3137            run_bloq(&bloq, 1, 0),
3138            run_bloq_with_captured_outputs(&bloq, 1, 0, |_, _| Ok(())),
3139        ] {
3140            assert!(matches!(
3141                result,
3142                Err(ExecError::IncompatibleEarlyOutputReads { .. })
3143            ));
3144        }
3145    }
3146
3147    #[test]
3148    fn joint_boundary_products_keep_their_pauli_sign() {
3149        for reuse in [false, true] {
3150            for split_includes in [false, true] {
3151                let mut bloq = Bloq::new();
3152                let mut circuit = CoordCircuit::new();
3153                circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
3154                circuit
3155                    .do_gate(GateType::CX, [IVec2::ZERO, IVec2::X])
3156                    .unwrap();
3157                let template = bloq.add_template(BloqTemplate::new(circuit));
3158                let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3159                let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3160                let operators =
3161                    [CircuitPauli::X, CircuitPauli::Z].map(|basis| InstanceBoundaryOperator {
3162                        instance: TemplateInstanceId(0),
3163                        face: BoundaryFace::Output,
3164                        operator: [(IVec2::ZERO, basis), (IVec2::X, basis)]
3165                            .into_iter()
3166                            .collect(),
3167                    });
3168                let includes = if split_includes {
3169                    operators
3170                        .into_iter()
3171                        .map(|operator| vec![operator])
3172                        .collect::<Vec<_>>()
3173                } else {
3174                    vec![operators.into_iter().collect()]
3175                };
3176                for (slot, operators) in includes.into_iter().enumerate() {
3177                    let include = bloq.add_node(BloqNode::classical(
3178                        ClassicalNode::observable_fragment(Vec::new(), operators),
3179                    ));
3180                    bloq.add_edge(owner, include, BloqEdge::Order);
3181                    bloq.add_edge(include, observable, BloqEdge::compose(slot as u32));
3182                }
3183                if reuse {
3184                    let template = one_qubit_template(&mut bloq, GateType::RZ);
3185                    let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3186                    bloq.add_edge(owner, reset, BloqEdge::Order);
3187                    bloq.add_edge(reset, observable, BloqEdge::Order);
3188                }
3189                let report = run_bloq(&bloq, 8, 0).unwrap();
3190                assert_eq!(
3191                    report.observables[0].per_shot, [false; 8],
3192                    "Bell XX * ZZ = -YY has eigenvalue +1 (reuse={reuse}, split={split_includes})"
3193                );
3194            }
3195        }
3196    }
3197}