Skip to main content

bloq_ir/
flatten.rs

1//! Repeat-block flattening.
2//!
3//! [`BloqTemplate::flatten`] unrolls a template's `REPEAT` bodies into a
4//! straight-line circuit and rewrites its side tables to match: repeat-scoped
5//! detectors expand into one concrete top-level detector **per unrolled
6//! iteration**, loop-carried recurrences ([`TemplateRepeatState`]) are
7//! resolved away iteration by iteration (the `initial` parity seeds the first
8//! iteration, `next` advances the state after each), and the cleared
9//! `repeat_states` table is what "no loop state left" means. The expansion
10//! replays [`CoordCircuit::flatten`]'s event trace with the same semantics the
11//! Stim emitter applies at emission time, so a flattened template emits the
12//! same detector set the looped one did.
13//!
14//! [`Bloq::flatten`] applies this to a whole program: every template an
15//! instance references is flattened once (memoized) and instances are
16//! re-pointed at the flattened copies. Instance-space references stay valid
17//! without any remap because the final unrolled occurrence of each
18//! measurement keeps its original id — exactly the record the emission
19//! frame's latest-occurrence rule already resolved out-of-loop references to.
20
21use bloq_circuit::{
22    BodyId, CircuitError, DetectorCoords, DetectorParity, DetectorTerm, FlattenEvent,
23    FlattenLimits, LoopStateId,
24};
25use thiserror::Error;
26
27use crate::{
28    Bloq, BloqNodeKind, BloqTemplate, DetectorBundle, DetectorBundleId, FxMap, SubGraph,
29    TemplateDetector, TemplateDetectorParity, TemplateDetectorScope, TemplateId, TemplateRestart,
30};
31
32/// Why a template or program could not be flattened. The target is left
33/// unmodified on any of these.
34#[derive(Debug, Error, Clone, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum FlattenError {
37    /// Appending flattened templates exhausted their `u32` pool ids.
38    #[error("template id space exhausted while flattening")]
39    TemplateIdOverflow,
40    /// A detector bundle use or definition is malformed.
41    #[error("{0}")]
42    DetectorBundle(#[from] crate::DetectorBundleError),
43    /// Unrolling the circuit itself failed (dangling/self-recursive body,
44    /// measurement-id space overflow, or a resource allowance).
45    #[error("{context}: {0}", context = if matches!(.0, CircuitError::FlattenResourceLimit { .. }) {
46        "resource limit flattening the circuit"
47    } else {
48        "flattening the circuit failed"
49    })]
50    Circuit(#[from] CircuitError),
51    /// A parity references a loop state no reachable repeat body defines
52    /// before the reference resolves.
53    #[error("parity references undefined loop state {0:?}")]
54    UnresolvedLoopState(LoopStateId),
55    /// An instance references a template missing from the pool.
56    #[error("instance references unknown template {0:?}")]
57    UnknownTemplate(TemplateId),
58}
59
60fn new_template_id(base: usize, index: usize) -> Result<TemplateId, FlattenError> {
61    let index = base
62        .checked_add(index)
63        .ok_or(FlattenError::TemplateIdOverflow)?;
64    u32::try_from(index)
65        .map(TemplateId)
66        .map_err(|_| FlattenError::TemplateIdOverflow)
67}
68
69impl FlattenError {
70    /// Whether materialization exceeded an allowance rather than failing a
71    /// circuit or loop-state check.
72    #[must_use]
73    pub fn is_resource_limited(&self) -> bool {
74        matches!(
75            self,
76            Self::Circuit(CircuitError::FlattenResourceLimit { .. })
77        )
78    }
79}
80
81struct FlattenWork {
82    limits: FlattenLimits,
83    used: usize,
84}
85
86impl FlattenWork {
87    fn new(limits: FlattenLimits) -> Self {
88        Self { limits, used: 0 }
89    }
90
91    fn charge(&mut self, amount: usize) -> Result<(), FlattenError> {
92        let observed = self.used.saturating_add(amount);
93        if observed == usize::MAX || observed > self.limits.max_work {
94            return Err(CircuitError::FlattenResourceLimit {
95                observed,
96                limit: self.limits.max_work,
97            }
98            .into());
99        }
100        self.used = observed;
101        Ok(())
102    }
103
104    fn check_circuit(&mut self, circuit: &bloq_circuit::CoordCircuit) -> Result<(), FlattenError> {
105        self.charge(crate::instantiation::circuit_work(circuit))?;
106        let remaining = FlattenLimits {
107            max_work: self.limits.max_work - self.used,
108        };
109        match circuit.check_flatten_limits(remaining) {
110            Ok(amount)
111            | Err(CircuitError::FlattenResourceLimit {
112                observed: amount, ..
113            }) => self.charge(amount),
114            Err(error) => Err(error.into()),
115        }
116    }
117}
118
119impl BloqTemplate {
120    /// This template with every repeat block flattened away (see the module
121    /// docs): the circuit unrolled, repeat-scoped detectors expanded into one
122    /// top-level detector per iteration, loop-carried states resolved and
123    /// `repeat_states` cleared. Detector coordinates are copied verbatim per
124    /// iteration, matching what the emitter writes for each pass over a
125    /// repeat body. `boundary_flows` pass through unchanged: they name final
126    /// occurrences, which keep their ids.
127    ///
128    /// A template without repeats (and without loop-state references) round
129    /// trips unchanged.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`FlattenError`] for invalid loop-state references, malformed
134    /// repeat bodies, or exhausted expansion limits.
135    pub fn flatten(&self) -> Result<BloqTemplate, FlattenError> {
136        self.flatten_with_limits(FlattenLimits::default())
137    }
138
139    /// [`Self::flatten`] with one allowance for circuit expansion and side-table
140    /// replay, including substituted loop-state parity terms.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`FlattenError`] under the same conditions as [`Self::flatten`],
145    /// including when `limits` is exhausted.
146    pub fn flatten_with_limits(&self, limits: FlattenLimits) -> Result<BloqTemplate, FlattenError> {
147        let mut work = FlattenWork::new(limits);
148        work.check_circuit(&self.circuit)?;
149        self.flatten_with_trace_budget(&mut work)
150            .map(|(template, _)| template)
151    }
152
153    pub(crate) fn flatten_with_trace(
154        &self,
155    ) -> Result<(BloqTemplate, Vec<FlattenEvent>), FlattenError> {
156        let mut work = FlattenWork::new(FlattenLimits::default());
157        work.check_circuit(&self.circuit)?;
158        self.flatten_with_trace_budget(&mut work)
159    }
160
161    fn flatten_with_trace_budget(
162        &self,
163        work: &mut FlattenWork,
164    ) -> Result<(BloqTemplate, Vec<FlattenEvent>), FlattenError> {
165        work.charge(self.repeat_states.len())?;
166        work.charge(self.detectors.len())?;
167        work.charge(self.restarts.len())?;
168        work.charge(self.boundary_flows.len())?;
169        for flow in &self.boundary_flows {
170            work.charge(
171                flow.start
172                    .len()
173                    .saturating_add(flow.end.len())
174                    .saturating_add(flow.measurements.len()),
175            )?;
176        }
177        let referenced_bodies = self.repeat_states.iter().map(|state| state.body).chain(
178            self.detectors
179                .iter()
180                .filter_map(|detector| match detector.scope {
181                    TemplateDetectorScope::TopLevel => None,
182                    TemplateDetectorScope::RepeatBody { body } => Some(body),
183                }),
184        );
185        if let Some(body) = referenced_bodies
186            .into_iter()
187            .find(|&body| self.circuit.body(body).is_none())
188        {
189            return Err(CircuitError::InvalidCircuitBody(body).into());
190        }
191
192        let mut circuit = self.circuit.clone();
193        // Expansion was charged before any template was materialized. Its
194        // own preflight still protects callers of this circuit-level API.
195        let trace = circuit.flatten_with_limits(work.limits)?;
196
197        // Index the loop-relative side tables by the repeat body they live in.
198        let mut states_by_body: FxMap<BodyId, Vec<&crate::TemplateRepeatState>> = FxMap::default();
199        for state in &self.repeat_states {
200            states_by_body.entry(state.body).or_default().push(state);
201        }
202        let mut detectors_by_body: FxMap<BodyId, Vec<&TemplateDetector>> = FxMap::default();
203        let mut top_level_detectors = Vec::new();
204        for detector in &self.detectors {
205            match detector.scope {
206                TemplateDetectorScope::TopLevel => top_level_detectors.push(detector),
207                TemplateDetectorScope::RepeatBody { body } => {
208                    detectors_by_body.entry(body).or_default().push(detector);
209                }
210            }
211        }
212
213        // Replay the unroll: `latest` mirrors the emission frame (original id
214        // -> the id of its most recent unrolled occurrence). The state stack
215        // mirrors Stim's lexical loop frames; the base frame is the resolved
216        // top-level state table.
217        let mut latest: FxMap<u32, u32> = FxMap::default();
218        let mut state_stack: Vec<FxMap<LoopStateId, TemplateDetectorParity>> =
219            vec![FxMap::default()];
220        let mut flat_detectors = Vec::new();
221        // Reused across every `IterationEnd`: each `next` must resolve against
222        // the pre-update `states` before any commits, so they stage here first.
223        let mut next_states: Vec<(LoopStateId, TemplateDetectorParity)> = Vec::new();
224        for event in &trace {
225            work.charge(1)?;
226            match *event {
227                FlattenEvent::Measurement { original, emitted } => {
228                    latest.insert(original, emitted);
229                }
230                FlattenEvent::LoopEnter { body } => {
231                    let mut initial_states = FxMap::default();
232                    for state in states_in(&states_by_body, body) {
233                        let initial = resolve_parity(&state.initial, &latest, &state_stack, work)?;
234                        initial_states.insert(state.state, initial);
235                    }
236                    state_stack.push(initial_states);
237                }
238                FlattenEvent::IterationEnd { body } => {
239                    for detector in detectors_by_body.get(&body).into_iter().flatten() {
240                        work.charge(detector.coords.as_ref().map_or(0, DetectorCoords::len))?;
241                        flat_detectors.push(TemplateDetector {
242                            scope: TemplateDetectorScope::TopLevel,
243                            parity: resolve_parity(&detector.parity, &latest, &state_stack, work)?,
244                            coords: detector.coords.clone(),
245                        });
246                    }
247                    // The emitter builds the whole next frame before swapping it
248                    // in, so stage every `next` (against pre-update states), then
249                    // commit.
250                    next_states.clear();
251                    for state in states_in(&states_by_body, body) {
252                        next_states.push((
253                            state.state,
254                            resolve_parity(&state.next, &latest, &state_stack, work)?,
255                        ));
256                    }
257                    // Stim leaves a state-less loop frame untouched during an
258                    // iteration, but a loop with declared states replaces the
259                    // frame wholesale with those next values.
260                    if !next_states.is_empty() {
261                        for (_, parity) in &next_states {
262                            work.charge(parity.terms().len())?;
263                        }
264                        let current = state_stack
265                            .last_mut()
266                            .expect("LoopEnter precedes IterationEnd");
267                        current.clear();
268                        current.extend(next_states.iter().cloned());
269                    }
270                }
271                FlattenEvent::LoopExit { body } => {
272                    let mut final_states = state_stack.pop().expect("LoopEnter precedes LoopExit");
273                    let parent = state_stack
274                        .last_mut()
275                        .expect("the top-level state frame is never popped");
276                    for state in states_in(&states_by_body, body) {
277                        let value = final_states
278                            .remove(&state.state)
279                            .ok_or(FlattenError::UnresolvedLoopState(state.state))?;
280                        parent.insert(state.state, value);
281                    }
282                }
283            }
284        }
285
286        // Top-level parities resolve after everything emitted: measurement
287        // terms already name final occurrences, and any loop-state term takes
288        // the state's post-loop value.
289        for detector in top_level_detectors {
290            work.charge(detector.coords.as_ref().map_or(0, DetectorCoords::len))?;
291            flat_detectors.push(TemplateDetector {
292                scope: TemplateDetectorScope::TopLevel,
293                parity: resolve_parity(&detector.parity, &latest, &state_stack, work)?,
294                coords: detector.coords.clone(),
295            });
296        }
297        let restarts = self
298            .restarts
299            .iter()
300            .map(|restart| {
301                Ok(TemplateRestart {
302                    parity: resolve_parity(&restart.parity, &latest, &state_stack, work)?,
303                })
304            })
305            .collect::<Result<Vec<_>, FlattenError>>()?;
306
307        Ok((
308            BloqTemplate::with_parts(
309                circuit,
310                flat_detectors,
311                Vec::new(),
312                self.boundary_flows.clone(),
313                restarts,
314            ),
315            trace,
316        ))
317    }
318}
319
320/// The repeat states declared for `body`, or nothing when it carries none.
321fn states_in<'a>(
322    by_body: &'a FxMap<BodyId, Vec<&'a crate::TemplateRepeatState>>,
323    body: BodyId,
324) -> impl Iterator<Item = &'a crate::TemplateRepeatState> + 'a {
325    by_body.get(&body).into_iter().flatten().copied()
326}
327
328/// Resolve a parity to concrete measurement ids on the flattened circuit:
329/// measurement terms follow their most recent unrolled occurrence (falling
330/// back to the id itself for measurements outside any loop), loop-state terms
331/// substitute the state's currently resolved parity. Terms and signs compose
332/// by XOR; term cancellation happens in
333/// [`DetectorParity::from_measurements`]'s canonicalization.
334fn resolve_parity(
335    parity: &TemplateDetectorParity,
336    latest: &FxMap<u32, u32>,
337    state_stack: &[FxMap<LoopStateId, TemplateDetectorParity>],
338    work: &mut FlattenWork,
339) -> Result<TemplateDetectorParity, FlattenError> {
340    work.charge(parity.terms().len().saturating_add(1))?;
341    let mut values = Vec::with_capacity(parity.terms().len());
342    let mut sign = parity.sign();
343    for term in parity.terms() {
344        match *term {
345            DetectorTerm::Measurement(measurement) => {
346                values.push(latest.get(&measurement).copied().unwrap_or(measurement));
347            }
348            DetectorTerm::LoopState(state) => {
349                work.charge(state_stack.len())?;
350                let resolved = state_stack
351                    .iter()
352                    .rev()
353                    .find_map(|states| states.get(&state))
354                    .ok_or(FlattenError::UnresolvedLoopState(state))?;
355                sign ^= resolved.sign();
356                work.charge(resolved.terms().len())?;
357                values.extend(resolved.measurements());
358            }
359        }
360    }
361    Ok(DetectorParity::from_measurements(values).with_sign(sign))
362}
363
364impl BloqTemplate {
365    /// Whether [`Self::flatten`] would change anything — i.e. the template
366    /// carries loop structure (a repeat body, a loop-carried recurrence, a
367    /// repeat-scoped detector, or a loop-state parity term). Colocated with
368    /// `flatten` because it must cover exactly what `flatten` rewrites: an
369    /// under-report would leak a `REPEAT` past a whole-program flatten (an
370    /// instance never re-pointed), so a new side table added to `flatten` must
371    /// be reflected here too. Checks run cheapest-first (O(1) before the body
372    /// and side-table walks).
373    fn needs_flatten(&self) -> bool {
374        !self.repeat_states.is_empty()
375            || self
376                .detectors
377                .iter()
378                .any(|detector| detector.scope != TemplateDetectorScope::TopLevel)
379            || self.circuit.has_repeats()
380            || self
381                .detectors
382                .iter()
383                .map(|detector| &detector.parity)
384                .chain(self.restarts.iter().map(|restart| &restart.parity))
385                .any(|parity| {
386                    parity
387                        .terms()
388                        .iter()
389                        .any(|term| matches!(term, DetectorTerm::LoopState(_)))
390                })
391    }
392}
393
394impl Bloq {
395    /// Flatten every repeat block in the program (see the module docs):
396    /// each template an instance references is flattened once via
397    /// [`BloqTemplate::flatten`] (flattened copies are appended to the pool —
398    /// it is append-only — and instances re-pointed), expanding repeat-scoped
399    /// detectors into per-iteration detectors and clearing all loop-carried
400    /// detector state. Instance-space side tables (node detectors,
401    /// observable record reads, boundary operators) stay valid untouched because
402    /// final occurrences keep their measurement ids.
403    ///
404    /// Verify-before-apply: all templates are flattened and all checks run
405    /// before any instance is re-pointed, so the program is unmodified on
406    /// error. Decoder-wait insertion after flattening splices looped padding
407    /// templates again; flatten again afterwards if needed.
408    ///
409    /// # Errors
410    ///
411    /// See [`FlattenError`].
412    pub fn flatten(&mut self) -> Result<(), FlattenError> {
413        self.flatten_with_limits(FlattenLimits::default())
414    }
415
416    /// [`Self::flatten`] with a cumulative allowance across all distinct
417    /// referenced templates and their side tables. Repeated instances share
418    /// one flattened template; an exhausted allowance leaves the Bloq intact.
419    ///
420    /// # Errors
421    ///
422    /// Returns [`FlattenError`] for invalid references or exhausted cumulative
423    /// work; `self` remains unchanged.
424    pub fn flatten_with_limits(&mut self, limits: FlattenLimits) -> Result<(), FlattenError> {
425        let mut work = FlattenWork::new(limits);
426        // Bundle definitions are shared. Check their terms once, and preserve
427        // their handles while instance-local template ids are repointed.
428        for (_, bundle) in self.detector_bundles().iter() {
429            work.charge(
430                bundle
431                    .detectors()
432                    .len()
433                    .saturating_add(bundle.owner_templates().len()),
434            )?;
435            for detector in bundle.detectors() {
436                work.charge(detector.parity.terms().len())?;
437                if let Some(state) = detector.parity.terms().iter().find_map(|term| match term {
438                    DetectorTerm::LoopState(state) => Some(*state),
439                    DetectorTerm::Measurement(_) => None,
440                }) {
441                    return Err(FlattenError::UnresolvedLoopState(state));
442                }
443            }
444        }
445        // Phase 1 (read-only): flatten every referenced template that needs it.
446        let mut memo: FxMap<TemplateId, TemplateId> = FxMap::default();
447        let mut referenced = Vec::new();
448        check_and_collect(self.top(), self, &mut memo, &mut referenced, &mut work)?;
449        if let Some(last) = referenced.len().checked_sub(1) {
450            new_template_id(self.templates().len(), last)?;
451        }
452        // Check the aggregate expansion before materializing even the first
453        // template, so a pool cannot evade the limit by splitting its repeats.
454        for &id in &referenced {
455            work.check_circuit(&self.templates()[id].circuit)?;
456        }
457        let pending = referenced
458            .into_iter()
459            .map(|id| {
460                self.templates()[id]
461                    .flatten_with_trace_budget(&mut work)
462                    .map(|(template, _)| (id, template))
463            })
464            .collect::<Result<Vec<_>, _>>()?;
465        if pending.is_empty() {
466            return Ok(());
467        }
468        for (index, (old, _)) in pending.iter().enumerate() {
469            memo.insert(*old, new_template_id(self.templates().len(), index)?);
470        }
471        let mut replacement_bundles = Vec::new();
472        for (old, bundle) in self.detector_bundles().iter() {
473            work.charge(bundle.owner_templates().len())?;
474            let owners = bundle
475                .owner_templates()
476                .iter()
477                .map(|template| memo.get(template).copied().unwrap_or(*template))
478                .collect::<Vec<_>>();
479            if owners != bundle.owner_templates() {
480                work.charge(bundle.detectors().len())?;
481                for row in bundle.detectors() {
482                    work.charge(
483                        row.parity
484                            .terms()
485                            .len()
486                            .saturating_add(row.coords.as_ref().map_or(0, DetectorCoords::len)),
487                    )?;
488                }
489                replacement_bundles.push((
490                    old,
491                    DetectorBundle::new(owners, bundle.detectors().to_vec()),
492                ));
493            }
494        }
495        // Phase 2 (infallible): install only already-budgeted replacements.
496        if let Some(last) = replacement_bundles.len().checked_sub(1) {
497            let index = self
498                .detector_bundles()
499                .len()
500                .checked_add(last)
501                .ok_or(crate::DetectorBundleError::CountOverflow)?;
502            u32::try_from(index).map_err(|_| crate::DetectorBundleError::CountOverflow)?;
503        }
504        for (_, template) in pending {
505            self.add_template(template);
506        }
507        let mut bundle_memo = FxMap::default();
508        for (old, bundle) in replacement_bundles {
509            bundle_memo.insert(old, self.add_detector_bundle(bundle));
510        }
511        rewrite_level(self.graph_mut_internal(), &memo, &bundle_memo);
512        Ok(())
513    }
514}
515
516fn check_and_collect(
517    level: &SubGraph,
518    program: &Bloq,
519    memo: &mut FxMap<TemplateId, TemplateId>,
520    pending: &mut Vec<TemplateId>,
521    work: &mut FlattenWork,
522) -> Result<(), FlattenError> {
523    let mut levels = vec![level.nodes()];
524    while let Some(nodes) = levels.last_mut() {
525        let Some((_, node)) = nodes.next() else {
526            levels.pop();
527            continue;
528        };
529        work.charge(1)?;
530        match &node.kind {
531            BloqNodeKind::Quantum(quantum) => {
532                work.charge(quantum.detector_bundles.len())?;
533                for use_ in &quantum.detector_bundles {
534                    work.charge(use_.instances.len())?;
535                }
536                program.node_detector_count(quantum)?;
537                // Node-level loop-state terms have no recurrence table and
538                // therefore cannot resolve after template flattening.
539                for parity in quantum.stored_parities() {
540                    work.charge(parity.terms().len())?;
541                    if let Some(state) = parity.terms().iter().find_map(|term| match term {
542                        DetectorTerm::LoopState(state) => Some(*state),
543                        DetectorTerm::Measurement(_) => None,
544                    }) {
545                        return Err(FlattenError::UnresolvedLoopState(state));
546                    }
547                }
548                for instance in &quantum.instances {
549                    work.charge(1)?;
550                    if memo.contains_key(&instance.template_id) {
551                        continue;
552                    }
553                    let template = program
554                        .templates()
555                        .get(instance.template_id)
556                        .ok_or(FlattenError::UnknownTemplate(instance.template_id))?;
557                    work.charge(crate::instantiation::circuit_work(&template.circuit))?;
558                    if template.needs_flatten() {
559                        // The pool id is assigned in phase 2; mark the memo so
560                        // the template is flattened only once.
561                        pending.push(instance.template_id);
562                    }
563                    memo.insert(instance.template_id, instance.template_id);
564                }
565            }
566            BloqNodeKind::Region(region) => {
567                for (_, body) in region.bodies().collect::<Vec<_>>().into_iter().rev() {
568                    levels.push(body.nodes());
569                }
570            }
571            BloqNodeKind::Classical(_) => {}
572        }
573    }
574    Ok(())
575}
576
577fn rewrite_level(
578    graph: &mut petgraph::stable_graph::StableDiGraph<crate::BloqNode, crate::BloqEdge>,
579    memo: &FxMap<TemplateId, TemplateId>,
580    bundle_memo: &FxMap<DetectorBundleId, DetectorBundleId>,
581) {
582    let mut pending = vec![graph];
583    while let Some(graph) = pending.pop() {
584        for node in graph.node_weights_mut() {
585            if node.try_quantum().is_some() {
586                let quantum = node.expect_quantum_mut();
587                for instance in &mut quantum.instances {
588                    instance.template_id = *memo
589                        .get(&instance.template_id)
590                        .expect("phase 1 visited every instance");
591                }
592                for use_ in &mut quantum.detector_bundles {
593                    if let Some(&new) = bundle_memo.get(&use_.bundle) {
594                        use_.bundle = new;
595                    }
596                }
597                continue;
598            }
599            // Walks the same body structure `check_and_collect` checked via
600            // `bodies()`, so the two phases cannot drift.
601            if let BloqNodeKind::Region(region) = &mut node.kind {
602                for (_, body) in region.bodies_mut() {
603                    pending.push(body.graph_mut());
604                }
605            }
606        }
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    #[test]
613    fn flattened_template_ids_do_not_wrap() {
614        let maximum = u32::MAX as usize;
615        assert_eq!(
616            super::new_template_id(maximum, 0),
617            Ok(crate::TemplateId(u32::MAX))
618        );
619        if maximum < usize::MAX {
620            assert_eq!(
621                super::new_template_id(maximum, 1),
622                Err(super::FlattenError::TemplateIdOverflow)
623            );
624        }
625    }
626    use bloq_circuit::{CircuitBody, CoordCircuit, Op, PauliBasis};
627    use glam::ivec2;
628
629    use super::*;
630    use crate::{
631        BloqNode, BundleDetector, BundleMeasurement, DetectorBundleUse, TemplateInstance,
632        TemplateInstanceId, TemplateRepeatState,
633    };
634
635    /// A seed measurement, then `REPEAT 3 { M }`, with the loop recurrence
636    /// `initial = seed`, `next = repeated` and a body detector comparing the
637    /// state against the current round — the memory-template shape the Stim
638    /// emitter's repeat tests use.
639    fn looped_template() -> (BloqTemplate, BodyId, u32, u32) {
640        let qubit = ivec2(0, 0);
641        let mut circuit = CoordCircuit::new();
642        let seed = circuit.measure(PauliBasis::Z, [qubit])[0];
643        let repeated = circuit.reserve_measurement_id(qubit);
644        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
645            basis: PauliBasis::Z,
646            qubits: vec![qubit],
647            measurements: vec![repeated],
648            flip_probability: 0.0,
649        }]));
650        circuit
651            .body_mut(circuit.entry_body())
652            .unwrap()
653            .ops_mut()
654            .push(Op::Repeat {
655                body,
656                repetitions: 3,
657            });
658        let state = LoopStateId(0);
659        let template = BloqTemplate::with_parts(
660            circuit,
661            vec![TemplateDetector {
662                scope: TemplateDetectorScope::RepeatBody { body },
663                parity: DetectorParity::from_terms([
664                    DetectorTerm::Measurement(repeated),
665                    DetectorTerm::LoopState(state),
666                ]),
667                coords: Some(bloq_circuit::DetectorCoords::from_slice(&[1.0, 2.0])),
668            }],
669            vec![TemplateRepeatState {
670                body,
671                state,
672                initial: DetectorParity::from_measurements([seed]),
673                next: DetectorParity::from_measurements([repeated]),
674            }],
675            Vec::new(),
676            Vec::new(),
677        );
678        (template, body, seed, repeated)
679    }
680
681    #[test]
682    fn template_flatten_expands_one_detector_per_iteration() {
683        let (template, _, seed, repeated) = looped_template();
684
685        let flat = template.flatten().unwrap();
686
687        assert!(!flat.circuit.has_repeats());
688        assert!(flat.repeat_states.is_empty());
689        // Fresh ids 2 and 3 are iterations one and two; the final iteration
690        // keeps `repeated`. Consecutive rounds compare, exactly like the
691        // emitter's unrolled `DETECTOR rec[-2] rec[-1]` chain.
692        let expected: Vec<TemplateDetectorParity> = vec![
693            DetectorParity::from_measurements([seed, 2]),
694            DetectorParity::from_measurements([2, 3]),
695            DetectorParity::from_measurements([3, repeated]),
696        ];
697        assert_eq!(
698            flat.detectors
699                .iter()
700                .map(|detector| detector.parity.clone())
701                .collect::<Vec<_>>(),
702            expected
703        );
704        for detector in &flat.detectors {
705            assert_eq!(detector.scope, TemplateDetectorScope::TopLevel);
706            assert_eq!(
707                detector.coords.as_deref(),
708                Some(&[1.0, 2.0][..]),
709                "each iteration copy keeps the detector coords, like the emitter"
710            );
711        }
712    }
713
714    #[test]
715    fn template_flatten_resolves_top_level_references() {
716        let (mut template, _, _, repeated) = looped_template();
717        // A top-level detector naming the repeated id means "its latest
718        // occurrence" — the final iteration, which keeps the original id.
719        template.detectors.push(TemplateDetector {
720            scope: TemplateDetectorScope::TopLevel,
721            parity: DetectorParity::from_measurements([repeated]),
722            coords: None,
723        });
724        // And one reading the loop state after the loop: the state's final
725        // value is the last round's parity.
726        template.detectors.push(TemplateDetector {
727            scope: TemplateDetectorScope::TopLevel,
728            parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
729            coords: None,
730        });
731
732        let flat = template.flatten().unwrap();
733
734        let top = &flat.detectors[3..];
735        assert_eq!(top[0].parity, DetectorParity::from_measurements([repeated]));
736        assert_eq!(top[1].parity, DetectorParity::from_measurements([repeated]));
737    }
738
739    #[test]
740    fn flatten_rejects_undefined_loop_state() {
741        let (mut template, _, _, _) = looped_template();
742        template.detectors.push(TemplateDetector {
743            scope: TemplateDetectorScope::TopLevel,
744            parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(99))]),
745            coords: None,
746        });
747
748        assert!(matches!(
749            template.flatten(),
750            Err(FlattenError::UnresolvedLoopState(LoopStateId(99)))
751        ));
752
753        for restart in [false, true] {
754            let mut bloq = Bloq::new();
755            let mut node = BloqNode::from_members(Vec::new());
756            let mut guard = crate::QuantumGuard::default();
757            let parities = if restart {
758                &mut guard.restart_parities
759            } else {
760                &mut guard.detector_parities
761            };
762            parities.push((
763                0,
764                DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(99))]),
765            ));
766            node.expect_quantum_mut().guards.push(guard);
767            bloq.add_node(node);
768            let original = bloq.to_binary();
769            assert!(matches!(
770                bloq.flatten(),
771                Err(FlattenError::UnresolvedLoopState(LoopStateId(99)))
772            ));
773            assert_eq!(bloq.to_binary(), original);
774        }
775    }
776
777    #[test]
778    fn template_flatten_rejects_dangling_side_table_body() {
779        let template = BloqTemplate::with_parts(
780            CoordCircuit::new(),
781            vec![TemplateDetector {
782                scope: TemplateDetectorScope::RepeatBody { body: BodyId(1) },
783                parity: DetectorParity::default(),
784                coords: None,
785            }],
786            Vec::new(),
787            Vec::new(),
788            Vec::new(),
789        );
790
791        assert!(matches!(
792            template.flatten(),
793            Err(FlattenError::Circuit(CircuitError::InvalidCircuitBody(body)))
794                if body == BodyId(1)
795        ));
796    }
797
798    #[test]
799    fn template_flatten_rejects_same_body_peer_in_initial_state() {
800        let mut circuit = CoordCircuit::new();
801        let body = circuit.add_body(CircuitBody::new());
802        circuit
803            .body_mut(circuit.entry_body())
804            .unwrap()
805            .ops_mut()
806            .push(Op::Repeat {
807                body,
808                repetitions: 1,
809            });
810        let template = BloqTemplate::with_parts(
811            circuit,
812            vec![],
813            vec![
814                TemplateRepeatState {
815                    body,
816                    state: LoopStateId(0),
817                    initial: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(1))]),
818                    next: DetectorParity::default(),
819                },
820                TemplateRepeatState {
821                    body,
822                    state: LoopStateId(1),
823                    initial: DetectorParity::default(),
824                    next: DetectorParity::default(),
825                },
826            ],
827            vec![],
828            vec![],
829        );
830
831        assert!(matches!(
832            template.flatten(),
833            Err(FlattenError::UnresolvedLoopState(LoopStateId(1)))
834        ));
835    }
836
837    #[test]
838    fn template_flatten_drops_nested_state_when_outer_loop_exits() {
839        let mut circuit = CoordCircuit::new();
840        let inner = circuit.add_body(CircuitBody::new());
841        let outer = circuit.add_body(CircuitBody::from_ops(vec![Op::Repeat {
842            body: inner,
843            repetitions: 1,
844        }]));
845        circuit
846            .body_mut(circuit.entry_body())
847            .unwrap()
848            .ops_mut()
849            .push(Op::Repeat {
850                body: outer,
851                repetitions: 1,
852            });
853        let nested = LoopStateId(1);
854        let template = BloqTemplate::with_parts(
855            circuit,
856            vec![TemplateDetector {
857                scope: TemplateDetectorScope::TopLevel,
858                parity: DetectorParity::from_terms([DetectorTerm::LoopState(nested)]),
859                coords: None,
860            }],
861            vec![TemplateRepeatState {
862                body: inner,
863                state: nested,
864                initial: DetectorParity::default(),
865                next: DetectorParity::default(),
866            }],
867            vec![],
868            vec![],
869        );
870
871        assert!(matches!(
872            template.flatten(),
873            Err(FlattenError::UnresolvedLoopState(state)) if state == nested
874        ));
875    }
876
877    #[test]
878    fn bloq_flatten_shares_one_limit_across_distinct_templates() {
879        let mut circuit = CoordCircuit::new();
880        let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
881            gate: bloq_circuit::GateType::H,
882            qubits: vec![ivec2(0, 0)],
883        }]));
884        circuit.push_repeat(body, 20);
885        for shared in [false, true] {
886            let mut bloq = Bloq::new();
887            let first = bloq.add_template(BloqTemplate::new(circuit.clone()));
888            let second = if shared {
889                first
890            } else {
891                bloq.add_template(BloqTemplate::new(circuit.clone()))
892            };
893            let mut node = BloqNode::from_members(Vec::new());
894            node.expect_quantum_mut().instances = vec![
895                TemplateInstance::new(TemplateInstanceId(0), first, ivec2(0, 0)),
896                TemplateInstance::new(TemplateInstanceId(1), second, ivec2(5, 0)),
897            ];
898            let node = bloq.add_node(node);
899            let original = bloq.to_binary();
900            let result = bloq.flatten_with_limits(FlattenLimits { max_work: 160 });
901            if shared {
902                result.expect("instances reuse one flattened template");
903            } else {
904                assert!(matches!(
905                    result,
906                    Err(FlattenError::Circuit(CircuitError::FlattenResourceLimit {
907                        limit: 160,
908                        ..
909                    }))
910                ));
911                assert_eq!(bloq.to_binary(), original);
912                bloq.flatten_with_limits(FlattenLimits { max_work: 320 })
913                    .unwrap();
914            }
915            for instance in &bloq[node].expect_quantum().instances {
916                assert!(!bloq.templates()[instance.template_id].circuit.has_repeats());
917            }
918        }
919    }
920
921    #[test]
922    fn template_flatten_charges_growing_loop_state_parities() {
923        let (mut template, body, seed, measurement) = looped_template();
924        for op in template
925            .circuit
926            .body_mut(template.circuit.entry_body())
927            .unwrap()
928            .ops_mut()
929        {
930            if let Op::Repeat { repetitions, .. } = op {
931                *repetitions = 64;
932            }
933        }
934        let state = template.repeat_states[0].state;
935        template.repeat_states[0].next = DetectorParity::from_terms([
936            DetectorTerm::LoopState(state),
937            DetectorTerm::Measurement(measurement),
938        ]);
939        assert_eq!(template.repeat_states[0].body, body);
940        assert!(matches!(
941            template.flatten_with_limits(FlattenLimits { max_work: 1000 }),
942            Err(FlattenError::Circuit(
943                CircuitError::FlattenResourceLimit { .. }
944            ))
945        ));
946        let flat = template
947            .flatten_with_limits(FlattenLimits { max_work: 100_000 })
948            .unwrap();
949        assert_eq!(flat.detectors.len(), 64);
950        let last = &flat.detectors.last().unwrap().parity;
951        assert_eq!(last.measurements().count(), 65);
952        assert!(last.measurements().any(|measurement| measurement == seed));
953    }
954
955    #[test]
956    fn bloq_flatten_repoints_instances_and_skips_flat_templates() {
957        let (template, _, _, repeated) = looped_template();
958        let mut flat_circuit = CoordCircuit::new();
959        flat_circuit.measure(PauliBasis::Z, [ivec2(5, 0)]);
960
961        let mut bloq = Bloq::new();
962        let looped_id = bloq.add_template(template);
963        let flat_id = bloq.add_template(BloqTemplate::new(flat_circuit));
964        let bundle = bloq.add_detector_bundle(DetectorBundle::new(
965            vec![looped_id],
966            vec![BundleDetector {
967                parity: DetectorParity::from_measurements([BundleMeasurement {
968                    owner: 0,
969                    measurement: repeated,
970                }]),
971                coords: None,
972            }],
973        ));
974        let mut node = BloqNode::from_members(vec![]);
975        node.expect_quantum_mut().instances = vec![
976            TemplateInstance::new(TemplateInstanceId(0), looped_id, ivec2(0, 0)),
977            TemplateInstance::new(TemplateInstanceId(1), flat_id, ivec2(5, 0)),
978        ];
979        node.expect_quantum_mut()
980            .detector_bundles
981            .push(DetectorBundleUse {
982                bundle,
983                instances: vec![TemplateInstanceId(0)],
984                offset: ivec2(0, 0),
985            });
986        let node_id = bloq.add_node(node);
987
988        let original = bloq.to_text();
989        assert!(
990            bloq.flatten_with_limits(FlattenLimits { max_work: 1 })
991                .is_err()
992        );
993        assert_eq!(
994            bloq.to_text(),
995            original,
996            "resource failure leaves bundles and instances untouched"
997        );
998
999        bloq.flatten().unwrap();
1000
1001        let instances = &bloq[node_id].expect_quantum().instances;
1002        let use_ = &bloq[node_id].expect_quantum().detector_bundles[0];
1003        assert_eq!(
1004            bloq.detector_bundles()
1005                .get(use_.bundle)
1006                .unwrap()
1007                .owner_templates(),
1008            &[instances[0].template_id]
1009        );
1010        assert_eq!(
1011            bloq.node_detectors(bloq[node_id].expect_quantum())
1012                .unwrap()
1013                .last()
1014                .unwrap()
1015                .measurements()
1016                .next()
1017                .unwrap()
1018                .measurement,
1019            repeated
1020        );
1021        for restored in [
1022            Bloq::from_text(&bloq.to_text()).unwrap(),
1023            Bloq::from_binary(&bloq.to_binary()).unwrap(),
1024        ] {
1025            assert_eq!(
1026                restored
1027                    .detector_bundles()
1028                    .get(restored[node_id].expect_quantum().detector_bundles[0].bundle)
1029                    .unwrap()
1030                    .owner_templates(),
1031                &[instances[0].template_id]
1032            );
1033        }
1034        let flattened_id = instances[0].template_id;
1035        assert_ne!(flattened_id, looped_id, "looped template re-pointed");
1036        assert_eq!(instances[1].template_id, flat_id, "flat template untouched");
1037        let flattened = bloq.templates().get(flattened_id).unwrap();
1038        assert!(!flattened.circuit.has_repeats());
1039        assert!(flattened.repeat_states.is_empty());
1040        assert_eq!(flattened.detectors.len(), 3);
1041
1042        let snapshot = bloq.clone();
1043        bloq.flatten().unwrap();
1044        assert!(std::ptr::eq(bloq.graph(), snapshot.graph()));
1045    }
1046}