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}