Skip to main content

bloq_ir/ir/
template.rs

1use std::ops::Index;
2use std::sync::Arc;
3
4use bloq_circuit::{BodyId, CoordCircuit, DetectorCoords, Flow, LoopStateId};
5use glam::{IVec2, IVec3};
6
7use super::{NodeDetectorParity, TemplateDetectorParity, TemplateId, TemplateInstanceId};
8use crate::instantiation::{
9    NodeTemplateInstanceMergeError, TemplateCircuitAnalysis, preflight_template_circuit,
10};
11
12/// A reusable circuit template and its local side tables.
13///
14/// Together they form the connection interface a persisted `.bloq` is edited against.
15/// Pooled and shared by [`TemplateId`]; a [`TemplateInstance`] places one at an
16/// `offset`.
17#[derive(Debug, serde::Serialize, serde::Deserialize)]
18pub struct BloqTemplate {
19    /// The template's circuit, in template-local qubit coordinates.
20    pub circuit: CoordCircuit,
21    /// Detectors closing entirely within this template.
22    pub detectors: Vec<TemplateDetector>,
23    /// Loop-carried detector recurrences for the circuit's repeat bodies.
24    pub repeat_states: Vec<TemplateRepeatState>,
25    /// The template's residual open flows — the only detectors instance lowering
26    /// must compose, since they close against a temporally adjacent node. The
27    /// retained connection interface that makes a persisted `.bloq` editable
28    /// (load → edit → recompose).
29    ///
30    /// A flat list of **fused** boundary chains: each is one maximal open chain
31    /// spliced into a single `start -> end` flow carrying all its measurements,
32    /// in template-measurement-id space. Cross-chunk chains are threaded once at
33    /// build time, so entries are mutually independent (no entry's `end` is
34    /// another's `start`). A loop's recurrence lives in `repeat_states`, not
35    /// here. Computed by the lowering flow pass and deduped per signature.
36    pub boundary_flows: Vec<Flow>,
37    /// Restart syndromes for `RepeatUntilSuccess` regions instantiating this
38    /// template. The executor evaluates them after each sandboxed attempt.
39    pub restarts: Vec<TemplateRestart>,
40    /// Lazily computed cache of the circuit's qubits (hot in footprint
41    /// queries). Safe because templates are immutable once pooled; a template
42    /// whose `circuit` is still being mutated must not call [`Self::qubits`].
43    #[serde(skip)]
44    qubit_cache: std::sync::OnceLock<Vec<IVec2>>,
45    /// Lazily cached structural circuit analysis. Safe under the same immutable
46    /// pooled-template contract as [`Self::qubits`].
47    #[serde(skip)]
48    circuit_analysis_cache:
49        std::sync::OnceLock<Result<TemplateCircuitAnalysis, NodeTemplateInstanceMergeError>>,
50    /// Successful validation of this immutable template's side tables.
51    #[serde(skip)]
52    validation_cache: std::sync::OnceLock<()>,
53}
54
55impl Clone for BloqTemplate {
56    fn clone(&self) -> Self {
57        Self {
58            circuit: self.circuit.clone(),
59            detectors: self.detectors.clone(),
60            repeat_states: self.repeat_states.clone(),
61            boundary_flows: self.boundary_flows.clone(),
62            restarts: self.restarts.clone(),
63            qubit_cache: std::sync::OnceLock::new(),
64            circuit_analysis_cache: std::sync::OnceLock::new(),
65            validation_cache: std::sync::OnceLock::new(),
66        }
67    }
68}
69
70impl PartialEq for BloqTemplate {
71    fn eq(&self, other: &Self) -> bool {
72        self.circuit == other.circuit
73            && self.detectors == other.detectors
74            && self.repeat_states == other.repeat_states
75            && self.boundary_flows == other.boundary_flows
76            && self.restarts == other.restarts
77    }
78}
79
80impl BloqTemplate {
81    /// Create a template containing only `circuit`.
82    pub fn new(circuit: CoordCircuit) -> Self {
83        Self::with_parts(circuit, Vec::new(), Vec::new(), Vec::new(), Vec::new())
84    }
85
86    /// Create a template with all side tables supplied explicitly.
87    pub fn with_parts(
88        circuit: CoordCircuit,
89        detectors: Vec<TemplateDetector>,
90        repeat_states: Vec<TemplateRepeatState>,
91        boundary_flows: Vec<Flow>,
92        restarts: Vec<TemplateRestart>,
93    ) -> Self {
94        Self {
95            circuit,
96            detectors,
97            repeat_states,
98            boundary_flows,
99            restarts,
100            qubit_cache: std::sync::OnceLock::new(),
101            circuit_analysis_cache: std::sync::OnceLock::new(),
102            validation_cache: std::sync::OnceLock::new(),
103        }
104    }
105
106    /// The circuit's qubits, computed once per template (the circuit walk is
107    /// expensive and footprint queries re-visit templates heavily).
108    pub fn qubits(&self) -> &[IVec2] {
109        self.qubit_cache
110            .get_or_init(|| self.circuit.qubits().into_iter().collect())
111    }
112
113    pub(crate) fn circuit_analysis(
114        &self,
115    ) -> Result<&TemplateCircuitAnalysis, NodeTemplateInstanceMergeError> {
116        self.circuit_analysis_cache
117            .get_or_init(|| preflight_template_circuit(&self.circuit))
118            .as_ref()
119            .map_err(Clone::clone)
120    }
121
122    pub(crate) fn validation_cached(&self) -> bool {
123        self.validation_cache.get().is_some()
124    }
125
126    pub(crate) fn cache_validation(&self) {
127        self.validation_cache.get_or_init(|| ());
128    }
129}
130
131/// A shared, append-only template pool.
132///
133/// Templates are keyed by [`TemplateId`] (the insertion index). Ids are never
134/// reused or renumbered, so anything that
135/// stores a `TemplateId` outside the pool — [`crate::PipePadding`], the
136/// decoder-wait specialization memo — can rely on it staying valid for the
137/// program's life.
138#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
139pub struct BloqTemplatePool {
140    #[serde(deserialize_with = "super::id::deserialize_pool")]
141    templates: Vec<Arc<BloqTemplate>>,
142}
143
144impl BloqTemplatePool {
145    /// Create an empty template pool.
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Insert an owned template and return its append-only id.
151    ///
152    /// # Panics
153    ///
154    /// Panics before insertion if the next id exceeds `u32`.
155    pub fn insert(&mut self, mut template: BloqTemplate) -> TemplateId {
156        template.qubit_cache = std::sync::OnceLock::new();
157        template.circuit_analysis_cache = std::sync::OnceLock::new();
158        template.validation_cache = std::sync::OnceLock::new();
159        self.insert_shared(Arc::new(template))
160    }
161
162    /// Unlike [`Self::insert`], warmed qubit, circuit-analysis, and validation caches are
163    /// kept: an `Arc`-shared template is already immutable (the pool hands out
164    /// no `&mut`), so they cannot be stale. Clearing them here would deep-clone
165    /// every template the caller shares with a live pool.
166    ///
167    /// # Panics
168    ///
169    /// Panics before insertion if the next id exceeds `u32`.
170    pub fn insert_shared(&mut self, template: Arc<BloqTemplate>) -> TemplateId {
171        let id = TemplateId(super::id::pool_index(self.templates.len()));
172        self.templates.push(template);
173        id
174    }
175
176    /// Look up a template by id.
177    pub fn get(&self, id: TemplateId) -> Option<&BloqTemplate> {
178        self.templates.get(id.0 as usize).map(Arc::as_ref)
179    }
180
181    /// Mutable access to a pooled template, for the few edits that retune a
182    /// template in place rather than pooling a new one.
183    ///
184    /// Copy-on-write: a template shared with another pool (via
185    /// [`Self::insert_shared`]) is cloned first, so the edit cannot reach
186    /// across programs. Its lazy caches describe the circuit that is about to
187    /// change, so they are dropped — the pool's usual immutability contract,
188    /// which is what lets those caches exist, is suspended for exactly this
189    /// call.
190    pub(crate) fn make_mut(&mut self, id: TemplateId) -> Option<&mut BloqTemplate> {
191        let template = Arc::make_mut(self.templates.get_mut(id.0 as usize)?);
192        template.qubit_cache = std::sync::OnceLock::new();
193        template.circuit_analysis_cache = std::sync::OnceLock::new();
194        template.validation_cache = std::sync::OnceLock::new();
195        Some(template)
196    }
197
198    /// Iterate over templates in id order.
199    pub fn iter(&self) -> impl Iterator<Item = (TemplateId, &BloqTemplate)> {
200        self.iter_shared()
201            .map(|(id, template)| (id, template.as_ref()))
202    }
203
204    /// Borrow immutable template handles for importing into another pool
205    /// without copying their circuits or discarding cached analyses.
206    pub fn iter_shared(&self) -> impl Iterator<Item = (TemplateId, &Arc<BloqTemplate>)> {
207        self.templates
208            .iter()
209            .enumerate()
210            .map(|(index, template)| (TemplateId(super::id::pool_index(index)), template))
211    }
212
213    /// Number of templates.
214    pub fn len(&self) -> usize {
215        self.templates.len()
216    }
217
218    /// Whether the pool contains no templates.
219    pub fn is_empty(&self) -> bool {
220        self.templates.is_empty()
221    }
222}
223
224impl Index<TemplateId> for BloqTemplatePool {
225    type Output = BloqTemplate;
226
227    fn index(&self, index: TemplateId) -> &Self::Output {
228        self.templates[index.0 as usize].as_ref()
229    }
230}
231
232/// One detector inside a template: a measurement `parity` at optional decoder
233/// `coords`, tagged by the body `scope` it lives in.
234#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
235pub struct TemplateDetector {
236    /// Circuit body containing the detector.
237    pub scope: TemplateDetectorScope,
238    /// Template-local detector parity.
239    pub parity: TemplateDetectorParity,
240    /// Optional decoder coordinates.
241    pub coords: Option<DetectorCoords>,
242}
243
244/// Where a [`TemplateDetector`] lives: the template's top level, or inside one
245/// of its repeat bodies.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
247pub enum TemplateDetectorScope {
248    /// Detector lives in the entry body.
249    TopLevel,
250    /// Detector lives in a repeat body.
251    RepeatBody {
252        /// Repeat body id.
253        body: BodyId,
254    },
255}
256
257/// A loop-carried detector recurrence for one repeat body: the `initial`
258/// parity on the first iteration and the `next` parity relating consecutive
259/// iterations of loop `state`.
260#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
261pub struct TemplateRepeatState {
262    /// Repeat body carrying the state.
263    pub body: BodyId,
264    /// Loop-state id.
265    pub state: LoopStateId,
266    /// First-iteration parity.
267    pub initial: TemplateDetectorParity,
268    /// Recurrence parity for later iterations.
269    pub next: TemplateDetectorParity,
270}
271
272/// A template-local restart syndrome for a `RepeatUntilSuccess` region.
273///
274/// Reuses [`TemplateDetectorParity`] — a restart syndrome
275/// is just another template-local parity — so instance lowering translates it
276/// like any other side-table entry. A named side table (like `detectors` /
277/// `repeat_states`) rather than a bare `Vec<TemplateDetectorParity>` so a
278/// bounded-retry scope/bound field can land here without a signature churn.
279#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
280pub struct TemplateRestart {
281    /// Template-local measurement parity that, when odd (failure syndrome),
282    /// restarts the enclosing RUS attempt.
283    pub parity: TemplateDetectorParity,
284}
285
286/// An instance-space detector on a [`crate::QuantumNode`].
287///
288/// This is the [`NodeDetectorParity`] analogue of [`TemplateDetector`], for chains that close across the node's
289/// instances rather than within a single template.
290#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
291pub struct NodeDetector {
292    /// Instance-space detector parity.
293    pub parity: NodeDetectorParity,
294    /// Optional decoder coordinates.
295    pub coords: Option<DetectorCoords>,
296}
297
298/// A `RepeatUntilSuccess` restart syndrome composed across the template seam:
299///
300/// the instance-measurement-space analogue of [`TemplateRestart`],
301/// exactly as [`NodeDetector`] is of [`TemplateDetector`]. Produced when a
302/// post-selected parity's chain crosses two template instances (e.g. an escape
303/// template's first merge round comparing against the cultivation template's
304/// last syndrome round), so neither template's local `restarts` table can carry
305/// it. Only meaningful on a node inside a RUS body; validation rejects it
306/// elsewhere. No coords — restart parities are never decoder food.
307#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
308pub struct NodeRestart {
309    /// Instance-measurement parity that, when odd (failure syndrome), restarts
310    /// the enclosing RUS attempt.
311    pub parity: NodeDetectorParity,
312}
313
314/// Memory-padding provenance for one temporal pipe.
315///
316/// This identifies the precompiled templates [`crate::Bloq::insert_memory_rounds`] instantiates on
317/// the pipe's seam, and the layout offset placing them over its cross-section.
318/// Edge scope matters for a temporal Hadamard, whose parent and child seams use
319/// different adjacent cube faces. A region's outgoing edge may instead carry
320/// the terminal-face templates used to append padding inside that region.
321///
322/// Which pipe this describes is the [`crate::PipeSeam`] that owns it.
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
324pub struct PipePadding {
325    /// Layout offset positioning the templates over the pipe's cross-section.
326    pub offset: IVec2,
327    /// Template implementing exactly one memory round (no repeat body).
328    pub one_round: TemplateId,
329    /// Template implementing `1 + r` rounds as a first round plus a
330    /// `REPEAT r` body. Side tables and boundary flows are repetition-count
331    /// independent, so an edit specializes it to any `rounds >= 2` by cloning
332    /// and rewriting `r` to `rounds - 1`.
333    pub looped: TemplateId,
334}
335
336/// Why a template instance exists.
337#[derive(
338    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
339)]
340pub enum InstanceProvenance {
341    /// Direct source instance.
342    #[default]
343    Source,
344    /// Instance emitted for a source block.
345    Block {
346        /// Source block position.
347        source: IVec3,
348    },
349    /// Instance emitted for a source pipe.
350    Pipe {
351        /// Pipe source.
352        src: IVec3,
353        /// Pipe destination.
354        dst: IVec3,
355    },
356    /// Instance emitted while replacing a spatial port.
357    SpatialPortSubstitution {
358        /// Source port position.
359        source: IVec3,
360        /// Source port role.
361        role: bloq_utils::PortRole,
362        /// Realized substitution half.
363        part: SpatialPortPart,
364    },
365}
366
367impl InstanceProvenance {
368    /// Whether this instance belongs to a spatial-port substitution.
369    #[must_use]
370    pub const fn is_spatial_port_substitution(self) -> bool {
371        matches!(self, Self::SpatialPortSubstitution { .. })
372    }
373}
374
375/// Which half of a spatial-Port substitution an instance realizes.
376#[derive(
377    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
378)]
379pub enum SpatialPortPart {
380    /// Replacement cube.
381    Cube,
382    /// Replacement temporal port.
383    TemporalPort,
384}
385
386/// One placement of a template: template `template_id` from the shared pool,
387/// laid down at layout `offset`, under program-global id `id`.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
389pub struct TemplateInstance {
390    /// Program-global instance id.
391    pub id: TemplateInstanceId,
392    /// Placed template.
393    pub template_id: TemplateId,
394    /// Layout offset.
395    pub offset: IVec2,
396    /// Source provenance.
397    #[serde(default)]
398    pub provenance: InstanceProvenance,
399}
400
401impl TemplateInstance {
402    /// Create a source-provenance placement.
403    #[must_use]
404    pub const fn new(id: TemplateInstanceId, template_id: TemplateId, offset: IVec2) -> Self {
405        Self {
406            id,
407            template_id,
408            offset,
409            provenance: InstanceProvenance::Source,
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416
417    use bloq_circuit::CoordCircuit;
418    use glam::ivec2;
419
420    use super::{BloqTemplate, BloqTemplatePool};
421
422    #[test]
423    fn owned_insertion_clears_warmed_template_caches() {
424        use bloq_circuit::{Op, PauliBasis};
425
426        let mut template = BloqTemplate::new(CoordCircuit::new());
427        template.circuit.register_measurement_id(0, ivec2(0, 0));
428        assert!(template.qubits().contains(&ivec2(0, 0)));
429        template.circuit_analysis().unwrap();
430        template.cache_validation();
431        template.circuit.register_measurement_id(1, ivec2(1, 0));
432        template
433            .circuit
434            .body_mut(template.circuit.entry_body())
435            .unwrap()
436            .ops_mut()
437            .push(Op::Measure {
438                basis: PauliBasis::Z,
439                qubits: vec![ivec2(2, 0)],
440                measurements: vec![2],
441                flip_probability: 0.0,
442            });
443
444        let mut pool = BloqTemplatePool::new();
445        let id = pool.insert(template);
446
447        assert!(pool[id].qubits().contains(&ivec2(1, 0)));
448        assert!(matches!(
449            pool[id].circuit_analysis(),
450            Err(crate::NodeTemplateInstanceMergeError::UnregisteredMeasurementOutput(2))
451        ));
452        assert!(!pool[id].validation_cached());
453    }
454}